일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- algorihm
- aop
- testcode
- CI/CD
- AWS
- 객체지향원칙
- querydls
- docker
- springboot
- JWT
- 어노테이션
- Github Actions
- algorithm
- trouble shooting
- Redis
- 멀티 모듈
- JPA
- EC2
- MSA
- Til
- Intellij
- Kafka
- swagger
- rabbitmq
- Java
- 테스트 코드
- 유효성 검사
- spring boot
- DevOps
- 프로그래머스
- Today
- Total
개발노트
25.04.10 Jackson LocalDateTime 직렬화 오류 해결 본문
문제
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Java 8 date/time type `java.time.LocalDateTime` not supported by default:
add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling
(through reference chain: com.taken_seat.payment_service.application.dto.response.PaymentRegisterResDto["approvedAt"])
Radis Cacging에서 DTO를 직렬화 / 역직렬화 하는 과정에서 오류가 발생했다.
해당 DTO ( PaymentRegisterResDto ) 의 필드 중에 `LocalDatetTime approvedAt`을 Jackjson이 처리하지못한다고한다.
원인
Jackson의 기본 ObjectMapper가 java.time.LocalDateTime을 처리할 수없는게 문제의 원인인 것 같다.
찾아보니까 Jackson core가 `java.tim` 패키지를 기본적으로 직렬화 / 역직렬화하는 기능을 포함하지 않기 때문이라고한다.
그래서 `LocalDate`, LocalDateTime` 등 시간 관련 타입은 별도의 모듈없이는 처리가 불가능한 것이였다.
나는 Redis Chcing 직렬화 / 역직렬화를 Jackson으로 설정하였다.
Jackson은 기본적으로 `java.time.LocalDateTiem`을 직렬화 / 역직렬화를 할 수 없다한다.
해결 과정
에러의 내용에 `java.time.LocalDateTiem`을 직렬화 하려면 `jackson-datatype-jsr310` 의존성을 추가하라고한다.
dependencies {
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
implementation 'com.fasterxml.jackson.core:jackson-databind'
}
jackson-databind 의존성
- Jackson의 핵심 모듈, ObjectMapper의 직렬화/역직렬화 기능 제공
- Java 객체를 JSON으로 , JSON을 Java 객체로 바꾸는 대부분의 기능은 이 모듈이 담당
jackson-datatype-jsr310 의존성
- Java 8의 날짜/시간API 지원을 위한 Jackson 모듈
- 기존에는 할 수 없었던 `LocalDate`, LocalDateTime` 등 시간 관련 타입을 직렬화 / 역직렬화가 가능하게 해준다.
- jackson-databind 가 기본적으로 java.time을 처리하지 못하기 때문에 별도로 추가해줘야한다.
의존성을 추가했으니 이제 ObjectMapper에 모듈을 추가해주도록 해보겠다.
Redis 캐시에서 사용할 ObjectMapper 설정
@Bean
public ObjectMapper redisObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.registerModule(new Jdk8Module());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return objectMapper;
}
Redis에 저장할 객체 중 `LocalDate`, LocalDateTime`, `LocalDate` 등이 포함되어 있는 경우, Jackson이 제대로
직렬화 / 역직렬화하도록 하기 위해 모듈을 등록한다.( javaTimeModule() , jdk8Module() )
WRITE_DATES_AS_TIMESTAMPS를 비활성화하면 날짜를 숫자(timestamp)가 아닌 ISO-8601 문자열(예: 2025-04-09T10:30:00)로 저장한다.
이렇게 설정하면 모든 Redis 캐시에서 직렬화에 공통적으로 사용된다.
DTO 전용 Jackson Serializer 생성
Jackson2JsonRedisSerializer<PaymentDetailResDto> paymentDetailSerializer =
new Jackson2JsonRedisSerializer<>(objectMapper, PaymentDetailResDto.class);
Jackson2JsonRedisSerializer<PagePaymentResponseDto> pagePaymentSerializer =
new Jackson2JsonRedisSerializer<>(objectMapper, PagePaymentResponseDto.class);
나는 직렬화 방식을 Jackson2JsonRedisSerializer 를 사용하기로했다.
Redis에 저장할 객체 ( DTO ) 에 따라 직렬화 대상 타입을 명시해줘야한다.
생성자에 위에서 생성한 ObjectMapper 와 저장할 객체 클래스 를 전달해준다.
Redis 캐시 구성에 맞게 RedisCacheConfigureation 설정
RedisCacheConfiguration defaultConfiguration = RedisCacheConfiguration
.defaultCacheConfig()
.entryTtl(DEFAULT_TTL)
.disableCachingNullValues()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(paymentDetailSerializer));
위에서의설정은 기본 TTL은 2분으로 위에서 상수로 설정했다.
Key는 문자열로 , Value는 PaymentDetailResDto 기준으로 직렬화를 진행한다.
disableCachingNullValues() 는 null 값은 캐시하지않도록 설정하는것이다.
이렇게 각각 상황에 맞는 캐시 설정들을 생성한 다음 CacheManager에 등록해보겠다.
캐시 이름별 설정 Map 생성 및 CacheManager 등록
// 각 캐시 별로 설정을 관리하는 Map을 생성
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put(PAYMENT_CACHE, orderConfiguration);
cacheConfigurations.put(PAYMENT_SEARCH_CACHE, orderSearchConfiguration);
return RedisCacheManager
.builder(redisConnectionFactory)
.cacheDefaults(defaultConfiguration)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
- RedisCacheManager는 여러개의 캐시에 대해 각각 따로 적용하도록 구성할 수 있다.
- 설정하지않은 나머지 캐시들은 default 설정에 따른다.
해결 테스트
/**
* 결제 수동 등록 기능
*
* 주어진 결제 수동 결제 요청 DTO ( PaymentCreateReqDto )를 기반으로 새 결제를 등록한다.
* 1. 결제 요청 금액이 1원 미만인지 검사한다.
* - 1원 미만인 경우 IllegalArgumentException 예외처리 발생
* 2. Payment 등록 이후 결제 이력 추적용 PaymentHistory 생성
* 3. 저장된 결제를 DTO ( PaymentCreateDto ) 형식으로 변환하여 반환한다.
*
* @param paymentRegisterReqDto 등록할 결제의 정보
* @return PaymentRegisterResDto 등록된 결제의 정보
* @throws IllegalArgumentException 결제 요청 금액이 1원 미만인 경우 예외 발생
*/
@CachePut(cacheNames = "paymentCache", key = "#result.paymentId")
public PaymentRegisterResDto registerPayment(PaymentRegisterReqDto paymentRegisterReqDto) {
// MASTER 계정이 직접 등록하는 API - 결제 API 호출 없이 수동 등록
if (paymentRegisterReqDto.getPrice() <= 0) {
throw new IllegalArgumentException("결제 금액은 1원 미만일 수 없습니다. 요청 금액 : " + paymentRegisterReqDto.getPrice());
}
LocalDateTime now = LocalDateTime.now();
Payment payment = Payment.builder()
.bookingId(paymentRegisterReqDto.getBookingId())
.price(paymentRegisterReqDto.getPrice())
.paymentStatus(PaymentStatus.COMPLETED)
.approvedAt(now)
.createdBy(UUID.randomUUID())
.build();
paymentRepository.save(payment);
paymentHistoryRepository.save(
PaymentHistory.builder()
.payment(payment)
.price(payment.getPrice())
.paymentStatus(payment.getPaymentStatus())
.approvedAt(now)
.createdBy(UUID.randomUUID())
.build()
);
return PaymentRegisterResDto.toResponse(payment);
}
결제를 수동으로 등록하고 등록된 결제의 정보를 반환하는 메서드이다.
요청되어 정상적으로 처리되면 캐시에 생성되어야한다. 요청해보겠다
이렇게 캐싱 직렬화 문제를 해결해보았다.
'TroubleShooting' 카테고리의 다른 글
25.04.15 결제 도메인에서 마일리지/쿠폰 차감 책임을 분리한 이유 (0) | 2025.04.15 |
---|---|
25.04.13 Spring Redis hashOps.keys()로 패턴 조회 안 되는 이유와 SCAN을 사용한 해결법 (0) | 2025.04.13 |
2025.03.22 RabbitMQ concurrency 설정과 비관적 락을 활용한 재고 감소 동시성 제어 (0) | 2025.03.25 |
25.03.21 사가 패턴 무한 재시도 방지 (0) | 2025.03.21 |
25.03.17 RabbitMQ message 직렬화 문제 해결 (0) | 2025.03.17 |