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인데요.

Decompiled Book

 

빌드 이후에 .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 할 때는 혼란의 소지가 있을 수 있는데요.

Test code

위 테스트는 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;

json : isSoldOut
Getter : isSoldOut

 

대안으로, boolean 타입이 아닌 Boolean Wrapper 타입을 사용함으로써 is getter가 생성되는 것을 방지할 수도 있는데요. 이에 대해서는 두 가지를 생각해보면 좋겠다는 생각입니다.

 

[1] isXXX 에서 is를 제외해도 충분히 좋은 변수인가?

[2] 해당 값이 nullable한가?

 

만약 isXXX 라는 변수명이 충분히 의미있고(팀/서비스 내의 컨벤션 등에 의해), nullable한 값이라면 Wrapper 타입으로 해당 값을 감싸는 것도 좋은 방법입니다.

 

하지만 위 예시에서 isSoldOut 대신에 soldOut 이라는 필드명도 충분히 의미있고, 해당 값이 nullable 하지 않다면 불필요한 Wrapping을 위한 타입은 사용하지 않는 것이 더 나을 수 있습니다.

 

각자의 상황에 맞춰 이러한 변수의 이름을 잘 짓거나 Jackson을 커스텀화하거나 Lombok을 잘 쓰거나 하는 방식으로 고민해보면 좋을 것 같습니다.

 

끝!