[Spring] Spring Cache์ ์ ์ฉํ ์กฐํ ์ฑ๋ฅ ๊ฐ์ (with. Redis)
ํ๋ก์ ํธ๋ฅผ ๋ฆฌํฉํฐ๋ง ํ๋ฉด์ ์กฐํํ๋ ๋ถ๋ถ์ ์บ์ฑ์ ์ ์ฉํ๋ ค๊ณ ํ๋ค!
์ ์บ์ฑ์ ์ ์ฉํ๋ ค๊ณ ํ๋? !
ํด๋น ์๋น์ค๋ ํ ํ๋ฉด์์ ์ง๋์์ ๋ด๊ฐ ์์ฑํ ๋ชจ๋ ๊ฒ์๊ธ ์์น๋ฅผ ํํํด ์ค๋ค.
์ค์ง์ ์ผ๋ก ๊ฒ์๊ธ ๋ฑ๋ก, ์ญ์ ๋ณด๋ค ์กฐํ์๊ฐ ํจ์ฌ ๋ง์ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ๊ณ , ๊ฐ์ฅ ๋ง์ด ๋
ธ์ถ๋๋ ํ ํ๋ฉด์์ ๊ณ์ ์์น ์ ๋ณด๋ฅผ ๋ถ๋ฌ์จ๋ค๋ฉด ์ฑ๋ฅ์ ์ผ๋ก ์ข์ง ์์ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ๋ค.!
์คํ๋ง ์บ์๊ฐ ๋ฌด์์ธ์ง๋ ์๋ ๊ธ์ ์ ๋ฆฌํด๋์๋ค๐
๐ ์บ์ฑ ์ ๋ต์ ๋ํด์
๋ก์ปฌ ์บ์ฑ
์๋ฒ ๋ด๋ถ ์ ์ฅ์์ ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ ๊ฒ์ด๋ค.
์๋๋ ๋น ๋ฅด์ง๋ง ์๋ฒ ๊ฐ ๋ฐ์ดํฐ ๊ณต์ ๊ฐ ์๋๋ค๋ ๋จ์ ์ด ์๋ค.
๊ธ๋ก๋ฒ ์บ์ฑ
์๋ฒ ๋ด๋ถ ์ ์ฅ์๊ฐ ์๋ ๋ณ๋์ ์บ์ ์๋ฒ๋ฅผ ๋์ด ๊ฐ ์๋ฒ์์ ์บ์ ์๋ฒ๋ฅผ ์ฐธ์กฐํ๋ ๊ฒ์ด๋ค.
์บ์ ๋ฐ์ดํฐ๋ฅผ ์ป์ผ๋ ค๊ณ ํ ๋๋ง๋ค ์บ์ ์๋ฒ๋ก์ ๋คํธ์ํฌ ํธ๋ํฝ์ด ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ ๋ก์ปฌ ์บ์ฑ๋ณด๋ค ์๋๋ ๋๋ฆฌ๋ค.
ํ์ง๋ง, ์๋ฒ ๊ฐ ์บ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ฒ ๊ณต์ ํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ก์ปฌ ์บ์ฑ์ ๋ฌธ์ ์ ์ ํด๊ฒฐํ ์ ์๋ค.
๋๋ ํผ์ ๋ฆฌํฉํ ๋ง ์ค์ด๋ผ์ ๊ตณ์ด ๊ธ๋ก๋ฒ ์บ์ฑ์ด ํ์ํ์ง ์์ง๋ง, ์ถํ ํ์ฅ์ฑ์ ๊ณ ๋ คํ์ ๋ ๊ธ๋ก๋ฒ ์บ์ฑ์ ์ ์ฉํ๋ ๊ฒ ์ข๊ฒ ๋ค๊ณ ์๊ฐํด์ Redis๋ฅผ ์ด์ฉํด์ ๊ตฌํํ๋ค.!
โ๏ธ ์ฝ๋
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
spring cache์ redis๋ฅผ ์์กด์ฑ์ ์ถ๊ฐํ๋ค.
application
@EnableCaching
@EnableJpaAuditing
@SpringBootApplication
public class FootprintApplication {
public static void main(String[] args) {
SpringApplication.run(FootprintApplication.class, args);
}
}
์ ํ๋ฆฌ์ผ์ด์
ํด๋์ค ์์ @EnableCaching์ ์ถ๊ฐํ๋ค.
์บ์ฑ์ ์ฌ์ฉํ๋ค๋ ์๋ฏธ์ด๋ค.
RedisConfig
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.port}")
private int port;
@Value("${spring.data.redis.host}")
private String host;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host, port);
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
// redisTemplate๋ฅผ ๋ฐ์์์ set, get, delete๋ฅผ ์ฌ์ฉ
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
// setKeySerializer, setValueSerializer ์ค์
// redis-cli์ ํตํด ์ง์ ๋ฐ์ดํฐ๋ฅผ ์กฐํ ์ ์์๋ณผ ์ ์๋ ํํ๋ก ์ถ๋ ฅ๋๋ ๊ฒ์ ๋ฐฉ์ง
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
redisCacheConfigurationMap.put("all_place", redisCacheConfiguration.entryTtl(Duration.ofHours(1)));
return RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(connectionFactory)
.cacheDefaults(redisCacheConfiguration)
.withInitialCacheConfigurations(redisCacheConfigurationMap)
.build();
}
}
host์ port๋ yml ํ์ผ์ ์ ์ํด๋์ผ๋ฉด ๋๋ค.
- redisConnectionFactory()
- redis ์๋ฒ์ ์ฐ๊ฒฐํ ์ ์๋ CommectionFactory๋ฅผ ์์ฑํ๋ค.
- redis ํด๋ผ์ด์ธํธ๋ Jedis์ Lettuce๊ฐ ์๋๋ฐ ์คํ๋ง์๋ ๊ธฐ๋ณธ์ ์ผ๋ก Lettuce๊ฐ ์ค์ ๋ผ ์๋ค.
- Jedis : ๋ฉํฐ์ฐ๋ ๋ ํ๊ฒฝ์์ unsafe ํ๋ค. Connection pool์ ์ด์ฉํด ๋ฉํฐ์ค๋ ๋ ํ๊ฒฝ์ ๊ตฌ์ฑํด์ผ ํ๊ณ , Jedis์ธ์คํด์ค๋ ์ฐ๊ฒฐํ ๋๋ง๋ค Connection pool์ ๋ถ๋ฌ์ ๋ฌผ๋ฆฌ์ ์ธ ๋น์ฉ์ด ์ฆ๊ฐํ๋ค.
- Lettuce : ๋ฉํฐ์ฐ๋ ๋ ํ๊ฒฝ์์ safe ํ๋ค. netty๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ ๊ตฌ์ถ๋๋ค. ๋น๋๊ธฐ ๋ฐฉ์์ผ๋ก ์์ฒญํ๋ฉฐ TPS/CPU/Connection ๊ฐ์์ ์๋ต ์๋ ๋ฑ jedis๋ณด๋ค ๋ฐ์ด๋๋ค.
- RedisTemplate()
- Spring Data Redis๋ RedisTemplate, RedisRepository ๋ ๊ฐ์ง ๋ฐฉ์์ผ๋ก ์ ๊ทผํ๋ค.
- RedisTemplate
- Redis ๋ฐ์ดํฐ ๋ฒ ์ด์ค์ ์ํธ์์ฉํ๋๋ฐ ํ์ํ ์ค์ ์ ๊ตฌ์ฑํ๋ค.
- Redis์ ๋ฌธ์์ด ํค์ JSON ํ์์ ๊ฐ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ ๊ฒ์ํ ์ ์๋ค.
- ์ผ๋ฐ์ ์ผ๋ก Redis์ ๋ค์ํ ๋ฐ์ดํฐ ์ ํ์ธ ๋ฌธ์์ด, ํด์, ๋ชฉ๋ก, ์งํฉ, ์ ๋ ฌ๋ ์งํฉ ๋ฑ์ ์ฒ๋ฆฌํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
- ๊ฐ์ฒด๋ค์ ์๋์ ์ผ๋ก ์ง๋ ฌํ/์ญ์ง๋ ฌํ ํ๋ฉฐ ๋ฐ์ด๋๋ฆฌ ๋ฐ์ดํฐ๋ฅผ Redis์ ์ ์ฅํ๋ค.
- ๊ฐ ๋ฐ์ดํฐ ์ ํ์ ๋ํด ๊ธฐ๋ณธ์ ์ธ CRUD ์์ ์ด ๊ฐ๋ฅํ๋ค.
- ์ ์์ค์ API๋ก ์ง์ ์ ์ธ Redis ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ ์ ์ํํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
- RedisRepository
- ๋ ์ถ์ํ๋ ๋ ๋ฒจ์ ์ปดํฌ๋ํธ๋ก, Redis ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ํ ๊ณตํต ์์ ์ ๋ ํธ๋ฆฌํ๊ฒ ์ฒ๋ฆฌํ ์ ์๋ ๊ณ ์์ค์ ๋ฉ์๋๋ฅผ ์ ๊ณตํ๋ค.
- Spring Data JPA์ JpaRepository์ ์ ์ฌํ ํํ๋ก ์์ฑ๋๋ฉฐ, ์ํฐํฐ CRUD์ ๊ฐ์ ๊ณ ์์ค์ ์์ ์ ์ง์ํ๋ค.
- ๊ฐ๋ฐ์๊ฐ ์ธํฐํ์ด์ค๋ฅผ ์ ์ํ๊ณ ํด๋น ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ๋ Redis ์ ์ฅ์ ํด๋์ค๋ฅผ ์์ฑํ์ฌ, ์ง์ Redis ์์ ์ ๊ตฌํํ ํ์ ์์ด Spring Data Redis์ ๊ธฐ๋ฅ์ ํ์ฉํ ์ ์๋ค.
- RedisTemplate
- redisCacheManager
- Redis Cache ์ ์ฉ์ ์ํด RedisCacheManager๋ฅผ ์ค์ ํ๋ค. ๊ธฐ๋ณธ RedisCacheConfiguration์ ๊ฐ์ ธ์จ ํ, key์ value์ ์ง๋ ฌํ ๋ฐฉ์์ ์ค์ ํ๋ค.
- ํด๋น ์ฝ๋์์๋ StringRedisSerializer๋ฅผ ์ฌ์ฉํ์ฌ ๋ฌธ์์ด๋ก ๋ ํค๋ฅผ ์ง๋ ฌํ ํ๊ณ , GenericJackson2 JsonRedisSerializer๋ฅผ ์ฌ์ฉํ์ฌ ์ผ๋ฐ์ ์ธ ๊ฐ์ฒด๋ฅผ JSON ํ์์ผ๋ก ์ง๋ ฌํํ๋ค.
- Redis ์บ์ ์ด๋ฆ๊ณผ RedisCacheConfiguration์ ๋งคํํ๋ ๋งต์ ์์ฑ ํ๋ค. ๋๋ ์์น ์ ๋ณด๋ฅผ ์ ์ฅํ๊ธฐ ์ํด์ "all_place"๋ผ๋ ์ด๋ฆ์ผ๋ก ์บ์๋ฅผ ์์ฑํ๋ค.
- entryTtl(Duration.ofHours())์ ์ํ๋ ์๊ฐ์ ๋ฃ์ผ๋ฉด ๋๋ค.
- Spring Data Redis๋ RedisTemplate, RedisRepository ๋ ๊ฐ์ง ๋ฐฉ์์ผ๋ก ์ ๊ทผํ๋ค.
Service
@Cacheable(cacheNames = "all_place", key = "#memberId")
public ListResult<GetAllPlaceResDto> getAllPlacesV2(Long memberId) {
//... ์์น ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ฝ๋
return responseService.getListResult(new ArrayList<>(getAllPlaceResDtoList));
}
@Cacheable ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํด์ ์บ์ฑ์ ๊ตฌํํ๋ฉด ๋๋ค.
๋๋ key๊ฐ์ memberId๋ก ์ค์ ํ๋ค.!! ์ ์ ๋ง๋ค ํ์์ ๋ณด์ด๋ ํ๋ฉด์ด ๋ค๋ฅผ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ด๋ค.
redis์์๋ key๊ฐ์ด all_place::{memberId} ํ์์ผ๋ก ์ ์ฅ๋๋ค.
์บ์ ์ ์ฉ ์
์บ์ ์ ์ฉ ํ
์บ์ฑ์ ์ ์ฉํ๊ธฐ ์ ๊ณผ ํ๋ฅผ ๋น๊ตํ์ ๋ ์กฐํ ์๊ฐ์ด 493ms -> 8ms๋ก ๋ณํ๋ค
์ฝ 61๋ฐฐ ๋นจ๋ผ์ง ๊ฒฐ๊ณผ์ด๋ค!! ์ด๋ ๊ฒ ์ฐจ์ด๋๋ ๋๋๊ฑด๊ฐ...?
redis์๋ ์ ์ ์ฅ๋์๋ค.
๊ทผ๋ฐ ์ด ๊ณผ์ ์์ ๊ณ ๋ฏผ์ด ์๊ฒผ๋ค. ์บ์ ์ ํจ์๊ฐ์ ์ค์ ํ๋๊น ๋ฐ์ดํฐ๊ฐ ์
๋ฐ์ดํธ๋๋๋ผ๋ ์กฐํํ ๋ ์ค์ ์๊ฐ์ด ์ง๋๊ธฐ ์ ๊น์ง ๋ฐ์ดํฐ๊ฐ ์
๋ฐ์ดํธ๋์ง ์๋ ๋ค๋ ๊ฒ์ด๋ค.
๊ทธ๋์ ๋๋ CachePut์ ์ฌ์ฉํ๋ฉด ๋๋ ค๋ ์๊ฐํ๋ค.
@CachePut(cacheNames = "all_place", key = "#memberId")
public ListResult<GetAllPlaceResDto> getAllPlacesV2(Long memberId) {
// ... ์์น ์ ๋ณด ๋ถ๋ฌ์ค๋ ์ฝ๋
return responseService.getListResult(new ArrayList<>(getAllPlaceResDtoList));
}
์ด ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํ๋๊น ๋งค๋ฒ ์
๋ฐ์ดํธ๊ฐ ์ ๋๋ ๊ฒ์ ํ์ธํ๋ค. ์๋๋ @Cacheable๋งํผ์ ์๋์ง๋ง ์ถฉ๋ถํ ๋นจ๋๋ค! ํ์ง๋ง ์
๋ฐ์ดํธ ๋์ง ์์ ์ ๋ณด์ธ๋ฐ๋ ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ๋ ๊ฒ์ ํ์ธํ๋ค.
๋น์ฐํ CachePut ์ด๋
ธํ
์ด์
์ ๋ฐํ๊ฐ์ด ์บ์ฑ๋์ด ์๋ ์ ๋ฌด์ ์๊ด์์ด ๋ฉ์๋๋ฅผ ๋ฌด์กฐ๊ฑด ์คํํ๋ค!!
๊ทธ๋์ ๋๋ ๊ทธ๋๋ก @Cacheable์ ์ฌ์ฉํ๊ณ , ๊ฒ์๊ธ์ ๋ฑ๋ก, ์์ , ์ญ์ ๋ ๋ ์บ์ ์ ๋ณด๋ฅผ ์ญ์ ํ๊ธฐ๋ก ํ๋ค ใ
ใ
@Transactional
@CacheEvict(cacheNames = "all_place", key = "#memberId")
public SingleResult<UploadPostingResDto> uploadPostingV3(UploadPostingReqDto uploadPostingReqDto, String imageUrl, Long memberId) {
// ... ๊ฒ์๊ธ ๋ฑ๋ก ๋ก์ง
return responseService.getSingleResult(uploadPostingResDto);
}
์ด๋ ๊ฒ ํ๋๊น ์ ์์ ์ผ๋ก ์บ์๊ฐ ์ญ์ ๋๊ณ , ์กฐํํ ๋ ๋ค์ ๋ฑ๋ก๋๋ ๊ฑธ ํ์ธํ๋ค ใ
ใ