본문 바로가기
Java

Jackson 라이브러리의 직렬화/역직렬화

by 긍정의고등어 2022. 9. 21.

api 개발을 하다보면 자바 객체를 json으로 직렬화 하고 json으로 들어온 요청을 자바 객체로 역직렬화 하는 일이 빈번하게 일어난다. 서버에서 데이터를 처리해 Dto에 담아 클라이언트로 응답하면 내부적으로 JacksonObjectMapper를 이용하여 자바 객체를 json으로 직렬화 하는데, 자바 객체에는 없던 필드가 요청에 담겨 리턴되는 경우가 생길 수 있다. 이번 포스트에서는 이러한 현상의 원인을 제공하는 Jackson 라이브러리의 직렬화/역직렬화에 대해 정리한다.

예시


public class User {

    public String name;
    public Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public boolean isOld() {
        return age > 35;
    }
}

ObjectMapper objectMapper = new ObjectMapper();
User user = new User("young", 20);
objectMapper.writeValueAsString(user); // "{\"name\":\"young\",\"age\":20,\"old\":false}"

위와 같이 User 클래스를 선언하고 ObjectMapper를 통해 직렬화 하면 객체에 없던 필드인 old 필드가 생겨난다. ObjectMapper 내부적으로 직렬화 할때 직렬화 대상으로 삼는 요소 중 우리가 흔히 생각하는 필드 외에 다른 것들이 있기 때문이다.

 

직렬화/역직렬화 대상


public field

ObjectMapper는 기본적으로 필드를 대상으로하는데 그 중에서 public 필드를 대상으로 삼는다. 아래와 같이 public으로 필드를 생성하고 직렬화, 역직렬화를 테스트 해보면 정상적으로 실행되는것을 확인할 수 있다.

public class Person {
    public String name;

    public Person() {
    }

    public Person(String name) {
        this.name = name;
    }
}

@Test
void public_필드_직렬화() throws JsonProcessingException {
    //given
    Person person = new Person("joon");

    //when
    String result = mapper.writeValueAsString(person);

    //then
    assertThat(result, containsString("name"));
}

@Test
void public_필드_역직렬화() throws JsonProcessingException {
    //given
    String json = "{\"name\":\"joon\"}";

    //when
    Person result = mapper.readValue(json, Person.class);

    //then
    assertThat(result.name, equalTo("joon"));
}

Getter, Setter

위 테스트를 통해 public으로 선언한 객체의 필드는 자동으로 매핑되는 것을 확인했지만, 통상 클래스 선언시에 필드를 private로 선언하기 때문에 Jackson에서는 이를 위해 getter 혹은 setter가 있을 경우 해당 메서드를 통해 필드 이름을 유추하여 직렬화/역직렬화에 사용한다.

그림 1. getter, setter를 통해 필드명 유추

Jackson은 getter와 setter 메서드명 중 get, set을 제거한 이름의 첫 문자를 소문자로 치환하여 필드명을 유추한다.

따라서 필드를 private로 선언하더라도 getter 혹은 setter가 있다면 문제 없이 ObjectMapper를 통해 직렬화/ 역직렬화 할 수 있다.

 

위에서 Getter와 Setter 모두 private 필드를 Jackson이 인식할 수 있도록 해준다고 했지만 정확하게는 Getter는 직렬화/역직렬화를 모두 가능케 하지만 Setter는 역직렬화만 가능하게 해준다. 이는 getter를 가진 private filed는 property로 간주되지만 setter는 그렇지 않기 때문이며, 실제로 아래와 같이 setter만 가지는 필드는 역직렬화만 가능하고 직렬화가 불가능하다.

public class DtoWithSetter {
    private int intValue;

    public void setIntValue(int intValue) {
        this.intValue = intValue;
    }

    public int accessIntValue() {
        return intValue;
    }
}

@Test
public void setter_역직렬화_테스트() throws JsonProcessingException, JsonMappingException, IOException {
    String jsonAsString = "{\"intValue\":1}";
    ObjectMapper mapper = new ObjectMapper();

    DtoWithSetter dtoObject = mapper.readValue(jsonAsString, DtoWithSetter.class);

    assertThat(dtoObject.anotherGetIntValue(), equalTo(1));
}

@Test
public void setter_직렬화_테스트() throws JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper();

    DtoWithSetter dtoObject = new DtoWithSetter();

    String dtoAsString = mapper.writeValueAsString(dtoObject);
    assertThat(dtoAsString, not(containsString("intValue")));
}

위 코드에서는 private 필드인 intValue에 대해 setter만 네이밍 컨벤션에 따라 선언하였고, getter역할은 네이밍 컨벤션을 따르지 않는 accessIntValue메서드가 수행한다. Json 문자열을 객체로 변환하는 역직렬화 테스트에서는 setter에 의해 정상적으로 intValue가 매핑 되었으나, 반대의 경우는 네이밍 컨벤션을 따르는 getter가 없어 intValue 필드를 인식하지 못하고 직렬화에 실패한 것을 확인할 수 있다.


isGetter

맨 위에서 언급한 예시에서는 get~, set~ 와 같은 메소드가 없음에도 불구하고 old라는 필드가 생겨났다. Jackson에서는 일반적인 getter, setter 외에 isGetter또한 getter로 인식하여 자동으로 필드로 인식 후 직렬화, 역직렬화에 사용한다.

 

Jackson이 감지하는 접근제한자


