menu

Jackson이 다형성을 다루는 방법

  • date_range 28/04/2021 00:00 info
    sort
    JAVA
    label
    JACKSON

잭슨은 json데이터와 java object 간에 직렬화/역직렬화 기능을 제공하는 강력하고 편리한 라이브러리이다.
잭슨을 통해 다형성이면서(polymorphic) 여러 하위구조를 갖는 자바 객체도 손쉽게 json 포맷으로 데이터 바인딩할 수 있다.

이 작업을 하려면 직렬화를 수행할 때 필요한 정보를 잭슨에게 넘겨주어야 한다. 그래야 역직렬화 시 데이터를 복원할 때
정확한 하위클래스 타입으로 복원할 수 있다.

본격적 내용으로 들어가기 전에, 직렬화와 역직렬화에 대해서 간단하게 짚고 넘어가도록 하자. 먼저 직렬화는

직렬화 또는 시리얼라이제이션은 컴퓨터 과학의 데이터 스토리지 문맥에서 데이터 구조나 오브젝트 상태를 동일하거나<br>  
다른 컴퓨터 환경에 저장하고 나중에 재구성할 수 있는 포맷으로 변환하는 과정이다. <br>  
오브젝트를 직렬화하는 과정은 오브젝트를 마샬링한다고도 한다.

라고 위키백과에 정의되어 있다.

좀더 알기쉽게 간단히 말하자면, 객체를 저장하거나 다른 PC나 데이터베이스 등의 어딘가로 옮기기 위해서 필요한 작업을 말한다. 즉 객체에 저장된 데이터를 스트림에 write하기 위해 연속적인serial 데이터로 변환한다. java에서 파일을 읽어오거나 저장할 때 byte형태로 직렬화하는 것을 많이 봤을 것이다. java의 byte 직렬화에 대해서는 이 글을 참고하면 좋을 것이다.

직렬화에 대해서 이해했다면 역직렬화에 대한 이해는 더욱 쉽다. 말 그대로 직렬화를 역으로 수행하여 byte형태나 다른 형태로 변환된 데이터를 원래의 형태로 되돌리는 작업을 말한다.

다형성 데이터의 경우 어떻게 해야할까? 다형성이란 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미한다.
자바에서는 상속을 통해 이러한 다형성을 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조할 수 있도록 한다.

이러한 다형성이라는 특징을 가진 상황에서,

다형성에 대한 역직렬화가 중요한 이유는, 자바의 상속과 non-concrete types을(추상 클래스, 인터페이스와 같은) 역직렬화 함에 있어서 정확한 타입을 지정하는 것이 중요한 문제이기 때문이다. 역직렬화 시에, 매핑하려는 데이터가 추상 클래스 타입이거나 인터페이스 타입의 경우에 해당 클래스의 서브타입이 어떤 클래스인지 잭슨이 판단할 수 없다. 다음의 경우를 보자.

public class Person {
  public String name;
  public int age;
  public PhoneNumber phone; // embedded POJO
}
abstract class PhoneNumber {
  public int areaCode, local;
}
public class InternationalNumber extends PhoneNumber {
  public int countryCode;
}
public class DomesticNumber extends PhoneNumber { }



Person 클래스를 직렬화/역직렬화하려고 한다면, Person 클래스의 멤버인 PhoneNumber 타입의 데이터가 추상클래스이므로 문제가 된다. 이 경우 이 추상클래스를 상속받아 구현한 클래스가 InernationalNumber인지, DomesticNumber인지 잭슨 스스로 결정할 수 없기 때문이다.

따라서 직렬화 시에 충분한 정보를 제공하여서 역직렬화 시에 올바른 서브타입을 선택할 수 있도록 해야 한다.

위에서 말한 것처럼 직렬화 시에 역직렬화에 필요한 타입 정보를 제공해야 한다. 이를 위해 잭슨이 사용하는 방법은 Defaulttyping이라는 것이다.

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(); // defaults to OBJECT_AND_NON_CONCRETE
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

위의 코드에 첫째 줄에 나온 ObjectMapper는 json데이터를 read/write하고 하는 데 사용되며, Jackon 라이브러리의 주요 기능을 담당한다. ObjectMapper를 사용하기 전에, 앞서 말한 default Typing을 설정해야 하는데, ObjectMapper.enableDefaultTyping()의 경우 기본값으로 OBJECT_AND_NON_CONCRETE값이 설정된다. defaultTyping은 enumeration 클래스이며, objectMapper의 enableDefaultTyping메서드의 매개변수로 사용된다.

enum 클래스인 DefaultTyping의 필드는 다음과 같다.

