프로젝트에서 공연 조회 등 짧은 시간에 반복적으로 요청되는 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);
    }
    
    // ...
}

- 공연 생성

  • 공연 생성과 같은 메서드는 캐싱처리할 이유가 없기 때문에 제외했다.

- 공연 목록 조회

  • 모든 공연 제외 메서드에서 Redis는 직렬화된 데이터(JSON과 같은)를 저장하므로 Pageable 객체를 포함한 Page 객체는 일반적인 직렬화 방법으로는 Redis에 사용하지 못한다.
  • 이 때 사용한 것이 PageImple을 상속받는 RestPage 클래스를 만들어 Page의 내용을 간단하게 직렬화하여 Redis에 저장되도록 한다.
/**
 * 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를 사용하는 경우도 있다고 한다.

장점

  1. 데이터 일관성 유지: 공연 정보를 수정할 때, 해당 공연에 대한 데이터가 변경되므로 기존 캐시 데이터를 그대로 두는 것은 데이터 일관성 문제를 초래할 수 있다.

  2. 복잡한 객체 구조: 수정된 공연 정보가 다른 관련 객체와 연결되어 있을 수 있기 때문에 단순히 캐시를 갱신하는 것보다 해당 캐시를 지우고 새로 로드하는 것이 더 안전할 수 있다.

  3. 성능 고려: 데이터가 자주 변경되는 경우, 캐시를 갱신하는 것보다 캐시를 지우고 새로 로드하는 것이 더 효율적일 수 있다.

단점

  1. 성능 저하: 캐시를 지우고 다시 로드하는 과정에서 데이터베이스에 대한 요청이 증가하여 성능 저하를 초래할 수 있다.

  2. 캐시 활용도 감소: 캐시의 주된 목적은 데이터베이스 호출을 줄이는 것인데, 캐시를 지우면 캐시의 이점을 충분히 활용하지 못할 수 있다.

  3. 일관성 문제: 캐시를 지우는 경우, 수정된 데이터가 즉시 반영되지 않을 수 있다.

내 프로젝트에서는 정보 수정에 대한 빈도가 많지 않으므로 @CachePut을 적용하기로 결정.


- 공연 삭제

처음 내 생각은 공연이 완료된 후에 삭제처리를 할 경우를 생각해서 여기에는 @CacheEvict을 추가하지 않을 계획이었지만, 팀원과 논의해보니 공연이 끝난 이후에는 예매 내역 확인을 위해 숨김처리를 하고, 오히려 갑작스러운 이유로 예매시간 직전이나 도중에 삭제를 해야될 상황이 있을 수 있으니 캐시를 지우는 과정이 필요하다 느껴서 추가하게되었다.

0개의 댓글

관련 채용 정보

Powered by GraphCDN, the GraphQL CDN