인프런 커뮤니티 질문&답변

후시딘님의 프로필 이미지

작성한 질문수

비전공자도 이해할 수 있는 Redis 입문/실전 (조회 성능 최적화편)

캐싱 객체 직렬화/역직렬화

24.10.03 20:31 작성

·

40

0

안녕하세요! 강의를 수강하고 제 프로젝트에서 캐싱을 적용하고 싶어서 따라 적용해보고 있습니다.

 

package com.ecommerceproduct.api.controller.product.dto.response;

import com.ecommerceproduct.domain.product.repository.dao.ProductDetailDao;
import com.ecommerceproduct.domain.product.type.OptionType;
import com.ecommerceproduct.domain.product.type.ProductCategory;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import lombok.Builder;

@Builder
public record ProductDetailResponse(
    Long id,
    String name,
    StoreInfo store,
    int quantity,
    ProductCategory category,
    String thumbnailImgUrl,
    int basePrice,
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonDeserialize(using = LocalDateTimeDeserializer.class)
    LocalDateTime createdDateTime,
    List<ProductOptionInfo> options
) {

  public static ProductDetailResponse from(List<ProductDetailDao> daos){
    List<ProductOptionInfo> options = new ArrayList<>();
    daos.forEach(dao -> options.add(ProductOptionInfo.from(dao.option())));
    ProductDetailDao dao = daos.get(0);
    return ProductDetailResponse.builder()
        .id(dao.id())
        .name(dao.name())
        .store(StoreInfo.from(dao.store()))
        .quantity(dao.quantity())
        .category(dao.category())
        .thumbnailImgUrl(dao.thumbnailImgUrl())
        .basePrice(dao.basePrice())
        .createdDateTime(dao.createdDateTime())
        .options(options)
        .build();
  }

  public record StoreInfo(
      Long storeId,
      String name
  ) {
    public static StoreInfo from(ProductDetailDao.StoreInfo daoStore){
      return new StoreInfo(daoStore.storeId(), daoStore.name());
    }
  }

  public record ProductOptionInfo(
      Long optionId,
      String name,
      int count,
      int price,
      OptionType optionType
  ){
    public static ProductOptionInfo from(ProductDetailDao.ProductOptionInfo option){
      return new ProductOptionInfo(
          option.optionId(),
          option.name(),
          option.count(),
          option.price(),
          option.optionType()
      );
    }
  }
}

해당 클래스를 반환하는 메서드에 @Cacheable을 적용하려고 합니다.

@Cacheable(cacheNames = "getProduct", key = "'product:productId:' + #productId", cacheManager = "매니저이름")
public ProductDetailResponse get(Long productId) {
  return ProductDetailResponse.from(productRepository.findWithOptions(productId));
}

강의에서 알려주신 매니저와 동일하게 매니저를 bean으로 등록해서 사용해본결과

직렬화/역질렬화가 안되는것으로 보입니다.

 

매니저에 설정을

 

serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
  new Jackson2JsonRedisSerializer<>(objectMapper, ProductDetailResponse.class)))

처럼 명시적으로 타입을 지정해주면 문제가 없는 것 같은데 record여서 안되는 걸까 싶어서 class로 바꾸어도 되지는 않더라고요! 강의에서의 Board 클래스는 문제없이 직렬화/역직렬화가 되는데 제가만든 dto가 안되는 이유는 강의의 Board가 @Entity클래스여서 인걸까요??

그렇다면 dto로 쓰려면 매니저를 각각의 dto마다 매니저를 하나씩 만들어주어야할까요?

답변 2

0

JSCODE 박재성님의 프로필 이미지
JSCODE 박재성
지식공유자

2024. 10. 03. 21:16

안녕하세요 후시딘님!

직렬화/역직렬화를 하는 과정에서 getter 함수가 필요합니다!

Board 엔티티에서는 직렬화/역직렬화가 정상적으로 수행되기 위해

getter 함수를 만들어주었습니다.

 

위에서 DTO를 만드셔서 사용하실 때 getter 함수를 추가해보시고

정상적으로 작동하는 지 확인 한 번 해보세요!

 

지금 만드신 복잡한 코드에서는 테스트가 어려울 수 있으니

먼저 간단한 클래스를 만드셔서 getter를 추가했을 때와 추가하지 않았을 때

작동 상의 차이가 있는 지 먼저 체크해보시면 좋을 것 같습니다~!!

 

혹시 테스트해보시고 안 되신다면 또 질문 남겨주시기 바랍니다:D

0

인프런 AI 인턴님의 프로필 이미지

2024. 10. 03. 20:36

안녕하세요, 인프런 AI 인턴입니다. 후시딘님께서 캐싱 적용에 관해 질문을 주셨네요. Redis에서 직렬화/역직렬화 문제를 해결하기 위해 어떤 접근 방식을 사용할 수 있는지 답변드리겠습니다.

문제의 원인

Spring Cache에서 객체를 캐시할 때, Jackson2JsonRedisSerializer를 사용하여 JSON 형식으로 직렬화를 진행합니다. 여기서 중요한 점은 Spring Cache와 Jackson이 객체를 직렬화/역직렬화할 때 어떤 방식으로 이 작업을 수행하는지 이해하는 것입니다.

  • 직렬화/역직렬화가 실패하는 경우는 보통 명시적으로 Java 객체를 JSON으로 변환하는 Jackson의 역할과 관련이 있습니다.
  • @Cacheable을 사용하면 메서드가 리턴하는 객체가 직렬화되어 Redis에 저장됩니다. 이때 사용하는 시리얼라이저가 해당 객체를 직렬화할 수 있어야 합니다.

해결방법

1. 직렬화 및 역직렬화 설정

Jackson2JsonRedisSerializer를 사용할 때 ObjectMapper에서 기본 설정을 확인하세요. 만약 record 형태를 사용하고 있다면, Jackson2JsonRedisSerializer를 사용할 때 클래스의 타입을 명확하게 지정해야 합니다:

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
    RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
            new Jackson2JsonRedisSerializer<>(ProductDetailResponse.class))
        );
    return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(cacheConfiguration)
            .build();
}

2. DTO를 위한 설정

DTO마다 별도의 CacheManager를 생성하는 것은 관리상 비효율적일 수 있습니다. 하나의 공통 CacheManager를 사용하고, 공통 Serialize 설정에서 각 DTO가 올바르게 직렬화/역직렬화 될 수 있게 합니다.

3. ObjectMapper 설정 확인

직렬화/역직렬화 시 ObjectMapper에서 참조하는 모듈이나 설정이 충분한지 확인합니다. 예를 들어, JavaTimeModule을 등록하여 LocalDateTime을 처리할 수 있도록 할 필요가 있습니다:

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

주의사항: record나 특정한 객체에 대해 Jackson이 제대로 동작하지 않는다면, 해당 클래스의 구조 혹은 의존성을 점검하시는 것이 필요합니다.

추가적으로 도움이 필요하시면 아래 질문 링크에서도 더 많은 정보를 얻을 수 있습니다:

이 외에 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 질문에 대해 더 명확한 답변 드릴 수 있도록 하겠습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.