문제
RedisTemplate
를 통해 데이터를 저장하기에는 성공했으나, 읽어오는 과정에서 역직렬화에 실패했다.
Redis에 저장할dto
에 존재하지 않는 필드가 함께 저장되어 역직렬화에 실패한 것인데 이유를 알아보자.
Redis에 String 타입의 Json 데이터를 저장했고 직/역직렬화에는 Jackson 2.18.1 라이브러리를 사용했다.
Redis
에 StringType으로 저정할 Dto는 아래와 같다.
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ProductCache implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private static final String KEY_FORMAT = "product:%d";
private Long id;
private Long hubId;
private Long companyId;
private Integer quantity;
private String name;
// ...
public String getKey() {
return String.format(KEY_FORMAT, id);
}
}
Key 포맷을 Dto에 두었고, getKey()
를 통해 외부에서 키를 얻을 수 있도록 했다.
정상적으로 동작하는지 테스트를 해보았다.
@Test
@DisplayName("캐시 생성 테스트")
public void saveProductCache_successTest() {
// Given
Product save = productCacheAdapter.save(TEST_PRODUCT);
// When
Optional<Product> productOptional = productCacheAdapter.findOne(save);
// Then
Assertions.assertAll(
() -> Assertions.assertTrue(productOptional.isPresent()),
() -> Assertions.assertEquals(TEST_PRODUCT.getId(), productOptional.get().getId())
);
}
결과는?
org.springframework.data.redis.serializer.SerializationException:
Could not read JSON:Unrecognized field "key"
(class com.sparta.logistics.product.infrastructure.cache.dto.ProductCache),
not marked as ignorable
(5 known properties: "hubId", "id", "companyId", "quantity", "name"])
직렬화 관련 예외가 발생하며 key
필드를 발견하지 못하여 JSON을 읽어오지 못한다고 한다. 발견된 프로퍼티는 hubId
, id
, companyId
, quantity
, name
인데 key
라는 필드가 없어서 역직렬화에 실패한 듯 했다.
캐시의 데이터를 확인해보자.
{
"@class":"com.sparta.logistics.product.infrastructure.cache.dto.ProductCache",
"id":1,
"hubId":1,
"companyId":1,
"quantity":1,
"name":"test",
"key":"product:1"
}
나는 캐시에 넣을 데이터에 key라는 필드를 추가한 적이 없다.
하지만 캐시 데이터에는 key라는 데이터가 저장되어있다. 놀라워
그러다 문득 Jackson
의 역직렬화 방식에 대해 탐구해봤던 내용이 떠올랐다.
Jackson
라이브러리가 JSON
포맷문자열을 객체로 역직렬화할 때, 아래의 순서를 따른다.
- 빈 생성자를 호출한다.
- 리플렉션으로
getXxx()
메서드를 찾아 Xxx에 해당하는 필드에 값을 매핑한다.
자세하게 정리한 블로그 포스팅이 있어 첨부한다.
@RequestBody에 왜 기본 생성자는 필요하고, Setter는 필요 없을까? #3
이전 글에서는 RestController에서 @RequestBody 바인딩을 Jackson 라이브러리의 ObjectMapper가 하는 것을 확인했습니다.그리고 RequestBody를 생성할 때, DTO가 Property기반이 아니거나 Delegate를 한 상태가 아니라
velog.io
ObjectMapper
Jackson ObjectMapper
The <em>Jackson ObjectMapper</em> can read JSON into Java objects and write Java objects to JSON. This Jackson ObjectMapper tutorial explains how to use the Jackson ObjectMapper class.
jenkov.com
따라서 Jackson
라이브러리를 사용하면 역직렬화를 위해 빈생성자와 모든 필드의Getter
가 필요한 것이다.
@NoArgsConstructor
@AllArgsConstructor
public class ProductCache implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private static final String KEY_FORMAT = "product:%d";
private Long id;
private Long hubId;
private Long companyId;
private Integer quantity;
private String name;
// ...
public String getKey() {
return String.format(KEY_FORMAT, id);
}
}
class
→ JSON
으로 직렬화 할 때에도 getter
가 필요한 것일까?
dto 에서 @Getter
어노테이션을 제거하고 데이터를 저장해봤다.
{
"@class":"com.sparta.logistics.product.infrastructure.cache.dto.ProductCache",
"key":"product:1"
}
놀랍게도 key만 저장되었다.
해결 방법
@Getter
어노테이션을 선언하여 모든 필드에 대해 getter 메서드를 생성했다.
getKey()
메서드명을 key()
로 변경하여 직렬화 시 key라는 필드 생성을 방지했다.
결론
jackson
라이브러리는 직렬화/역직렬화 시 getXxx()
메서드를 통해 필드를 파악한다.
직렬화 시에 getXxx()
를 통해 반환받는 값을 xxx
필드에 매핑시켜 JSON
으로 파싱한다.
getKey() 라는 메서드가 key:1 을 반환한다면 “key” : “key:1” 으로 직렬화 된다.
즉, jackson을 통한 직렬화 / 역직렬화를 위해선 빈생성자와 getter가 강제된다.
이는 객체의 캡슐화를 해칠 수 있는 우려가 있기에
도메인 객체를 직렬화하는 경우 캡슐화를 고려하거나다른 라이브러리 사용을 고려해야할 것 같다.
jackson을 사용하여 직렬화 / 역직렬화를 수행한다면 도메인 객체가 아닌 dto를 통해 동작하도록 하자.
Reference
@RequestBody에 왜 기본 생성자는 필요하고, Setter는 필요 없을까? #3
이전 글에서는 RestController에서 @RequestBody 바인딩을 Jackson 라이브러리의 ObjectMapper가 하는 것을 확인했습니다.그리고 RequestBody를 생성할 때, DTO가 Property기반이 아니거나 Delegate를 한 상태가 아니라
velog.io
Jackson ObjectMapper
The <em>Jackson ObjectMapper</em> can read JSON into Java objects and write Java objects to JSON. This Jackson ObjectMapper tutorial explains how to use the Jackson ObjectMapper class.
jenkov.com
'트러블슈팅' 카테고리의 다른 글
Spring Event - Listener가 이벤트를 인식하지 못하는 문제 (Type Erasure) (0) | 2025.01.16 |
---|---|
이벤트 기반 비동기 통신 구현 및 Kafka를 사용한 이유 (feat. RabbitMQ) (2) | 2024.12.20 |
테스트 더블을 직접 구현한 테스트에서 발생했던 문제들 (0) | 2024.05.03 |
SpringBoot 멀티 모듈 프로젝트 Gradle 빌드 시 bootJar 태스크 실행 오류 (1) | 2024.02.10 |
모듈간 의존성은 있는데 소스 정보를 읽어오지 못하는 이슈 (Plain Archive의 사용처?) (1) | 2024.02.10 |