spring / / 2023. 9. 14. 10:48

JsonTypeInfo으로 추상 클래스 매핑하기

json을 다양한 클래스로 매핑하기

일반적으로 json을 deserialize할 때 특정 유형에 따라 다른 객체로 매핑을 하고 싶을 경우가 있다.

REST로 @RequestBody의 특정 json을 받는 경우나, DB에서 데이터를 읽어서 객체로 매핑하는 경우가 있다.

이런 경우 기본적인 유형의 경우는 별 문제가 없지만 추상 클래스로 정의되어 있는 유형일 경우에는 일반적인 매핑 방법을 사용하지 못한다.

추상 클래스 사례

아래와 같은 Message 클래스가 있다고 하자. Message에는 TextMessageFileMessage 두 가지의 유형이 존재한다.

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class Message {

    private long messageId;

    private MessageType type;

    public Message(long messageId, MessageType type) {
        this.messageId = messageId;
        this.type = type;
    }
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TextMessage extends Message {
    private String message;

    public TextMessage(long messageId, String message) {
        super(messageId, MessageType.TEXT);
        this.message = message;
    }
}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FileMessage extends Message {
    private String fileInfo;

    public FileMessage(long messageId, String fileInfo) {
        super(messageId, MessageType.FILE);
        this.fileInfo = fileInfo;
    }
}
public enum MessageType {
    TEXT,
    FILE;
}

기본 매핑

여기서 아래와 같은 json을 Message 클래스로 deserialize 해보자.

{
  "messageId" : 1,
  "type" : "TEXT", 
  "message" : "텍스트"
}

objectmapper로 deserialize할 때 이렇게 사용할 수 있다.

ObjectMapper mapper = new ObjectMapper();
String json = "{\"messageId\":1,\"type\":\"TEXT\",\"message\":\"텍스트\"}";
Message message = mapper.readValue(json, TextMessage.class); // TEXT로 매핑할 경우
Message message = mapper.readValue(json, FileMessage.class); // File로 매핑할 경우

하지만 위의 경우는 구체적인 유형이 결정되어 있는 상태라서 매핑이 되지만 아래와 같은 방식으로 사용할 수 없을까?

Message message = mapper.readValue(json, Message.class);

실행을 해보면 아래와 같은 오류가 발생한다.

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.example.jsontypeinfo.message.Message` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (String)"{"messageId":1,"type":"TEXT","message":"텍스트"}"; line: 1, column: 1]

오류 내용은 구체 클래스로 매핑되거나 추가적인 정보를 포함해야 한다고 한다.

추상 클래스 매핑

위에서 본 것 처럼 type=TEXT 인 경우는 TextMessage로 매핑하고 type=FILE인 경우는 FileMessage로 자동으로 매핑해주는 방법이다.

  1. type: TEXT
{
  "messageId" : 1,
  "type" : "TEXT", 
  "message" : "텍스트"
}
Message message = mapper.readValue(json, Message.class); // TextMessage
  1. type: FILE
{
  "messageId" : 1,
  "type" : "FILE", 
  "fileInfo" : "fileinfo"
}
Message message = mapper.readValue(json, Message.class); // FileMessage

@JsonTypeInfo

이렇 듯 추상 타입으로 매핑(deserialize)할 때 특정 필드 값을 참조하여 자동으로 매핑을 해주는 것이 @JsonTypeInfo이다.

@JsonTypeInfo은 추상타입에 정의를 하고 어떤 필드(type)가 어떤 클래스로 매핑될 지 결정을 해주는 역할을 한다.

또한 추상 타입의 하위 타입이 어떤 것인지 정의하는 @JsonSubTypes와 같이 사용하게 된다.

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type", visible = true)
@JsonSubTypes({
    @JsonSubTypes.Type(value = TextMessage.class, name = "TEXT"),
    @JsonSubTypes.Type(value = FileMessage.class, name = "FILE")
})
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public abstract class Message {

    private long messageId;

    private MessageType type;

    public Message(long messageId, MessageType type) {
        this.messageId = messageId;
        this.type = type;
    }
}

@JsonTypeInfo

  • use = JsonTypeInfo.Id.NAME

  • 식별자를 어떤 것으로 지정할 것인지 정의

  • include = JsonTypeInfo.As.EXISTING_PROPERTY

    • 현재 필드 정보를 사용
    • 만일 JsonTypeInfo.As.PROPERY를 사용한다면 직렬화 시 type정보가 두 번 표시된다.
  • property = "type"

    • 유형에 대한 필드 명이다. 소스 내에서 MessageType type으로 지정이 되어 있다.
  • visible = true

  • type으로 지정한 필드(MessageType)로 역직렬화를 할지 여부

    • false로 지정되면 직렬화되지 않는다. (null)
  • defaultImpl = TextMessage.class

    • 여기서 설정하지는 않았는데, defaultImpl은 type 값이 존재하지 않을 경우 매핑할 클래스의 기본 값을 설정할 수 있다.

    • 예를 들어 defaultImpl = TextMessage.class으로 지정을 하는 경우에는 아래와 같이 type이 없는 경우에 TextMessage로 매핑하겠다는 의미이다.

      {
        "messageId" : 1,
        "message" : "텍스트"
      }

@JsonSubTypes

  • value = TextMessage.class는 어떤 클래스로 매핑할 지 정의
  • name = "TEXT"는 type의 이름이 TEXT일 경우 TextMessage로 매핑한다는 의미이다.
    여기서 name은 생략될 수 있는 데 생략하는 경우 아래와 같이 실제 매핑되는 클래스에 @JsonTypeName를 정의해야 한다.
  @JsonTypeName(value = "TEXT")
  public class TextMessage extends Message {
    ...
  }
  • 아래와 같이 두 값을 동시에 설정하니 @JsonSubTypes에 정의한 name이 우선 적용된다.
// 1. TEXT로 정의
@JsonSubTypes.Type(value = TextMessage.class, name = "TEXT") 

//  text로 정의
@JsonTypeName(value = "text")
public class TextMessage extends Message { .. }

이렇게 @JsonTypeInfo와 @JsonSubTypes를 설정한 다음 실행해보자.

String json = "{\"messageId\":1,\"type\":\"TEXT\",\"message\":\"텍스트\"}";
Message message = mapper.readValue(json, Message.class);
System.out.println(message); // com.example.jsontypeinfo.message.TextMessage@704921a5

잘 실행되는 것을 확인할 수 있다.

type이 여러 값(TEXT, text)일 경우 TextMessage에 매핑되게 하려면 어떻게 하면 될까?

name 대신 names로 배열을 넘기면 된다.

@JsonSubTypes.Type(value = TextMessage.class, names = {"TEXT", "text"})

typeTEXT이거나 text일 경우 TextMessage로 매핑한다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유