Spring Boot์์ Redis ์บ์ ์ ์ฉ ์ ๋ต๊ณผ ์ค์ ๊ตฌํ: ์บ์ ํํธ์จ 90% ๋ฌ์ฑํ๊ธฐ
API ์๋ต ์๋๊ฐ ๋๋ฆฌ๊ฑฐ๋ DB ๋ถํ๊ฐ ๋์ ๋ ๊ฐ์ฅ ๋จผ์ ๊ณ ๋ ค๋๋ ํด๊ฒฐ์ฑ
์ค ํ๋๋ ์บ์(Cache)์
๋๋ค.
ํ์ง๋ง ๋จ์ํ Redis๋ฅผ ๋ถ์๋ค๊ณ ํด์ ์ฑ๋ฅ์ด ๋ฌด์กฐ๊ฑด ์ข์์ง์ง ์์ต๋๋ค.
์บ์ ํํธ์จ(Cache Hit Rate)์ด ๋ฎ์ผ๋ฉด Redis๊ฐ ์์ด๋ ํจ๊ณผ๋ ๋ฏธ๋ฏธํ์ฃ .
์ด๋ฒ ๊ธ์์๋ ๋ค์ ๋ด์ฉ์ ๋ค๋ฃน๋๋ค:
@Cacheable
, @CacheEvict
์ค์ ์ฝ๋์บ์๋ Controller์ ์ง์ ์ ์ฉํ ์๋ ์๊ณ , Service ๊ณ์ธต์ ์ ์ฉํ ์๋ ์์ต๋๋ค.
ํ์ง๋ง ๊ฐ ๊ณ์ธต์ ์ฑ
์(SOC: Separation of Concerns)์ ๊ณ ๋ คํ๋ฉด Service ๊ณ์ธต์ด ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ๋ ์ ์ ํฉ๋๋ค.
ํญ๋ชฉ | Controller์์ ์บ์ ์ ์ฉ ์ |
---|---|
โ ์ฅ์ | ๋น ๋ฅธ ์๋ต ๊ฐ๋ฅ (์บ์ → ๋ฐ๋ก ๋ฆฌํด) ๋จ์ JSON ์บ์์ ์ ๋ฆฌ |
โ ๋จ์ | ๋น์ฆ๋์ค ๋ก์ง ์ค๋ณต ๊ด์ฌ์ฌ ๋ถ๋ฆฌ ์นจํด ํ ์คํธ ์ด๋ ค์ ์ฌ์ฌ์ฉ์ฑ ๋ฎ์ |
Controller ์บ์ ์ ์ฉ ์์ (๋น์ถ์ฒ):
@GetMapping("/api/products/{id}")
public ResponseEntity<ProductDto> getProduct(@PathVariable Long id) {
String key = "product:" + id;
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return ResponseEntity.ok(objectMapper.readValue(cached, ProductDto.class));
}
ProductDto product = productService.getProductById(id);
redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(product), 10, TimeUnit.MINUTES);
return ResponseEntity.ok(product);
}
Service๋ ๋๋ฉ์ธ ๋ก์ง์ ์ฒ๋ฆฌํ๋ ๊ณ์ธต์ด๊ธฐ ๋๋ฌธ์ ์บ์ ์กฐ๊ฑด/๋ฌดํจํ ๋ฑ์ ์ ์ฑ
์ ๋ ์ ์ฐํ๊ฒ ๋ค๋ฃฐ ์ ์์ต๋๋ค.
๋ํ, ์ฌ๋ฌ ๊ณณ์์ ์ฌ์ฌ์ฉ๋๋ฏ๋ก ์ ์ง๋ณด์์ฑ๊ณผ ์ผ๊ด์ฑ์ด ๋์ต๋๋ค.
Service ์บ์ ์ ์ฉ ์์ (์ถ์ฒ):
@Cacheable(value = "productCache", key = "#id")
public ProductDto getProductById(Long id) {
Product product = productRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Product not found"));
return new ProductDto(product);
}
@CacheEvict(value = "productCache", key = "#productDto.id")
public void updateProduct(ProductDto productDto) {
Product product = productRepository.findById(productDto.getId())
.orElseThrow(() -> new NotFoundException("Product not found"));
product.setName(productDto.getName());
product.setPrice(productDto.getPrice());
productRepository.save(product);
}
Controller ์บ์ ์ฌ์ฉ ์์:
@GetMapping("/api/ranking")
public ResponseEntity<List> getDailyRanking() {
String key = "ranking:daily";
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return ResponseEntity.ok(objectMapper.readValue(cached, new TypeReference<List>() {}));
}
List<ProductDto> ranking = productService.getDailyRanking();
redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(ranking), 1, TimeUnit.HOURS);
return ResponseEntity.ok(ranking);
}
์ ๊ฐ ๊ฐ๋ฐ ์ค์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ๊ณ์ธต ๊ตฌ์กฐ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
Controller → Application Service → Database Service → DB
์ ๋ Database Service ๊ณ์ธต์ ์บ์๋ฅผ ์ ์ฉํ์ต๋๋ค. ์ด์ ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค:
์ด์ | ์ค๋ช |
---|---|
DB ์ ๊ทผ ๋ก์ง์ด ์ด ๊ณ์ธต์ ์ง์ค๋์ด ์์ | ๋๋ถ๋ถ์ ์บ์ฑ ๋์์ด DB ์กฐํ ๊ฒฐ๊ณผ |
Application Service์์ ์บ์๊ฐ ์๋์ง ์ ๊ฒฝ ์ฐ์ง ์์๋ ๋จ | ๋๋ฉ์ธ ์ ์ค์ผ์ด์ค ๋ก์ง์ด ๋จ์ํด์ง |
๋ณ๊ฒฝ ์ ์ผ๊ด์ฑ ์๋ ๋ฌดํจํ ๊ฐ๋ฅ | ์ ์ฅ/์ญ์ ๋ก์ง๋ ์ด ๊ณ์ธต์์ ์ฒ๋ฆฌ |
@Service
public class UserDatabaseService {
@Cacheable(value = "userCache", key = "#userId", unless = "#result == null")
public UserDto findUserById(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new NotFoundException("User not found"));
return new UserDto(user);
}
@CacheEvict(value = "userCache", key = "#userDto.id")
public void updateUser(UserDto userDto) {
User user = userRepository.findById(userDto.getId())
.orElseThrow(() -> new NotFoundException("User not found"));
user.setName(userDto.getName());
user.setEmail(userDto.getEmail());
userRepository.save(user);
}
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
์บ์ ํํธ์จ = (์บ์ ํํธ ์ / ์ ์ฒด ์์ฒญ ์) × 100
Redis CLI์์ ํ์ธ:
127.0.0.1:6379> INFO stats
keyspace_hits:158920
keyspace_misses:4180
ํํธ์จ = 158920 / (158920 + 4180) ≈ 97.4%
@Cacheable(value = "userCache", key = "'user:' + #userId")
@PostConstruct
public void preloadPopularItems() {
List<Product> top = productRepository.findTop10ByOrderBySalesDesc();
for (Product p : top) {
redisTemplate.opsForValue().set("product:" + p.getId(), objectMapper.writeValueAsString(p), 1, TimeUnit.HOURS);
}
}
@Cacheable(value = "userCache", key = "#userId", sync = true)
redisTemplate.opsForValue().set("user:active:123", jsonData, 5, TimeUnit.MINUTES);
redisTemplate.opsForValue().set("user:archived:123", jsonData, 60, TimeUnit.MINUTES);
Redis ์บ์๋ ๋จ์ํ "๋น ๋ฅด๊ฒ ์๋ตํ๊ธฐ" ์ํ ๋๊ตฌ๊ฐ ์๋๋ผ,
์ด๋ป๊ฒ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฌ์ฉํ๊ณ ๊ด๋ฆฌํ ์ง์ ๋ํ ์ํคํ
์ฒ ์ ๋ต์
๋๋ค.
์ํฉ | ์ด์ |
---|---|
๋์ผํ ์์ฒญ์ด ์์ฃผ ๋ฐ๋ณต๋ ๋ | ์๋ต์ ๊ทธ๋๋ก ์ฌ์ฌ์ฉ ๊ฐ๋ฅ → ํํธ์จ ↑ |
DB I/O ๋น์ฉ์ด ํฌ๊ฑฐ๋ ํธ๋ํฝ์ด ๊ธ์ฆํ ๋ | ๋ถํ ๋ถ์ฐ |
๋ฐ์ดํฐ๊ฐ ์์ฃผ ๋ฐ๋์ง ์๋ ๊ฒฝ์ฐ | ์ ์ ์ฝํ ์ธ ๋ก ์บ์ฑ ์์ ์ |
์ธ๋ถ API ํธ์ถ ๊ฒฐ๊ณผ๋ฅผ ์ ์ฅํ ๋ | ์๋ต ์๋ + ๋น์ฉ ์ ๊ฐ |
์์
์ํฉ | ์ด์ |
---|---|
๋ฐ์ดํฐ๊ฐ ์์ฃผ ๋ณ๊ฒฝ๋๋ ๊ฒฝ์ฐ | ์บ์ ๋ฌดํจํ ๊ด๋ฆฌ๊ฐ ๋ณต์กํ๊ณ ์ํ |
์ค์๊ฐ์ฑ์ด ๋งค์ฐ ์ค์ํ ๊ฒฝ์ฐ | ์บ์๋ ์ค๋๋ ๋ฐ์ดํฐ๊ฐ ์น๋ช ์ ์ผ ์ ์์ |
ํธ๋์ญ์ /์ผ๊ด์ฑ์ด ์ค์ํ ๋น์ฆ๋์ค ๋ก์ง | ์บ์๋ณด๋ค DB ์ ํ์ฑ์ด ์ฐ์ |
์์