개발노트

25.04.10 Jackson LocalDateTime 직렬화 오류 해결 본문

TroubleShooting

25.04.10 Jackson LocalDateTime 직렬화 오류 해결

ddong-kka 2025. 4. 9. 15:21

문제

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);
	}

 

결제를 수동으로 등록하고 등록된 결제의 정보를 반환하는 메서드이다.

요청되어 정상적으로 처리되면 캐시에 생성되어야한다. 요청해보겠다

 

결제 수동 등록 요청

 

 

캐시가 정상적으로 등록되었다.

 

이렇게 캐싱 직렬화 문제를 해결해보았다.