du.study기록공간

lombok + jackson 사용하여 직렬화시, 변수명 첫 단어가 한글자일때 본문

자바

lombok + jackson 사용하여 직렬화시, 변수명 첫 단어가 한글자일때

du.study 2024. 3. 18. 22:20
728x90

 

다른회사로 요청,응답을 기록하는 과정에, 필드의 로그값이 이중으로 찍혀있는 모습을 보고 내용을 정리해봅니다.

 

우선 발생된 상황을 정리해보면 다음과 같습니다.

1. request dto는 롬복의 @Getter와 @JsonProperty를 사용하고 있습니다.

2. 변수명중 두번째 글자가 대문자인 변수들이 있고, JsonProperty는 이 값을 다르게 사용하고 있습니다. 

3.  objectMapper.writeValusAsString() 를 활용하여 로그를 작성중에 있습니다.


이 경우, 의도했던 값과 다르게 유사한 변수명이 추가로 String에 노출되는 현상이 발생됩니다.

lombok의 getter를 사용하면서 objectMapper.writeValusAsString()을 사용할때 발생될 수 있는 문제점을 확인하고자합니다.

우선 간단한 예시를위해 클레스를 만듭니다.

@Getter
@Builder
public class Test1 {
    private String pCouponCode;
}

 

빌드된 파일을 보면 두번째 글짜가 대문자인 변수를 만들면 lombok은 아래와같은 getter를 만들어줍니다.

 

요걸 실제로 objectMapper.writeValusAsString 를 통해 찍어보면 아래와같이 변환이됩니다.

{"pcouponCode":"pCouponCode"}

 

하지만 보통 원하는 값은 아래와 같을겁니다.

 {"pCouponCode":"pCouponCode"}

 

우선.. 이게 왜 이렇게 변했나를 확인해보기 위해 jackson 변환과정의 해당 클래스를 확인해봅니다.  (궁금하니까.. 한줄한줄 보면서 들어가봅니다)

com.fasterxml.jackson.databind.util.BeanUtil

해당 클레스에서 get, is, set의 prefix를 사용하는 경우 아래 method를 통해 변수명이 변환됩니다.

 

요 코드가 한글자를 쓰면 안된다는 이유입니다.

 protected static String legacyManglePropertyName(final String basename, final int offset)
    {
        final int end = basename.length();
        if (end == offset) { // empty name, nope
            return null;
        }
        // next check: is the first character upper case? If not, return as is
        char c = basename.charAt(offset);
        char d = Character.toLowerCase(c);
        
        if (c == d) {
            return basename.substring(offset);
        }
        // otherwise, lower case initial chars. Common case first, just one char
        StringBuilder sb = new StringBuilder(end - offset);
        sb.append(d);
        int i = offset+1;
        for (; i < end; ++i) {
            c = basename.charAt(i);
            d = Character.toLowerCase(c);
            if (c == d) {
                sb.append(basename, i, end);
                break;
            }
            sb.append(d);
        }
        return sb.toString();
    }

해당 로직을 보면 첫글자를 소문자로 만들고, 연속된 글자중 대문자는 전부 소문자로 변경후,  name을 가져옵니다!
이 로직으로 인해 원했던 pCouponCode 이 아니라 pcouponCode 를 가져옵니다.

 

 

그렇다면 원하는 값을 어떻게 가져올것인가를 고민해보면..

1. getter를 직접 생성한다.

해당 방법을 이용한다면,   {"pCouponCode":"pCouponCode"} 와 같은 응답을 얻을 수 있습니다. 
위에 BeanUtil의 로직을 보면 첫글자가 소문자인경우 그대로 리턴을 해주고 있습니다. 

@Getter
@Builder
public class Test1 {

    private String pCouponCode;

    public String getpCouponCode(){
        return pCouponCode;
    }
}

하지만 수작업을 피하고 롬복을 사용하고 싶으니 다른방법을 찾아봅니다. 

 

 

2. @JsonProperty를 사용하는경우

아래와 같이 클래스를 만들고 objectMapper.writeValusAsString() 를 하면 전혀 의도되지않은 값을 만날 수 있습니다.
{"pcouponCode":"pCouponCode","pCouponCode":"pCouponCode"}

@Getter
@Builder
public class Test1 {

    @JsonProperty(value = "pCouponCode")
    private String pCouponCode;
}

 

다시 코드를 파해쳐보면.. 아래 부분의 로직을 보다보면 getter와 jsonProperty로 설정한 값이 각각 다른 key값을 가지게 되기때문에 두개의 값이 추가되어 변환되는 모습을 보게 됩니다. (하나는 변수명(pCouponCode), 하나는 getter의 이름 (pcouponCode))

com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector

POJOPropertiesCollector {
...

    protected void collectAll()
    {
        LinkedHashMap<String, POJOPropertyBuilder> props = new LinkedHashMap<String, POJOPropertyBuilder>();

        // First: gather basic data
        _addFields(props);
        _addMethods(props);
        // 25-Jan-2016, tatu: Avoid introspecting (constructor-)creators for non-static
        //    inner classes, see [databind#1502]
        if (!_classDef.isNonStaticInnerClass()) {
            _addCreators(props);
        }
        _addInjectables(props);

        // Remove ignored properties, first; this MUST precede annotation merging
        // since logic relies on knowing exactly which accessor has which annotation
        _removeUnwantedProperties(props);
        // and then remove unneeded accessors (wrt read-only, read-write)
        _removeUnwantedAccessor(props);

}

 

이부분을 해결하기 위해서는 아래와같이 @JsonAutoDetect를 사용하여 직렬화 대상을 설정해줍니다.

@Getter
@Builder
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE)
public class Test1 {

    @JsonProperty(value = "pCouponCode")
    private String pCouponCode;
}

해당 방식으로 설정 후, objectMapper를 사용하여 변환하면 사용하는 DTO의 수정만으로 원하는 값을 얻을 수 있게됩니다.

 

 

728x90
Comments