Redis
는 인메모리 데이터 저장소
로, 데이터를 메모리
에 저장하여 빠른 읽기 및 쓰기
작업을 지원한다. 이는 디스크 기반의 데이터베이스 시스템과는 달리, 데이터를 메모리에 유지함으로써 빠른 응답 시간과 높은 처리량을 제공한다.
성능
: Redis는 메모리 기반의 데이터 저장소이므로 매우 빠른 읽기 및 쓰기
작업을 지원한다. 따라서 캐시로 사용할 경우, 데이터에 빠르게 접근하여 응답 시간을 단축시킬 수 있다.
확장성
: Redis는 분산 환경
에서 높은 확장성을 제공한다. 여러 개의 Redis 인스턴스를 클러스터링하고 데이터를 분산하여 처리할 수 있으며, 이를 통해 시스템의 성능과 처리량을 향상시킬 수 있다.
다양한 데이터 구조
: Redis
는 다양한 데이터 구조
를 지원하며, 이를 활용하여 캐싱 이외에도 다양한 용도로 활용할 수 있다. 예를 들어, 리스트, 해시, 세트
등의 데이터 구조를 활용하여 다양한 데이터 처리 작업을 수행할 수 있다.
지속성
: Redis
는 메모리 기반의 데이터 저장소이지만, 디스크에 데이터를 지속적으로 저장할 수 있는 기능을 제공
한다. 이를 통해 시스템 재시작 시
에도 데이터의 유실을 방지
할 수 있다.
크게 읽기 전략과 쓰기 전략이 있다.
그 중에 이번 프로젝트는 읽는 작업(설문 조회)이 많기 때문에 읽기 전략 중 Look Aside
전략, 읽을 때 데이터가 없으면 캐시를 저장하는 전략을 사용한다.
spring-data-redis
의존성 추가 (build.gradle)implementation 'org.springframework.boot:spring-boot-starter-data-redis
Application
메인 메소드에 @EnableCaching
적용@EnableCaching
@SpringBootApplication
public class SurveyDocumentApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
@EnableCaching
은 스프링 프레임워크의 캐시 관리 기능을 활성화하는 어노테이션이다. 이를 사용하면 애플리케이션에서 캐시를 사용하겠다는 것을 명시적으로 선언하고, 캐시 관련 설정을 일관되게 관리할 수 있다.
application.properties
에 Redis
의 위치 추가(저는 spring 프로젝트와 같은 container에 redis를 배포하기 때문에 localhost로 지정하였습니다.)
## Redis 설정 추가
spring.cache.type=redis
spring.cache.redis.host=localhost
spring.cache.redis.port=6379
Redis
설정 클래스 생성RedissionConfig
@Configuration
public class RedissonConfig {
@Value("${spring.cache.redis.host}")
private String redisHost;
@Value("${spring.cache.redis.port}")
private int redisPort;
private static final String REDISSON_HOST_PREFIX = "redis://";
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + ":" + redisPort);
return Redisson.create(config);
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}
@Bean
public CacheManager cacheManager() {
RedisCacheManager.RedisCacheManagerBuilder builder =
RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory());
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경
.entryTtl(Duration.ofMinutes(30)); // 캐시 수명 30분
builder.cacheDefaults(configuration);
return builder.build();
}
}
@Cacheable
// 설문 조회
@GetMapping(value = "/survey-list/{id}")
@Cacheable(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager")
public SurveyDetailDto readDetail(@PathVariable Long id) {
return surveyService.readSurveyDetail(id);
}
value
: 이 캐시의 제목이다.key
: 캐시의 키를 지정한다. "'survey-' + #id"
: 여기서는 survey-
(단순 String) 와 경로 변수인 id
(GetMapping
으로 받는 {id}
)를 조합하여 키를 생성한다. 캐시는 이 키를 기반으로 결과를 저장하고 조회한다.cacheManager
: Redis
설정에 정의한 cacheManager
를 사용한다.@CacheEvict
// 설문 수정
@PutMapping("/update/{id}")
@CacheEvict(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager")
public void updateSurvey(HttpServletRequest request, @RequestBody SurveyRequestDto requestDto, @PathVariable Long id) {
surveyService.updateSurvey(request, requestDto, id);
}
value
: survey
라는 이름의 캐시를 대상으로 지정한다. 이 이름은 캐시 관리자에서 식별하는 데 사용된다.key
: 삭제할 캐시의 키를 지정한다.'survey-' + #id
: survey-
와 경로 변수인 id
(PutMapping
으로 받는 id
)를 조합하여 키를 생성한다. 해당 키에 해당하는 캐시가 삭제된다.먼저 id
가 1
인 설문을 조회한다고 생각해보자. @Cacheable
의 어노테이션이 작동하여 value
는 survey
, key
는 survey-1
로 지정되어 id
가 1
인 메소드의 반환 값인 SurveyDetailDto
가 저장된다. (캐시에 아무것도 저장되어 있지 않다는 전제하에)
그 후, id
가 1
인 설문을 수정한다고 생각해보자. @CacheEvict
의 어노테이션이 작동하여 value
는 survey
인 것들을 찾고, 그 중에 key
가 survey-1
인 캐시 데이터를 확인하여 캐시 목록에서 삭제한다.
만약 캐시가 저장만되고 @CacheEvict
는 작동이 안된다면 어떻게 될까?
id
가 1
인 설문을 아무리 수정해도 값을 조회하면 캐시에 저장되어 있는 수정되기 전의 설문을 조회할 것이다.id
를 키 값으로 캐시 삭제@PostMapping(value = "/survey-list")
@Cacheable(value = "surveyPage", key = "'surveyPage-' + #pageRequest", cacheManager = "cacheManager" )
public Page<SurveyPageDto> readList(HttpServletRequest request, @RequestBody PageRequestDto pageRequest) {
return surveyService.readSurveyList(request, pageRequest);
}
@GetMapping(value = "/survey-list/{id}")
@Cacheable(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager" )
public SurveyDetailDto readDetail(@PathVariable Long id) {
return surveyService.readSurveyDetail(id);
}
@GetMapping(value = "/getSurveyDocument/{id}")
@Cacheable(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager")
public SurveyDetailDto readDetail1(@PathVariable Long id) {
return surveyService.readSurveyDetail(id);
}
@GetMapping(value = "/getSurveyDocument2/{id}")
@Cacheable(value = "survey2", key = "'survey2-' + #id", cacheManager = "cacheManager")
public SurveyDetailDto2 readDetail2(@PathVariable Long id) {
return surveyService.readSurveyDetail2(id);
}
id
로 추가해주었다.@Transactional
@Caching(evict = {
@CacheEvict(value = "surveyPage", allEntries = true, cacheManager = "cacheManager"),
@CacheEvict(value = "survey", key = "'survey-' + #id", cacheManager = "cacheManager"),
@CacheEvict(value = "survey2", key = "'survey2-' + #id", cacheManager = "cacheManager")
})
public void deleteSurvey(HttpServletRequest request, Long id)@Transactional
@Caching(evict = {
@CacheEvict(value = "surveyPage", allEntries = true, cacheManager = "cacheManager"),
@CacheEvict(value = "survey", key = "'survey-' + #surveyId", cacheManager = "cacheManager"),
@CacheEvict(value = "survey2", key = "'survey2-' + #surveyId", cacheManager = "cacheManager")
})
public void updateSurvey(HttpServletRequest request,SurveyRequestDto requestDto, Long surveyId) {
pagination
캐시는 설문이 수정되거나 삭제될 시 전부 삭제 하도록 설정key
값에 따라 삭제할 캐시를 지정@GetMapping("/me")
@Cacheable(value = "user", key = "'user-' + #request", cacheManager = "cacheManager")
public User getCurrentUser(HttpServletRequest request) {
return userService.getCurrentUser(request);
}
@PatchMapping("/updatepage")
@CacheEvict(value = "user", key = "'user-' + #request", cacheManager = "cacheManager")
public String updateMyPage(HttpServletRequest request, @RequestBody UserUpdateRequest user) {
return userService.updateMyPage(request, user);
}
@PatchMapping("/deleteuser")
@CacheEvict(value = "user", key = "'user-' + #request", cacheManager = "cacheManager")
public String deleteUser(HttpServletRequest request) {
return userService.deleteUser(request);
}
// 설문 응답 저장
@PostMapping(value = "/response/create")
@Caching(evict = {
@CacheEvict(value = "responseList", key = "'responseList-' + #surveyForm.id", cacheManager = "cacheManager" ),
@CacheEvict(value = "getQuestionAnswerByCheckAnswerId", allEntries = true, cacheManager = "cacheManager" )
})
public void createResponse(@RequestBody SurveyResponseDto surveyForm)
// 설문 응답들 조회
@GetMapping(value = "/response/{id}")
@Cacheable(value = "responseList", key = "'responseList-' + #id", cacheManager = "cacheManager" )
public List<SurveyAnswer> readResponse(@PathVariable Long id){
return surveyService.getSurveyAnswersBySurveyDocumentId(id);
}
// 설문 상세 분석 조회
@GetMapping(value = "/research/analyze/{surveydocumentId}")
@Cacheable(value = "surveyAnalyze", key = "'surveyAnalyze-' + #surveydocumentId", cacheManager = "cacheManager" )
public SurveyAnalyzeDto readDetailAnalyze(@PathVariable Long surveydocumentId) {
return surveyService.readSurveyDetailAnalyze(surveydocumentId);
}
// 설문 분석 시작
@Transactional
@PostMapping(value = "/research/analyze/create")
@CacheEvict(value = "surveyAnalyze", key = "'surveyAnalyze-' + #surveyId", cacheManager = "cacheManager" )
public String saveAnalyze(@RequestBody String surveyId) {