ObjectMapper를 통해 프로퍼티 정보를 얻는 방법은 기본생성자와 함께 위에서 언급한 필드, getter, setter, isGetter를 통해 얻을 수 있다. 평소 dto를 선언할 때 기본 생성자의 접근 제한을 private로 두는 편인데 어떻게 해당 객체에 대한 프로퍼티를 찾을 수 있는지 궁금하여 ObjectMapper의 내부 코드를 들여다 보았다.

public interface VisibilityChecker<T extends VisibilityChecker<T>> {
    public static class Std implements VisibilityChecker<VisibilityChecker.Std>, Serializable {
        protected static final VisibilityChecker.Std DEFAULT;
        //...

        public static VisibilityChecker.Std defaultInstance() {
            return DEFAULT;
        }

        static {
            DEFAULT = new VisibilityChecker.Std(
                Visibility.PUBLIC_ONLY, 
                Visibility.PUBLIC_ONLY, 
                Visibility.ANY, 
                Visibility.ANY, 
                Visibility.PUBLIC_ONLY);
        }

        public Std(Visibility getter, Visibility isGetter, Visibility setter, Visibility creator, Visibility field) {
            this._getterMinLevel = getter;
            this._isGetterMinLevel = isGetter;
            this._setterMinLevel = setter;
            this._creatorMinLevel = creator;
            this._fieldMinLevel = field;
        }        
    }
}

ObjectMapper내에서 사용되는 VisibilityChecker 코드인데 기본적으로 각 요소에 대한 접근제한자를 지정해두었다. getter, isGetter, field는 기본적으로 public으로 선언하여야 인식이 가능하고 setter, 생성자는 접근제한자에 제한이 없이 접근이 가능한 것을 확인할 수 있으며, 이 때문에 private으로 생성자를 선언하였음에도 자동으로 필드 매핑이 가능했다.


데이터 매핑 정책 변경하기

Default라는 이름에서 알 수 있듯이 이러한 요소들에 대한 접근제한자 정책을 개발자 임의로 수정할 수 있는데, 객체에 어노테이션을 붙여 수정하거나, ObjectMapper에 옵션을 주어 수정할 수 있다.

@JsonAutoDetect(fieldVisibility=JsonAutoDetect.Visibility.ANY, getterVisibility=JsonAutoDetect.Visibility.ANY)
public class Person {
    public String name;
}

//------

ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(
	mapper.getSerializationConfig().getDefaultVisibilityChecker()
		.withFieldVisibility(JsonAutoDetect.Visibility.ANY)
		.withGetterVisibility(JsonAutoDetect.Visibility.ANY)
);

 

의도치 않은 프로퍼티 매핑 피하기


예시와 같이 직렬화로 인해 의도치 않은 프로퍼티가 생겨나는 것을 방지하려면 Jackson 라이브러리의 특징을 이용하여 아래와 같이 조치할 수 있다.

 

메서드명 변경하기

getter, isGetter 메소드 네이밍 컨벤션을 피해 메서드명을 변경함으로써 Jackson의 프로퍼티 감지에서 벗어날 수 있다.

public class User {
    ...
    
    public boolean checkOld() {
        return age > 35;
    }
}

어노테이션 사용하기

스프링에는 Json과 관련된 많은 어노테이션들이 존재하는데, 그 중 Json value에서 특정 요소를 제외시킬 때 사용하는 @JsonIgnore 어노테이션을 사용하여 의도치 않은 매핑을 피할 수 있다.

public class User {
    ...
    
		@JsonIgnore
    public boolean isOld() {
        return age > 35;
    }
}

@JsonIgnore 외에도 위 단락에서 보았던 @JsonAutoDetect 어노테이션을 이용하여 해당 객체에 대한 접근제한자 범위를 수정할 수 있다.

@JsonAutoDetect(isGetterVisibility=JsonAutoDetect.Visibility.NONE)
public class User {
    ...
    
    public boolean isOld() {
        return age > 35;
    }
}

ObjectMapper 설정 변경하기

위 단락에서 살펴보았던 ObjectMapper의 감지 접근제한자 범위를 수정하여 마찬가지로 의도치 않은 매핑을 피할 수 있다.

ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(
	mapper.getSerializationConfig().getDefaultVisibilityChecker()
		.withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)
);

 

정리


맨 처음 관련 이슈를 접했을 때 아무리 코드를 살펴보아도 이유를 찾을 수 없어 고생했던 기억이 있다. 당시에는 검색을 통해 끝내 이유를 찾고 대충 ‘getter 사용시에 주의해야겠다’ 정도의 생각만 하고 지나쳤지만, 이번 기회에 더 깊게 공부하게 되면서 의도치 않은 매핑을 피할 수 있는 다양한 방법을 알게되어 좋은것 같다. 이제까지는 제일 간편한 @JsonIgnore만 사용해왔지만 필드에 매핑되지 않는 getter 메소드가 많은 경우는 @JsonAutoDetect를 활용하는 등, 상황에 따라 다른 방법들도 활용해 보면 좋을것 같다.

 

참고


'Java' 카테고리의 다른 글

[Spring Cloud] HA of Service Discovery  (0) 2023.02.07
[Spring Cloud] Service Discovery  (0) 2023.01.22
제네릭을 이용한 마이바티스 쿼리 유틸 만들어보기  (0) 2022.09.13
GC 개념 및 동작 원리  (0) 2022.09.13
JVM 이란?  (0) 2022.08.23

댓글