๐ Redis Caching
๐ Redis Caching
Redis
๋์
๊ทผ๊ฑฐ
- ์ฌ๋ฌ ์๋น์ค์์ ์์ฃผ ์กฐํํ๋ ๋ฐ์ดํฐ์ ๋ํด
Caching
์ฒ๋ฆฌ๊ฐ ํ์ํ๋ค.
Redis
Value
์ฒ๋ฆฌ ๋ฐฉ์
โ ๋ฌธ์์ด ๋ฐฉ์
1
2
"user:test@example.com" =>
"{\"email\":\"test@example.com\", \"name\":\"ํ๊ธธ๋\", \"service\":\"helpdesk\"}"
JSON
๋๋ ์ง๋ ฌํ ๋ ๊ฐ์ฒดValue
๋ฅผ ํ๋์ ๋ฌธ์์ด๋ก ์ฒ๋ฆฌํ๋ค.- ๋จ์ํ๊ณ ๋น ๋ฅด๋ฉฐ, ํ ๋ฒ์ ์ ์ฅํ๊ณ ์กฐํ๋ฅผ ํ๋ ๊ฒ์ด ๊ฐ๋ฅํ๋ค.
- ํ์ง๋ง ๋ด๋ถ ํ๋๋ณ ์ ๊ทผ์ ํ๋ ค๋ฉด ํ์ฑ ์ฒ๋ฆฌ๋ฅผ ์ถ๊ฐ์ ์ผ๋ก ํด์ผ ํ๊ณ ์ ๋ ฌ, ์กฐ๊ฑด๋ณ ํํฐ๋ง์ด ์ด๋ ต๋ค.
โ ํด์ ๋ฐฉ์
1
2
3
4
HMSET user:test@example.com
ย ย emailย ย ย ย ย ย test@example.com
ย ย nameย ย ย ย ย ย ย ย ํ๊ธธ๋
ย ย serviceย ย ย ย helpdesk
- ํ๋ ๋จ์๋ก ์ ๊ทผ์ด ๊ฐ๋ฅํ๊ธฐ ๋๋ฌธ์ ํน์ ํ๋ ๊ธฐ์ค์ผ๋ก ์ ๋ ฌ๋ ๊ฐ๋ฅํจ
- ๋ง์ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํ ๋ ๋ฉ๋ชจ๋ฆฌ ํจ์จ ๋ฉด์์ ์ ๋ฆฌํ๋ค.
- ํ๋์ ํ๋๋ง ์์ ํ๊ฑฐ๋ ์กฐํ ๊ฐ๋ฅํ๋ค.
- ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ์ง๋ ฌํ, ์ญ์ง๋ ฌํํ๋ ค๋ฉด ๋ฐ๋ก
Mapping
์์ ์ด ํ์ํ๋ฐ, ์ด ์์ ์ยRedisTemplate
์ด ์๋์ผ๋ก ์ฒ๋ฆฌํด ์ค๋ค.
์์ฑ ์ฝ๋
โ ์ค์ ํ์ผ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
- ํด๋น
Template
์Key
,Value
์ ์ง๋ ฌํ์ ์ญ์ง๋ ฌํ, ์ธ๋ถ ์์คํ ์ผ๋ก์ ๋ฐ์ดํฐ ์ ์ก์ ๋ด๋นํ๋ค. Redis Server
์ ์ฐ๊ฒฐ์ ๊ด๋ฆฌํ๋LettuceConnectionFactory
๊ตฌํ์ฒด๋ฅผConnection Factory
๋ก ์ค์ ํ์๋ค.
โ
Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@PostMapping
public ResponseEntity<Void> saveUser(@RequestBody @Validated UserDto user) {
service.setUser(user);
return ResponseEntity.ok().build();
}
@GetMapping("/all")
public ResponseEntity<Page<UserDto>> getAllUserPage(
@PageableDefault(
size = 10, sort = "service", direction = Sort.Direction.DESC
) Pageable pageable
) {
Page<UserDto> allUserListPage = service.getAllUserListPage(pageable);
return ResponseEntity.ok(allUserListPage);
}
โ
Service
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Service
@RequiredArgsConstructor
public class UserService {
private final RedisTemplate<String, Object> template;
private static final String USER_KEY_PREFIX = "user:";
public void setUser(UserDto dto) {
String key = "user:" + dto.getEmail();
template.opsForHash().put(key, "email", dto.getEmail());
template.opsForHash().put(key, "name", dto.getName());
template.opsForHash().put(key, "description", dto.getDescription());
template.opsForHash().put(key, "phoneNumber", dto.getPhoneNumber());
template.opsForHash().put(key, "service", dto.getService());
template.opsForSet().add("user:keys", key);
}
public Page<UserDto> getAllUserListPage(Pageable pageable) {
// 1. ๋ชจ๋ ํค ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
Set<Object> keySet = template.opsForSet().members("user:keys");
// 2. ๊ฐ์ ธ์จ ํค ๋ชฉ๋ก์ด ๋น์๋ค๋ฉด ๋น ํ์ด์ง ๋ฐํ
if (keySet == null || keySet.isEmpty()) return Page.empty(pageable);
// 3. ์ ๋ ฌ ์ฒ๋ฆฌ
List<String> allKeys = keySet.stream()
.map(Object::toString)
.map(key -> Map.entry(key, template.opsForHash().get(key, "service")))
.sorted(
Comparator.comparing(
entry -> (String) entry.getValue(),
Comparator.nullsLast(String::compareTo)
)
)
.map(Map.Entry::getKey)
.toList();
// 4. ํ์ด์ง ์ฒ๋ฆฌ
int start = (int) pageable.getOffset();
int end = Math.min(start + pageable.getPageSize(), allKeys.size());
List<String> pagedKeys = allKeys.subList(start, end);
// 5. ๋ฐํ ์ฌ์ฉ์ ๋ชฉ๋ก ์์ฑ
List<UserDto> userList = pagedKeys.stream()
.map(key -> template.opsForHash().entries(key))
.map(map -> {
UserDto dto = new UserDto();
dto.setEmail((String) map.get("email"));
dto.setName((String) map.get("name"));
dto.setDescription((String) map.get("description"));
dto.setPhoneNumber((String) map.get("phoneNumber"));
dto.setService((String) map.get("service"));
return dto;
})
.toList();
// 6. ๋ฐํ
return new PageImpl<>(userList, pageable, allKeys.size());
}
}
- ๋ฑ๋ก ๋ฉ์๋์์
opsForHash()
๋ฉ์๋๋ฅผ ๋ด๋ถ์ ์ผ๋ก ์ดํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* RedisTemplate.class */
private final HashOperations<K, ?, ?> hashOps = new DefaultHashOperations(this);
public <HK, HV> HashOperations<K, HK, HV> opsForHash() {
return this.hashOps;
}
/* DefaultHashOperations.class */
public void put(K key, HK hashKey, HV value) {
byte[] rawKey = this.rawKey(key);
byte[] rawHashKey = this.rawHashKey(hashKey);
byte[] rawHashValue = this.rawHashValue(value);
this.execute((connection) -> {
connection.hSet(rawKey, rawHashKey, rawHashValue);
return null;
});
}
RedisTemplate
์ดKey
,HashKey
,HashValue
๋ฅผ ๋ด๋ถ์ ์ผ๋ก ์ง๋ ฌํ ํ๋ค.- ์ง๋ ฌํ ํ ๊ฐ์
execute()
๋ฉ์๋๋ฅผ ํตํดRedis
๋ก ์ ์กํ๋ ๊ฒ์ด๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
// 1. ๋ชจ๋ ํค ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
Set<Object> keySet = template.opsForSet().members("user:keys");
// 2. ๊ฐ์ ธ์จ ํค ๋ชฉ๋ก์ด ๋น์๋ค๋ฉด ๋น ํ์ด์ง ๋ฐํ
if (keySet == null || keySet.isEmpty()) return Page.empty(pageable);
// 3. ์ ๋ ฌ ์ฒ๋ฆฌ
List<String> allKeys = keySet.stream()
.map(Object::toString)
.map(key -> Map.entry(key, template.opsForHash().get(key, "service")))
// ...
- ์ ์ฝ๋๋ ๋ชจ๋
Key
๋ชฉ๋ก์ ๊ฐ์ ธ์ ์ ๋ ฌ ์ฒ๋ฆฌ๋ฅผ ํ๋ ์ฝ๋์ ์ผ๋ถ๋ถ์ด๋ค. 1
๋ฒ์์Set<Object>
๋กKey
๋ชฉ๋ก์ ์ ์ธ ๋ฐ ์ด๊ธฐํํ๊ธฐ ๋๋ฌธ์.map(Object::toString)
๋ฉ์๋๋ฅผ ํตํดStream
๋ด๋ถ์์Object
๋ฅผString
์ผ๋ก ๋ณํํ๋ค..map(key -> Map.entry(key, template.opsForHash().get(key, "service")))
๋ผ์ธ์ ๋ณด๋ฉดStream
๋ด๋ถ์์Key
์HashKey
๋ฅผ ํตํด ๊ฐ์ ์ถ์ถํ๋ ๋ถ๋ถ์์ ์ ์ ์๋ค.- ๋ด๋ถ ์ฝ๋๋ฅผ ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ๋ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* RedisTemplate.class */
private final HashOperations<K, ?, ?> hashOps = new DefaultHashOperations(this);
public <HK, HV> HashOperations<K, HK, HV> opsForHash() {
return this.hashOps;
}
/* DefaultHashOperations.class */
public HV get(K key, Object hashKey) {
byte[] rawKey = this.rawKey(key);
byte[] rawHashKey = this.rawHashKey(hashKey);
byte[] rawHashValue = (byte[])this.execute((connection) -> connection.hGet(rawKey, rawHashKey));
return (HV)(rawHashValue != null ? this.deserializeHashValue(rawHashValue) : null);
}
return (HV)(rawHashValue != null ? this.deserializeHashValue(rawHashValue) : null);
์ด ๋ถ๋ถ์ด ์ญ์ง๋ ฌํ๊ฐ ์ผ์ด๋๋ ๋ผ์ธ์ด๋ค.rowHashValue
๋Redis
์์ ๊ฐ์ ธ์จbyte[]
ํ์ ์ ๋ฐ์ดํฐ๋ก,Generic
ํ์ ์ธHV
๋ก ๋ฐ๊พธ๋ ค๋ฉด ์ญ์ง๋ ฌํ๊ฐ ํ์ํ๋ค.
ํ์ด์ง ์ง๋ ฌํ ๊ฒฝ๊ณ
1
2
3
Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure!
For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO))
or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.
Spring
์์PageImpl
๊ฐ์ฒด๋ฅผ ์ง์ JSON
์ผ๋ก ์ง๋ ฌํ ํ ๊ฒฝ์ฐ ์ ๊ฒฝ๊ณ ๊ฐ ๋ฌ๋ค.- ์ค์ ํ์ผ์ ๋ค์
Annotation
์ ์ถ๊ฐํ๋ฉด ๋๋ค.
1
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
PageImpl
์ ์๋์ผ๋กDTO
๊ตฌ์กฐ๋ก ์ง๋ ฌํ ํด์ ์์ ์ ์ธJSON
๋ฐ์ดํฐ๋ฅผ ๋ง๋ค์ด ์ค๋ค.
ํ๊ณ
- ๋ฉ์์ด์ฌ์์ฒ๋ผ ๋ฐฑ์๋ ๋ถํธ์บ ํ ํ๋ฌ์ค 4๊ธฐ ๊ฐ์ฌ๋๊ป โ๋ด๋ถ ์ฝ๋๋ฅผ ๊น๋ณด๋ ๊ฒ์ด ์ข๋คโ๋ ๋ด์ฉ์ ๋ฐฐ์ ๋๋ฐ, ์ด๋ฒ์ ์ง์ ์ค์ตํด๋ณด๋ ๋ก์ง์ ์ดํดํ๋ ์ธก๋ฉด์์ ํ์ํ ์์ ์์ ์ค๊ฐํ๋ค.
This post is licensed under CC BY 4.0 by the author.