jackson-databind, AUTO_DETECT_IS_GETTERS 옵션(is getter)
kindof
·2024. 2. 14. 22:11
0. 문제
아래 예제 코드를 보겠습니다.
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class Book implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String title;
@Column
private int stock;
@Column
private boolean isSoldOut;
// .. 생략
}
간단한 책(Book) Entity 입니다. 설명을 위해 필요한 코드만 작성했습니다. 해당 엔티티에서 주의깊게 볼 부분은 private boolean isSoldOut인데요.
빌드 이후에 .class 파일을 보면 isSoldOut 필드에 대한 Getter 메서드가 getIsSoldOut()이 아닌, isSoldOut() 자체로 정의된 것을 볼 수 있습니다.
1. AUTO_DETECT_IS_GETTERS
jackson-databind > MapperFeature.java 에서는 ObjectMapper의 몇 가지 기능에 대한 On-Off 기능을 정의합니다.
public enum MapperFeature implements ConfigFeature
{
// .. 생략
/**
* Feature that determines whether regular "getter" methods are
* automatically detected based on standard Bean naming convention
* or not. If yes, then all public zero-argument methods that
* start with prefix "get"
* are considered as getters.
* If disabled, only methods explicitly annotated are considered getters.
*<p>
* Note that since version 1.3, this does <b>NOT</b> include
* "is getters" (see {@link #AUTO_DETECT_IS_GETTERS} for details)
*<p>
* Note that this feature has lower precedence than per-class
* annotations, and is only used if there isn't more granular
* configuration available.
*<p>
* Feature is enabled by default.
*/
AUTO_DETECT_GETTERS(true),
/**
* Feature that determines whether "is getter" methods are
* automatically detected based on standard Bean naming convention
* or not. If yes, then all public zero-argument methods that
* start with prefix "is", and whose return type is boolean
* are considered as "is getters".
* If disabled, only methods explicitly annotated are considered getters.
*<p>
* Note that this feature has lower precedence than per-class
* annotations, and is only used if there isn't more granular
* configuration available.
*<p>
* Feature is enabled by default.
*/
AUTO_DETECT_IS_GETTERS(true),
해당 옵션들은 Serialization, Deserialization 전 단계에서 적용되어 ObjectMapper 클래스가 이러한 설정을 실제로 적용할 지 결정하고, ObjectReader/ObjectWriter를 통해 설정을 적용하는데요.
위 코드에서 AUTO_DETECT_IS_GETTERS 옵션은 default = true로 설정되어 있으며, 설명을 읽어보면 "is" 접두사로 시작하고 반환 형식이 boolean(primitive)인 public method 메서드가 "is getter"로 간주된다고 합니다.
따라서 우리가 작성했던 isLocal 필드는 is + boolean 타입을 갖고 있기 때문에 해당 필드에 대한 Getter는 getIsLocal()이 아닌, is getter 형식의 isLocal() 자체가 됩니다.
이러한 설정이 개발자로 하여금 getIsLocal() 대신에 isLocal() 이라는 좀 더 의미가 잘 전달되는 코드를 작성할 수 있게 합니다.
하지만 아래와 같이 해당 값 자체를 Serialize/Deserialize 할 때는 혼란의 소지가 있을 수 있는데요.
위 테스트는 Book 클래스 자체를 Serialize 할 때 isSoldOut 필드가 어떻게 변환되는지 보여줍니다.
결과에서 확인할 수 있는 것처럼 isSoldOut이 아닌, soldOut으로 Serialize 되는데요. 위에서 설명한 것처럼 해당 필드는 is getter가 되기 때문에 앞에 'is'를 제거하고 처음 문자를 소문자로 바꾸게 된 것 입니다.
따라서, 만약 Book 엔티티 혹은 DTO로 비슷한 is getter 대상인 필드를 Serialize 하여 다른 곳(BE/FE)에 전달하면 해당 필드는 원본 필드 이름으로 파싱할 수 없습니다.
즉, 아래 코드는 틀린 코드입니다.
fetch('url/where/to/fetch/book', {
method: 'GET',
headers: {
'Content-Type': 'application/json' // JSON 형식의 데이터 요청
}
})
.then(response => {
// .. 생략
return response.json(); // JSON 데이터로 변환하여 반환
})
.then(data => {
const soldOut = data.isSoldOut // <- 파싱 불가능
})
.catch(error => {
// .. 생략
});
위에서 설명한 것처럼 해당 엔티티의 필드는 soldOut으로 Serialize 되기 때문입니다.
지금까지 설명한 것처럼, AUTO_DETECT_IS_GETTERS 옵션은 분명 편의를 위해 유용하게 사용할 수 있는 옵션입니다.
하지만 이 내용을 정확히 인지하지 않고 사용하면 Serialize/Deserialize 문제는 언제든 발생할 수 있습니다. 특히 FE 디버깅이 자유롭지 않은 BE 개발자라면 더욱...
이러한 Serde 문제가 발생하거나, 의도하지 않은(?) 필드나 메서드의 변환/생성이 있다면 해당 클래스의 .class 파일을 한 번 확인해보고, Jackson 라이브러리를 한 번 잘 찾아보시면 도움이 될 것 같습니다.
한편, 구글링을 해보면 isXXX 형식의 필드를 그대로 사용하기 위해 @JsonProperty를 사용하는 방식도 소개하는데요. 이 방식을 사용하면 isSoldOut 필드를 json 형식에서 그대로 사용할 수 있으나, is getter는 여전히 남아있어 혼란을 주기 쉽다는 생각입니다.
@Column
@JsonProperty("isSoldOut")
private boolean isSoldOut;
대안으로, boolean 타입이 아닌 Boolean Wrapper 타입을 사용함으로써 is getter가 생성되는 것을 방지할 수도 있는데요. 이에 대해서는 두 가지를 생각해보면 좋겠다는 생각입니다.
[1] isXXX 에서 is를 제외해도 충분히 좋은 변수인가?
[2] 해당 값이 nullable한가?
만약 isXXX 라는 변수명이 충분히 의미있고(팀/서비스 내의 컨벤션 등에 의해), nullable한 값이라면 Wrapper 타입으로 해당 값을 감싸는 것도 좋은 방법입니다.
하지만 위 예시에서 isSoldOut 대신에 soldOut 이라는 필드명도 충분히 의미있고, 해당 값이 nullable 하지 않다면 불필요한 Wrapping을 위한 타입은 사용하지 않는 것이 더 나을 수 있습니다.
각자의 상황에 맞춰 이러한 변수의 이름을 잘 짓거나 Jackson을 커스텀화하거나 Lombok을 잘 쓰거나 하는 방식으로 고민해보면 좋을 것 같습니다.
끝!
'Spring & Springboot' 카테고리의 다른 글
Spring 5.2.X 버전에서 @RequestParam required = true 옵션은 null safety 하지 않다. (1) | 2024.01.28 |
---|---|
Spring RestTemplate 까보기 (1) | 2024.01.11 |
Spring READ 관련 API에서 @Transactional(readOnly = true)는 필수인가? (0) | 2023.11.10 |
장애 전파 대응을 위한 Resilience4j Circuit Breaker 실습 (0) | 2023.11.06 |
JPA 트랜잭션 격리 수준과 낙관적 락 동작을 테스트해보자. (0) | 2023.10.15 |