프로젝트에서 공연 조회 등 짧은 시간에 반복적으로 요청되는 api는 매번 호출이 될 경우 처음 요청만 DB에 접근하고, 그 이후 요청들은 캐시에서 데이터를 가져오므로 DB의 부하를 줄이면서 응답 시간이 크게 단축시킬 수 있다.
캐시를 구성하는 설정 클래스
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // Redis 서버와의 연결을 생성하는 데 사용되는 팩토리 클래스
// 설정 구성
// ObjectMapper : Java 객체를 JSON으로 직렬화하거나 JSON을 Java 객체로 역직렬화하는 데 사용
ObjectMapper objectMapper = new ObjectMapper();
// registerModule : Java 8의 날짜 및 시간 API (LocalDate, LocalDateTime 등)를 직렬화
objectMapper.registerModule(new JavaTimeModule());
// activateDefaultTyping : 객체가 직렬화될 때 해당 타입 정보를 JSON에 포함시킬 수 있고, 이를 이용해 역직렬화 할 때 어떤 클래스의 인스턴스인지 확읺한다.
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
// Redis를 이용해서 Spring Cache를 사용할 때 Redis 관련 설정을 모아두는 클래스
RedisCacheConfiguration configuration = RedisCacheConfiguration
.defaultCacheConfig()
// null을 캐싱 할것인지
.disableCachingNullValues()
// 기본 캐시 유지 시간 (Time To Live)
.entryTtl(Duration.ofSeconds(60))
// 캐시를 구분하는 접두사 설정
.computePrefixWith(CacheKeyPrefix.simple())
// 캐시에 저장할 값을 어떻게 직렬화 / 역직렬화 할것인지
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper))); // Value 직렬화
return RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(configuration)
.build();
}
}
이제 각 서비스에 어노테이션을 달아줘서 맛깔나게 사용하면 된다.
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ConcertService {
private final ConcertRepository concertRepository;
private final ConcertMapper concertMapper;
// ...
// 모든 공연 조회
@Cacheable(value = "concerts")
public Page<GetConcertsRes> getAllConcerts(Pageable pageable) {
Page<Concert> concertList = concertRepository.findAll(pageable);
// Page<Concert>를 List<GetConcertsRes>로 변환
List<GetConcertsRes> concertResponses = concertList.getContent()
.stream()
.map(concertMapper::toGetConcertsRes)
.collect(Collectors.toList());
// RestPage로 변환하여 반환
return new RestPage<>(concertResponses, concertList.getNumber(), concertList.getSize(), concertList.getTotalElements());
}
// 특정 공연 조회
@Cacheable(value = "concert")
public GetConcertRes getConcertById(Long concertId) {
Concert concert = getConcertUtil(concertId);
return concertMapper.toGetConcertRes(concert);
}
// 특정 공연 수정
@PreAuthorize("hasAnyRole('MANAGER', 'SELLER')")
@Transactional
@CachePut(value = "concert", key = "#concertId")
public GetConcertRes updateConcertById(Long concertId, UpdateConcertReq updateConcertReq) {
Concert concert = getConcertUtil(concertId);
changeConcert(updateConcertReq, concert);
return concertMapper.toGetConcertRes(concert);
}
// 특정 공연 삭제
@PreAuthorize("hasAnyRole('MANAGER', 'SELLER')")
@Transactional
@CacheEvict(value = "concert", key = "#concertId")
public void deleteConcertById(String email, Long concertId) {
Concert concert = getConcertUtil(concertId);
concert.delete(email);
}
// ...
}
/**
* Page<T> 데이터를 캐싱하기 위한 객체. Page<T>를 리턴하는 부분을 감싸서 사용한다.
*/
@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"}) // JSON 직렬화 시 무시할 필드를 지정
public class RestPage<T> extends PageImpl<T> {
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES) // JSON에서 이 클래스의 인스턴스를 생성할 때 사용할 생성자를 지정
public RestPage(@JsonProperty("content") List<T> content,
@JsonProperty("number") int page,
@JsonProperty("size") int size,
@JsonProperty("totalElements") long total) {
super(content, PageRequest.of(page, size), total);
}
public RestPage(Page<T> page) {
super(page.getContent(), page.getPageable(), page.getTotalElements());
}
}
어노테이션 안에 key를 Entity 클래스에 정의된대로 key = "#id"
라고 설정했더니 에러가 발생...
알고보니 메서드 안에 사용중인 객체 이름으로 사용해야된다.
여기서는 특정 공연을 수정할 때 @CachePut
을 사용하고 있지만, @CacheEvict
를 사용하는 경우도 있다고 한다.
장점
데이터 일관성 유지: 공연 정보를 수정할 때, 해당 공연에 대한 데이터가 변경되므로 기존 캐시 데이터를 그대로 두는 것은 데이터 일관성 문제를 초래할 수 있다.
복잡한 객체 구조: 수정된 공연 정보가 다른 관련 객체와 연결되어 있을 수 있기 때문에 단순히 캐시를 갱신하는 것보다 해당 캐시를 지우고 새로 로드하는 것이 더 안전할 수 있다.
성능 고려: 데이터가 자주 변경되는 경우, 캐시를 갱신하는 것보다 캐시를 지우고 새로 로드하는 것이 더 효율적일 수 있다.
단점
성능 저하: 캐시를 지우고 다시 로드하는 과정에서 데이터베이스에 대한 요청이 증가하여 성능 저하를 초래할 수 있다.
캐시 활용도 감소: 캐시의 주된 목적은 데이터베이스 호출을 줄이는 것인데, 캐시를 지우면 캐시의 이점을 충분히 활용하지 못할 수 있다.
일관성 문제: 캐시를 지우는 경우, 수정된 데이터가 즉시 반영되지 않을 수 있다.
내 프로젝트에서는 정보 수정에 대한 빈도가 많지 않으므로 @CachePut
을 적용하기로 결정.
처음 내 생각은 공연이 완료된 후에 삭제처리를 할 경우를 생각해서 여기에는 @CacheEvict
을 추가하지 않을 계획이었지만, 팀원과 논의해보니 공연이 끝난 이후에는 예매 내역 확인을 위해 숨김처리를 하고, 오히려 갑작스러운 이유로 예매시간 직전이나 도중에 삭제를 해야될 상황이 있을 수 있으니 캐시를 지우는 과정이 필요하다 느껴서 추가하게되었다.