Post

๐Ÿ’Ž 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.