defaultTyping 용도
JAVA_LANG_OBJECT 선언된 유형으로서 객체가 포함된 프로퍼티(명시적 타입을 제외한 제네릭 타입을 포함한다.) 정보를 제공하겠다.
NON_CONCRETE_AND_ARRAYS OBJECT_AND_NON_CONCRETE에 포함된 모든 타입의 프로퍼티와 이러한 요소의 배열 타입 정보를 제공하겠다.
NON_FINAL JSON으로부터 올바르게 유추된 String, Boolean, Integer, Double을 제외한 모든 non-final 타입정보를 제공하겠다. 뿐만 아니라 non-final 타입의 모든 배열의 프로퍼티 정보도 제공하겠다.
OBJECT_AND_NON_CONCRETE 선언된 타입의 객체이거나 abstract 타입의 프로퍼티 정보를 제공하겠다.

위와 같은 defaultTyping을 미리 설정함으로 ObjectMapper를 통해 직렬화/역직렬화 작업을 할 때에, 전달해야하는 프로퍼티 정보를 의도적으로 제한/조정할 수 있는 것이다.

Annotations 잭슨 다형성 타입을 사용할 때 제공돼야 할 타입 정보는 어노테이션을 통해서도 제공이 가능하다.

어노테이션의 종류는 아래와 같다.
|이름|설명| |—|:—|
|@JsonTypeInfo|어떤 타입의 정보가 직렬화 작업에 포함되는지를 가리킨다.|
|@JsonSubTypes|어노테이션 타입의 sub-type을 가리킨다.|
|@JsonTypeName|어노테이션이 선언 된 클래스의 논리적 타입 이름을 정의한다. |

이렇게만 보면 사용법에 대한 감이 잘 오지 않으니 아래의 코드를 보자.

public class Zoo {
  public Animal animal;
  
  @JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,   
    include = As.PROPERTY,
    property = "type")
  @JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "cat")
  })

  public static class Animal {
    public String name;
  }

  @JsonTypeName("dog")
  public static class Dog extends Animal {
    public double barkVolume;
  }
  
  @JsonTypeName("cat")
  public static class Cat extends Animal {
    boolean likesCream;
    public int lives;
  }
}

이 어노테이션들은 서브클래스들이 올바르게 재생성되도록 해준다. JsonTypeInfo.Id는 직렬화 중에 JSON에 포함되어 역직렬화에 사용할 수 있는 여러 형식 식별자의 정의이며, 그 값들은 아래와 같다.

이름 설명
CLASS fully-qualified한 자바 클래스 이름이 타입 식별자로 사용된다.
CUSTOM 타입지정 매커니즘이 사용자 지정 구성을 사용하여 사용자 지정 처리를 사용함을 의미한다.
MINIMAL_CLASS 최소 경로를 가진 자바 클래스 명이 타입 식별자로 사용된다.
NAME 논리적 타입 명이 유형 정보로 사용됨을 의미한다. 그러면 이름을 실제 콘크리트 유형(Class)으로 별도로 확인해야 한다.
NONE 이는 명시적 형식 메타데이터가 포함되지 않으며, 다른 주석과 함께 보강될 수 있는 contextual 정보를 사용하여 순수하게 타이핑이 수행된다는 것을 의미한다.

위의 코드에서 Animal에 선언된 @JsonTypeInfo 어노테이션은 논리적 타입 명이 사용됐다.

@JsonTypeInfo는 Animal 클래스에 대한 논리적 이름의 사용을 지정한다. @JsonTypeInfo.AS enumeration은 포함하는 메커니즘을 지정한다. 그리고 이것은 속성이나 wrapper나 배열 객체 등을 속성으로 하는 단일 구성의 메커니즘도 가능하다. @JsonTypeInfo를 사용하는 것은 enableDefaultTyping 메서드를 호출하는 것과 동일한 기능이다.

Jackson 어노테이션을 사용하는 데 있어서 한가지 문제점은, 부모타입에서 서브타입으로 의존성을 가짐으로 인해서 캡슐화가 중단된다는 것이다. 이것은 이 매커니즘을 사용하기에 어렵게 만든다. 예를 들어서, 타사 라이브러리에서 특정 클래스를 상속받아 서브클래싱할 때, 잭슨은 이 문제를 해결할 방법이 없다. 하지만 ObjectMapper에서 registerSubtypes 메서드를 사용하거나 TypeResolverBuilder을 구현함으로 특정 클래스를 등록하는 방법을 사용할 수 있다.

모든 타입을 표시하는 기능은 유연한 역직렬화를 위한 핵심 요구사항이다.

실제 사용의 상당 부분은 java.lang.Object를 정의하여 해석할 수 있는 모든 클래스를 역직렬화할 수 있도록 돼 있으니 큰 불편은 없을 것이라고 생각한다.

참고 : Jackson Deserialization Vulnerabilities - Robert C. Seacord – Technical Director