Notice
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

jackson-databind Polymorphic Serialization / Deserialization (1) - 2-Depth 본문

Java/References

jackson-databind Polymorphic Serialization / Deserialization (1) - 2-Depth

defacto standard 2019. 3. 10. 18:39


 - Summary

자바에서 직렬화(Serialization)란, Object를 네트워크나 스트림으로 저장할 수 있는 String 으로 변환하는 것을 의미한다.


이러한 스트림통신에서는 객체 자체를 전송할 방법이 없다.



웹에서 이미지를 보여주는 것을 예로 들어보자.


이미지가 포함된 페이지를 클라이언트가 HTTP Request를 보내면,

웹서버에서는 <img src="url"> 등과 같이 String형태로 HTTP Response에 담아서 보낸다.


실제로, HTTP 헤더나 바디를 브라우저의 개발자 도구를 사용해서 까보면 전부 스트링 형태로 되어있다.


브라우저에서는 해당 HTML 코드를 해석하여 src에 해당하는 url에 존재하는 이미지를 바이트코드로 읽어들인 후,

브라우저가 그림으로 보여주는 것이지, 이미지 자체 정보를 받는 것이 아니다.


이미지라는 것의 정보도 결국 바이트 코드로서 존재한다.


즉, 통신에서 오브젝트 자체를 보내는 것은 불가능하다.



이렇게 서로 다른 영역에서 데이터 통신을 하는 경우 객체를 반드시 문자열 형태로 변환해야 전송할 수 있는데,

이것을 Serialization이라고 한다.




자바에서 역직렬화(Deserialization)이란, String으로 넘어온 값을 Object으로 변환하는 것을 의미한다.


SpringFramework에서는 MessageConverter 등을 사용해서 클라이언트에서 보낸 Form Request에 담긴 여러 파라미터를 객체로 바인딩해서,

@RequestParam 어노테이션의 무분별한 남발을 방지해주는 용도 등으로 사용한다.


이는 특정 문자열 프로토콜을 객체로 변환해주는 작업이고, 이것을 Deserialization이라고 한다.




Serialization와 Deserialization에는 여러가지 방법을 사용할 수 있다.


각각 용도와 포맷은 다르지만, 대충 어떤 방법들이 있는지 많이 쓰이는 것들 위주로 알아보겠다.



 - Various Serialization

1) Serializable 인터페이스


2) toString()


3) jackson-databind의 ObjectMapper

 - https://defacto-standard.tistory.com/419


4) Spring의 MessageConverter (@ResponseBody, @RestController)

 - https://defacto-standard.tistory.com/418


5) net.sf.json

 - https://defacto-standard.tistory.com/420


6) 기타 등등



 - Various Deserialization

1) Enum 활용


2) Factory Method Pattern / Abstract Factory Pattern


3) jackson-databind의 ObjectMapper


4) Spring의 MessageConverter (@RequestBody, @RequestParam)

 - https://defacto-standard.tistory.com/383

 - https://defacto-standard.tistory.com/416

 - https://defacto-standard.tistory.com/417

5) 기타 등등




 - Why jackson-databind ?

 json - object 간 매핑을 하는데 주로 사용되는 jackson-databind를 활용한 Serialization/Deserialization을 사용해보겠다.


최대한 객체지향적인 코드를 짜보겠다고 Enum이나 Factory Method 등 디자인 패턴을 써보려고 했지만,

여러 깊이의 다형성이 적용되어야 하는 경우에는 주니어 개발자의 실력으로 이게 쉽지 않았다.

(불가능 한 것은 아니지만 소스가 굉장히 복잡해졌고 객체지향적으로 짜기 생각보다 어려웠다. 짜고보니 별로 객체지향적인 것 같지도 않다.)


그냥 인터페이스(혹은 추상클래스) 하나만 상속받으면 쉽지만, 서브 클래스가 또 서브 클래스를 가지는 구조가 되기 때문이다.

상속을 하지 않고 시도도 해봤지만 결국 실패했다.



또한 Enum만을 사용할 경우,

시스템 이라는 것이 최상위 시스템 프로토콜을 의미하는 것인지, 서브 프로토콜 타입의 시스템을 의미하는 것인지 정의하기 애매했고,

서브 프로토콜 타입마다 Enum을 정의해서 클래스 파일이 많아지고 점점 복잡하게 얽히는 문제도 있었다.


서브 프로토콜의 타입을 전부 한 Enum으로 넣는 미친짓은 하기 싫었다.

구분하기도 힘들고, 이름이 겹치는 경우 네이밍을 다르게 하며, 이를 어떤 상위 프로토콜에서 사용해야 하는지 관례를 정해야 하므로

객체지향적인 코드가 나오지 않기 때문이다.


(단, 나는 아직 객체지향적인 개념이 부족한 주니어 개발자라 어려운 것일 수도 있다.)



그리고 스프링의 MessageConverter를 사용해볼까도 싶었지만, 뭔가 배보다 배꼽이 더 커지는 느낌이었다.

아직 여기까지는 내공이 안돼서 우선 포기했다...  나중에 기회가 되면 해봐야겠다.



그래서 결론적으로 jackson-databind를 사용해서 String을 Object로 Deserialization하고, Object를 String으로 Serialization하기로 했다.


검증할 내용은 다음과 같다.


 - Serialize 검증

상위 클래스로 서브 클래스를 받은 후 ObjectMapper로 JSON을 출력한다.

서브 클래스의 필드의 출력이 제대로 이루어지는지 확인한다.

databind-jackson이 어떻게 json 포맷을 결정하는지 알아본다.



 - Deserialize 검증

Json 포맷의 스트링을 ObjectMapper로 특정 클래스로 변환시킨다.

특정 클래스의 결정은 Polymorphic Mapping 정보를 담고있는 어노테이션을 포함하는 클래스를 사용한다.

Deserialize가 끝나면, 상위 클래스에서 정의하고 서브 클래스가 구현한 메서드를 실행해봄으로써

정확하게 Polymorphic Deserialize가 이루어졌는지 확인한다.




 - Example


우선 Protocol 인터페이스를 두자. 이 인터페이스는 execute()라는 메서드를 정의한다.


프로토콜에는 여러 개의 종류가 있을 수 있고, 이를 A, B, C 클래스로 정의했다.

A, B, C 프로토콜은 Protocol의 일종이므로 Protocol을 상속받아 메서드를 구현한다. 


현재는 2depth Polymorphic을 구현하고 있고, 일반적으로 추상클래스보단 인터페이스로 추상화하는 것이 더 좋으므로 인터페이스로 결정했다.



 * 1depth Serialize / Deserialize

1depth의 경우는 그냥 클래스 하나를 Serialize/Deserialize하는 것이다.

Serialize할 때는 Serializable 인터페이스를 사용하거나, jackson-databind를 사용하는 경우 특정 클래스 정보를 넘겨주게 되므로 크게 어려울 것이 없을 것이다. 1depth Deserialize 역시 크게 어렵지 않다.


* multi-level Polymorphic Serialize / Deserialize

3depth Polymorphic에서는 추상클래스를 사용하고, 특별히 다를 것은 없으나 처음 구현하는 입장에서는 여러 런타임 에러를 만날 수 있다.

3depth Polymorphic과 관련된 글을 보면 개략적인 개념을 잡고 n-depth 까지 응용할 수 있을 것이다.




여기서 하고싶은 것은 다음과 같다.


Serialize의 경우는 new 키워드를 사용한 객체의 인스턴스 자체는 구체적인 서브 클래스로 만들지만,

Protocol 타입의 변수로 받아서 이를 Serialize하는 것이다.


Protocol은 인터페이스이므로 아무런 필드나 메서드 구현이 없으나,

이를 상속하는 서브 클래스에는 필드와 메서드가 존재하므로 다형성을 사용하여 Serialize하는 것이다.



Polymorphic Serialize / Deserialize는 이를 수행하는 클래스 입장에서 구체적인 클래스의 정보는 최소화되는 것이 바람직하다.

즉, 이를 변환하는 클래스에서는 Protocol 인터페이스로만 객체를 다루지, 서브 클래스의 정보는 거의 없어야 한다.


서브클래스로 직접적으로 다룬다면 애초에 Polymorphic이 아니다.


모든 n-depth에서 Serialize에서는 서브 클래스 정보가 아예 등장하지 않는다.


하지만 3depth 이상의 Deserialize에서는 어느 정도 서브클래스의 이름이 노출되어야 한다.

이는 jackson-databind에서 지원하지 않기 때문인데, 이 경우 CustomDeserializer를 구현하거나 Enum을 활용해서 구현해보겠다.



Deserialize의 경우는 제공된 json 문자열을, 상위 클래스와 서브클래스의 매핑 정보에 따라 서브 클래스를 결정한 후 해당 서브 클래스의 인스턴스를 만들고 json 문자열의 내용을 집어넣는 것을 말한다.


이 경우에는 서브클래스가 아예 등장하지 않을 수는 없다.

하지만 서브클래스가 등장하는 것은 Deserialize에서 상위클래스와 서브클래스의 매핑 관계를 설정하는 Enum이나 어노테이션에만 등장하게 될 것이다.


프로젝트 구조는 다음과 같다.


여기서 complex 패키지는 3-depth Polymorphic Serialize / Deserialize에 사용되는 패키지이므로 무시하도록 하자.


2-depth Polymorphic Serialize / Deserialize에서는 simple 패키지를 사용한다. 참고로 message 패키지는 공통으로 사용된다.


simple 패키지에서 사용되는 클래스는 다음과 같다.


 - Message

package message;

public class Message {
private String message;

public void setMessage(String message) {
this.message = message;
}

public String getMessage() {
return this.message;
}
}

json 포맷의 스트링을 저장하는 클래스이다.

Serialize에서는 Protocol 객체를 사용해 json 포맷의 스트링을 얻으면 이 Message 객체에 저장할 것이다.

Deserialize에서는 Message 객체로부터 json 포맷의 스트링을 얻어서 Protocol 객체에 매핑하여 인스턴스를 만들 것이다.



 - Protocol

package simple.Protocol;

public interface Protocol {
void execute();
}

Protocol이라는 상위 개념의 인터페이스다.

execute()라는 메서드를 선언했다.

이 execute()는 Serialize에서는 사용되지 않으며, Deserialize에서 검증 시 서브 클래스마다 구현한 로직이 실행되는 Polymorphic Deserialize를 검증하기 위해 선언했다.



 - AProtocol, BProtocol ( CProtocol 생략 )

package simple.Protocol;

public class AProtocol implements Protocol {
private String aProtocolField1;
private String aProtocolField2;

public AProtocol(String aProtocolField1, String aProtocolField2) {
this.aProtocolField1 = aProtocolField1;
this.aProtocolField2 = aProtocolField2;
}

@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
}
}

package simple.Protocol;

public class BProtocol implements Protocol {
private String bProtocolField1;
private String bProtocolField2;
private String bProtocolField3;

public BProtocol(String bProtocolField1, String bProtocolField2, String bProtocolField3) {
this.bProtocolField1 = bProtocolField1;
this.bProtocolField2 = bProtocolField2;
this.bProtocolField3 = bProtocolField3;
}

@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
}
}

A/B/C Protocol이다. 

Protocol 인터페이스를 상속하므로 execute() 메서드를 구현한다.

this.getClass().getSimpleName()은 각 서브 클래스의 이름을 출력하는 함수로, java.lang.Object에 구현되어 있다.


A/B/C 모두 필드명이 다르고, 필드의 개수도 다르다.



 - MessageProtocolConverter

MessageProtocolConverter는

Protocol 객체를 받아서 Message 객체로 변환해주는 역할을 한다. 이 과정 중 Protocol 객체를 json string 으로 serialize하는 로직이 등장한다.

Message객체로부터 json string을 받아서 Protocol 객체로 변환시켜주는 Deserialize 기능을 한다.


MessageProtocolConverter는 Serialize, Deserialize Solution 코드를 만들면서 살펴보겠다.



 - Main

Serialize, Deserialize를 실제로 수행해보고 결과를 확인할 Entry Point이다.

역시 Solution 코드를 보면서 살펴보자.



 - Serialize Example


우선 Serialize를 먼저 살펴보자.


jackson-databind에서 객체를 Serialize할 때는


ObjectMapper.writeValueAsString()을 사용한다. 이 메서드는 객체에 존재하는 모든 필드들을 json 포맷으로 출력한다.


이를 기반으로 MessageProtocolConverter를 작성해보자.

package simple;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import message.Message;
import simple.Protocol.Protocol;

import java.io.IOException;

class MessageProtocolConverter {
private ObjectMapper objectMapper;

MessageProtocolConverter() {
objectMapper = new ObjectMapper();
}

/** Serialize **/
Message protocolSerialize(Protocol protocol) {
Message message = new Message();

try {
String json = objectMapper.writeValueAsString(protocol);
message.setMessage(json);
} catch (JsonProcessingException e) {
System.out.println("Protocol to Message Serialization Failure");
}

return message;
}
}


protocolSerialize(Protocol protocol) 메서드는 Protocol 인터페이스 타입으로 객체를 받아서,

ObjectMapper.writeValueAsString()에 Serialize 로직을 위임한다.


Serialize의 결과를 Message객체에 담아서 리턴한다. Message.getMessage()를 통해 결과 출력하면 Serialize의 결과를 볼 수 있을 것이다.



Main 함수를 작성하고 실행 결과를 보자.

package simple;

import message.Message;
import simple.Protocol.AProtocol;
import simple.Protocol.Protocol;

public class Main {
public static void main(String[] args) {
MessageProtocolConverter messageProtocolConverter = new MessageProtocolConverter();

Protocol protocol = new AProtocol("1", "2");
Message message = messageProtocolConverter.protocolSerialize(protocol);
System.out.println(message.getMessage());
}
}


AProtocol의 객체를 만들고, 생성자에서 필드를 주입한다.

단, 이를 받는 타입은 AProtocol 이라는 구체적 클래스 정보가 아닌 Protocol 이라는 추상적인 인터페이스로 받는다.


그리고 MessageProtocolConverter 객체를 만들어서 protocolSerialize() 메서드의 인자로 넘겨주자.


MessageProtocolConverter.protocolSerialize()에서는 내부적으로 ObjectMapper.writeValueAsString()을 사용해서 Protocol 오브젝트를 Serialize하므로

결론적으로는 ObjectMapper를 쓴다고 할 수 있다.


결과는 다음과 같다.

-------------------

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class simple.Protocol.AProtocol and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)

at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1191)

at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:313)

at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.failForEmpty(UnknownSerializer.java:71)

at com.fasterxml.jackson.databind.ser.impl.UnknownSerializer.serialize(UnknownSerializer.java:33)

at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider._serialize(DefaultSerializerProvider.java:480)

at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:319)

at com.fasterxml.jackson.databind.ObjectMapper._configAndWriteValue(ObjectMapper.java:3905)

at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3219)

at simple.MessageProtocolConverter.protocolSerialize(MessageProtocolConverter.java:22)

at simple.Main.main(Main.java:12)

Protocol to Message Serialization Failure

null

-------------------


스택 트레이스를 읽어보니 MessageProtocolConverter 내부에서 ObjectMapper.writeValueAsString() 메서드에서 오류가 났다.

이 부분의 코드는 다음과 같다.

String json = objectMapper.writeValueAsString(protocol);


위 에러는 Protocol 이라는 인터페이스를 바탕으로 Serialize를 시도했는데, AProtocol이라는 클래스의 정보를 찾을 수 없다는 것이다.


해결하려면 SerializationFeature.FAIL_ON_EMPTY_BEANS 를 disable하라는데 우선 EMPTY_BEANS라는 것부터 좀 이상하다.

분명 AProtocol 클래스를 만들었고, 생성자도 있으며 필드에 값까지 넣어줬는데 뭔가 이런 옵션을 건들기는 불안하다.



사실 Polymorphic Serialize / Deserialize는 마법이 아니다.

이 말은, 어느정도 클래스 매핑에 대한 정보를 기술을 해줘야만 해당 내용을 바탕으로 서브 클래스를 찾아서 매핑한다는 뜻이다.


이를 위해서는 어떤 서브 클래스가 매핑되어 있는지,

서브 클래스를 찾았다면 json 결과에서 어떤 필드로 출력할 것인지 등을 기술해줘야 한다.


ObjectMapper에 넘기는 클래스의 정보는 Protocol이므로, Protocol에 해당 내용을 기술해줘야 한다.


다음은 수정된 Protocol 인터페이스다.

package simple.Protocol;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = AProtocol.class, name = "A"),
@JsonSubTypes.Type(value = BProtocol.class, name = "B"),
@JsonSubTypes.Type(value = CProtocol.class, name = "C")
})
public interface Protocol {
void execute();
}

뭔가 복잡해졌지만, 가만히 들여다보면 한 눈에 알 수 있는 정보들이 있다.


위 어노테이션은 Protocol이라는 상위 인터페이스에 매치되는 A,B,C Protocol을 Serialize / Deserialize 시 매핑 정보를 준 것이다.


그 외에도 결과를 어떤 식으로 출력할지에 대한 전략 역시도 설정할 수 있는데 (클래스 네임 기반 혹은 name 지정 기반 전략 등)

자세한 내용은 jackson-databind 레퍼런스를 참고하도록 하자.


클래스 네임을 기반으로 Serialize하면 뭔가 어색하다.

이보다는 키 값을 "type"이라는 키 값으로 커스터마이징해서

서브클래스에 따른 serialize 결과를 구분하는 "type" 프로퍼티를 출력하는 name 지정 기반 전략을 사용하겠다.


위 어노테이션을 해석하면 다음과 같다.

1. Protocol 인터페이스는 JsonSerialize, Deserialize 시 A/B/CProtocol 과 매핑된다.

2. 각 서브타입에 따라서 "type" 키에 출력하는 값이 다르다.

3. AProtocol의 경우 "type" 키의 값은 'A', BProtocol의 경우 "type" 키의 값은 'B', CProtocol의 경우 "type" 키의 값은 'C'가 된다.


이러한 매핑 메타 정보는 굳이 Protocol 클래스에 작성하지 않아도,

ObjectMapper.wirteValueAsString()을 사용하기 전에 ObjectMapper.addMixin() 메서드를 통해

해당 메타정보가 포함된 매핑용 클래스를 따로 작성하여 제공해도 된다.


근데 복잡해질 것 같고 간단한 예제이므로 그냥 Protocol에 정의했다.

만약 Protocol 인터페이스가 아니라 ProtocolMixin 이라는 클래스를 정의하고,

위 애노테이션을 기술한다면 Protocol 인터페이스에 어노테이션을 모두 ProtocolMixin으로 옮겨도 제대로 동작한다.


단, ObjectMapper.addMixin(Protocol.class, ProtocolMixin.class)와 같이

Serialize / Deserialize전에 매핑 정보를 포함하는 클래스 정보를 줘야 한다.


json 결과의 'type' 필드는 A/B/CProtocol에 따라 다르게 출력되게 하고 싶으며,

그 외의 다른 필드들은 각 서브 Protocol의 필드의 값을 출력하고 싶다.


예를 들면

AProtocol

{"type" : "A", "aProtocolKey1" : "avalue1", "aProtocolKey2" : "avalue2"}


BProtocol

{"type" : "B", "bProtocolKey1" : "bvalue1", "bProtocolKey2" : "bvalue2", "bProtocolKey3" : "bvalue3"}


CProtocol

{"type" : "C", "cProtocolKey1" : "cvalue1", "cProtocolKey2" : "cvalue2", "cProtocolKey3" : "cvalue3", "cProtocolKey4" : "cvalue4"}


등과 같이 서브 클래스에 따라 다르게 동적으로 Serialize 한다는 말이다.


@JsonTypeInfo의 property는 각 서브 클래스의 구분을 하는 키 값을 json 결과에 포함시키며, 키값을 "type"으로 한다는 의미이다.

여기서 "type" 키의 값은 @JsonSubType 어노테이션에 지정된 name 프로퍼티의 값이 된다.



이 차이를 볼 수 있도록 Main 함수를 변경해보자.

package simple;

import message.Message;
import simple.Protocol.AProtocol;
import simple.Protocol.BProtocol;
import simple.Protocol.CProtocol;
import simple.Protocol.Protocol;

public class Main {
public static void main(String[] args) {
MessageProtocolConverter messageProtocolConverter = new MessageProtocolConverter();

Protocol protocolA = new AProtocol("1", "2");
Message messageA = messageProtocolConverter.protocolSerialize(protocolA);
System.out.println(messageA.getMessage());

Protocol protocolB = new BProtocol("1", "2", "3");
Message messageB = messageProtocolConverter.protocolSerialize(protocolB);
System.out.println(messageB.getMessage());

Protocol protocolC = new CProtocol("1", "2", "3", "4");
Message messageC = messageProtocolConverter.protocolSerialize(protocolC);
System.out.println(messageC.getMessage());
}
}


Protocol 인터페이스와 Main 함수가 변경되었으니 출력결과를 살펴보자.


에러는 사라지고, A, B, C Protocol에 따라 다르게 Serialize가 되었다.


그런데 A, B, C가 각각 다르게 Serialize되는 것은 확인했지만, 각 서브 프로토콜의 필드값들이 나타나지 않았다.


jackson-databind는 private 필드의 값들을 출력해주지 않는다.

각 서브 클래스의 필드값 또한 Serialize하려면 2가지 방법이 존재한다. 둘 다 해야하는 것은 아니며, 둘 중 하나라도 만족하면 된다.


둘 다 설정하는 경우, 둘 중 하나의 우선순위에 맞춰져서 출력될 텐데,

필드를 public으로 설정하면서 public setter를 설정하는 사람은 없을테니 굳이 설명하지 않겠다.



1. 필드를 public으로 선언

(이 방법은 딱 봐도 캡슐화가 깨지게 되므로. 사용하지 않는 것이 좋다. 사용하려면 2번-public getter 메서드 방법을 사용하도록 하자)


public 필드를 사용해서 Serialize하는 경우, public 필드의 변수명이 json 결과의 key 값으로 들어가게 된다.

A/B/CProtocol의 필드를 전부 public으로 변경하고 결과를 실행해보자. (CProtocol 생략)

package simple.Protocol;

public class AProtocol implements Protocol {
public String aProtocolField1;
public String aProtocolField2;

public AProtocol(String aProtocolField1, String aProtocolField2) {
this.aProtocolField1 = aProtocolField1;
this.aProtocolField2 = aProtocolField2;
}

@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
}
}

public class BProtocol implements Protocol {
public String bProtocolField1;
public String bProtocolField2;
public String bProtocolField3;

public BProtocol(String bProtocolField1, String bProtocolField2, String bProtocolField3) {
this.bProtocolField1 = bProtocolField1;
this.bProtocolField2 = bProtocolField2;
this.bProtocolField3 = bProtocolField3;
}

@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
}
}


다시 Main을 실행해보자.


이제 어느정도 제대로 Serialize가 되는 것 같다.

AProtocol의 필드명을 바꾸면 해당 필드명이 결과의 key값으로 들어간다.


AProtocol의 첫 번째 필드의 이름을 '티모'로 변경하고 다시 실행해보자.

예를 보여주기 위해 '티모'로 한 것이지, 한글은 프로그래밍 하기에 별로 적절하지 않다.

package simple.Protocol;

public class AProtocol implements Protocol {
public String 티모;
public String aProtocolField2;


public AProtocol(String aProtocolField1, String aProtocolField2) {
this.티모 = aProtocolField1;
this.aProtocolField2 = aProtocolField2;
}

@Override
public void execute() {
System.
out.println(this.getClass().getSimpleName() + ".execute()");
}
}


실행 결과는 다음과 같다.



하지만 이 방법은 클래스의 캡슐화가 깨지게 되므로, 별로 사용을 추천하지 않는다.



2. public getter 메서드 선언

앞에서 봤듯이 jackson-databind는 private 필드를 Serialize하지 않는다.

그렇다고 1번과 같이 public 필드를 선언해서 Serialize하면 결과는 제대로 나오겠지만 캡슐화를 깨트리게 된다.


차라리 다른 방법으로 구현하는 것이 더 나을 정도다.


jackson-databind는 이에 대비해서 public getter의 이름을 기반으로 해서 json 결과를 뽑아주는 기능이 존재한다.

해야 할 것은 그냥 public getter를 구현하기만 하면 알아서 동작한다.


다음은 수정된 AProtocl이다.

package simple.Protocol;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

public class AProtocol implements Protocol {
private String aProtocolField1;
private String aProtocolField2;


public AProtocol(String aProtocolField1, String aProtocolField2) {
this.aProtocolField1 = aProtocolField1;
this.aProtocolField2 = aProtocolField2;
}

// 출력하려는 필드를 private으로 선언했다면, public getter가 있어야 한다.
// 이때, Serializae 시 getXXXX에서 XXXX가 key이며, 필드의 값이 value가 된다.
public String getAField1() {
return aProtocolField1;
}

public String getAField2() {
return aProtocolField2;
}

@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
}
}


필드의 이름은 aProtocolField1로 다시 변경했으며, 접근지정자는 private으로 변경했다.


getter는 getaProtocolField()가 아니라, getAField()로 정의했다.


결과를 살펴보자.



분명 AProtocol의 필드는 aProtocolField1이지만 getter method의 이름이 'getAField' 이므로

key는 getXXX에서 get을 제외한 'afield'가 lowercase로 들어갔으며,

value는 aProtocolField1의 값인 1이 들어간 것을 확인할 수 있다.


그런데 C를 보면 좀 불편하다. cfield1~4 순으로 나오는게 아니라 cfield2,4,1,3 순으로 나오고 있다.


json 출력 순서는 완전히 랜덤으로 바뀐다. 이 순서를 제어하려면 각 서브 클래스에 @JsonPropertyOrder 어노테이션을 활용하면 된다.


이를 적용한 CProtocol은 다음과 같다.

package simple.Protocol;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({ "cfield1", "cfield2", "cfield3", "cfield4" })
public class CProtocol implements Protocol {
private String cProtocolField1;
private String cProtocolField2;
private String cProtocolField3;
private String cProtocolField4;

public CProtocol(String cProtocolField1, String cProtocolField2, String cProtocolField3, String cProtocolField4) {
this.cProtocolField1 = cProtocolField1;
this.cProtocolField2 = cProtocolField2;
this.cProtocolField3 = cProtocolField3;
this.cProtocolField4 = cProtocolField4;
}

public String getCField1() {
return cProtocolField1;
}

public String getCField2() {
return cProtocolField2;
}

public String getCField3() {
return cProtocolField3;
}

public String getCField4() {
return cProtocolField4;
}

@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
}
}


A~C Protocol에 모두 @JsonPropertyOrder를 적용한 후 결과는 다음과 같다.


이렇게 2depth-Polymorphic Serialize 구현이 완료되었다.




 - Deserialize Example

현재 MessageProtocolConverter는 MessageProtocolConverter.protocolSerialize() 기능만을 수행한다.


이 메서드는 protocol이라는 상위 클래스를 받아서,

protocol.class에 기술되어있는 매핑 메타 어노테이션 정보를 기반으로 바인딩된 서브 클래스를 직렬화해서 json 메시지를 뽑고,

이를 Message 객체에 담아서 반환한다.


이제 Deserialize 기능을 구현해보자.


ObjectMapper.readValue()를 사용하면 Deserialize를 구현할 수 있다. 이를 사용해서 MessageProtocolConverter를 수정해보자.

package simple;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import message.Message;
import simple.Protocol.Protocol;

import java.io.IOException;

class MessageProtocolConverter {
private ObjectMapper objectMapper;

MessageProtocolConverter() {
objectMapper = new ObjectMapper();
}

/** Serialize **/
Message protocolSerialize(Protocol protocol) {
Message message = new Message();
try {
String json = objectMapper.writeValueAsString(protocol);
message.setMessage(json);
} catch (JsonProcessingException e) {
e.printStackTrace();
System.out.println("Protocol to Message Serialization Failure");
}

return message;
}

/** Deserialize **/
Protocol messageDeserialize(Message message) {
Protocol protocol = null;

try {
protocol = objectMapper.readValue(message.getMessage(), Protocol.class);
} catch (IOException e) {
e.printStackTrace();
System.out.println("Message to Protocol Deserialization Failure");
}

return protocol;

}
}



이번엔 json 스트링이 들어있는 Message를 받아서 Protocol 객체로 변환해준다.

물론 Protocol 객체는 구체적인 서브 클래스(A~CProtocol)가 바인딩되어 있어야 한다.


ObjectMapper.readValue()는 오버로딩되어 있다.

여기서는 ObjectMapper.readValue(String content, Class<T> valueType) 메서드를 사용했다.


중요한 것은 두 번째 인자인 Protocol.class다.


ObjectMapper 입장에서 Protocol.class 라는 정보만으로 서브 클래스가 어떤 것이 있는지를 알 수 있다.


그 이유는, Serialize를 구현하는 과정 중에서 Protocol 인터페이스에 매핑 메타 어노테이션을 설정했기 때문이다.


Main 메서드를 수정하고 바로 출력해보자. 우선 AProtocol만 Deserialize해보자.


위에서 Serialize가 제대로 수행됐으므로, json string은 AProtocol을 Serialize한 결과인 messageA 객체에서 가져오겠다.


아무래도 상위 Protocol 개념으로 Serialize / Deserialize를 하다보니 A~CProtocol이 정확히 바인딩됐는지 알 수 없다.


따라서 Protocol 인터페이스에 정의된 execute() 메서드를 각 서브 클래스에서 다르게 출력되도록 정의한 후에

이 결과를 각각 확인해보는 방법으로 Deserialize를 검증하겠다.

package simple;

import message.Message;
import simple.Protocol.AProtocol;
import simple.Protocol.BProtocol;
import simple.Protocol.CProtocol;
import simple.Protocol.Protocol;

public class Main {
public static void main(String[] args) {
MessageProtocolConverter messageProtocolConverter = new MessageProtocolConverter();

System.out.println("-------------Serialize-------------");
Protocol protocolA = new AProtocol("1", "2");
Message messageA = messageProtocolConverter.protocolSerialize(protocolA);
System.out.println(messageA.getMessage());

Protocol protocolB = new BProtocol("1", "2", "3");
Message messageB = messageProtocolConverter.protocolSerialize(protocolB);
System.out.println(messageB.getMessage());

Protocol protocolC = new CProtocol("1", "2", "3", "4");
Message messageC = messageProtocolConverter.protocolSerialize(protocolC);
System.out.println(messageC.getMessage());

System.out.println("-------------Deserialize-------------");
Protocol deserializedProtocolA = messageProtocolConverter.messageDeserialize(messageA);
deserializedProtocolA.execute();
}
}


결과는 다음과 같다.

-------------Serialize-------------

{"type":"A","afield1":"1","afield2":"2"}

{"type":"B","bfield1":"1","bfield2":"2","bfield3":"3"}

{"type":"C","cfield1":"1","cfield2":"2","cfield3":"3","cfield4":"4"}

-------------Deserialize-------------

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `simple.Protocol.AProtocol` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)

 at [Source: (String)"{"type":"A","afield1":"1","afield2":"2"}"; line: 1, column: 13]

at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)

at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1452)

at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1028)

at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297)

at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326)

at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:194)

at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:161)

at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:130)

at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:97)

at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:254)

at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:68)

at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)

at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)

at simple.MessageProtocolConverter.messageDeserialize(MessageProtocolConverter.java:36)

at simple.Main.main(Main.java:27)

Message to Protocol Deserialization Failure

Exception in thread "main" java.lang.NullPointerException

at simple.Main.main(Main.java:28)



또 에러가 난다... 스택트레이스를 보니 Serialize는 제대로 된 것 같고,

아무래도 Deserialize쪽에서 ObjectMapper.readValue()를 수행하면서 오류가 난 것 같다.


스택을 추적해보니 다음 문구에 눈이 간다.

Cannot construct instance of `simple.Protocol.AProtocol` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)


우선 AProtocol이란건 눈치를 챈 것 같다. 매핑에는 이상이 없어보인다.


그러나 default constructor가 없다고 한다.



자바에서 클래스를 작성할 때, parameter가 있는 생성자를 생성하면 NoArgsConstructor는 정의되지 않는다.


jackson-databind는 기본 생성자를 바탕으로 동작하나보다.


파라미터를 받는 생성자가 있긴 하지만, 우선 기본 생성자도 오버로딩해보자.


다음은 수정된 AProtocol이다.

package simple.Protocol;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({ "afield1", "afield2" })
public class AProtocol implements Protocol {
private String aProtocolField1;
private String aProtocolField2;

// 반드시 기본 생성자가 있어야 함
public AProtocol() {}

public AProtocol(String aProtocolField1, String aProtocolField2) {
this.aProtocolField1 = aProtocolField1;
this.aProtocolField2 = aProtocolField2;
}

// 출력하려는 필드를 private으로 선언했다면, public getter가 있어야 한다.
// 이때, Serializae 시 getXXXX에서 XXXX가 key이며, 필드의 값이 value가 된다.
public String getAField1() {
return aProtocolField1;
}

public String getAField2() {
return aProtocolField2;
}


@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
}
}


Main을 다시 실행해보자.


분명 ObjectMapper.read() 메서드에 Protocl.class를 사용했는데,

Protocol 인터페이스에는 매핑정보가 있기때문에 제대로 deserialize 타겟을 정할 수 있는 것이다.


또한 Protocol 인터페이스를 통해 MessageProtocolConverter와 서브 프로토콜 클래스(A~CProtocol) 간의 결합도를 줄일 수 있게 되었다.

만약 서브 클래스를 직접적으로 쓴다면 MessageProtocolConverter의 유지보수성은 포기해야한다.


B, C도 동일한 방법을 통해 수정하고, Main에서 검증할 수 있도록 해보자.

package simple;

import message.Message;
import simple.Protocol.AProtocol;
import simple.Protocol.BProtocol;
import simple.Protocol.CProtocol;
import simple.Protocol.Protocol;

public class Main {
public static void main(String[] args) {
MessageProtocolConverter messageProtocolConverter = new MessageProtocolConverter();

System.out.println("-------------Serialize-------------");
Protocol protocolA = new AProtocol("1", "2");
Message messageA = messageProtocolConverter.protocolSerialize(protocolA);
System.out.println(messageA.getMessage());

Protocol protocolB = new BProtocol("1", "2", "3");
Message messageB = messageProtocolConverter.protocolSerialize(protocolB);
System.out.println(messageB.getMessage());

Protocol protocolC = new CProtocol("1", "2", "3", "4");
Message messageC = messageProtocolConverter.protocolSerialize(protocolC);
System.out.println(messageC.getMessage());

System.out.println("-------------Deserialize-------------");
Protocol deserializedProtocolA = messageProtocolConverter.messageDeserialize(messageA);
deserializedProtocolA.execute();

Protocol deserializedProtocolB = messageProtocolConverter.messageDeserialize(messageB);
deserializedProtocolB.execute();

Protocol deserializedProtocolC = messageProtocolConverter.messageDeserialize(messageC);
deserializedProtocolC.execute();
}
}


결과는 다음과 같다.



이로서 Protocol 이라는 상위 객체를 포함해 Polymorphic Deserialize까지 제대로 타겟을 잡을 수 있는 것을 확인했다.



그러나.. 아직 해결되지 않은 문제는 사실 2가지가 더 있다.


1. Constructor가 불필요하게 2개 존재한다.

2. Deserialized Protocol의 프로퍼티 값을 검증하지 않았다.



1번은 우선 제껴두고, 2번부터 검증하자.



Protocol 인터페이스에는 execute()가 있고, 각 서브 클래스마다 다른 값을 출력하고있다.


따라서 각 서브클래스에서 자신이 가진 프로퍼티의 값을 출력해서 확인하면 프로퍼티까지 제대로 Deserialize 됐는지 확인할 수 있을 것 같다.


A~CProtocol의 execute()의 내용을 자신의 프로퍼티 값을 출력하도록 변경해보자. CProtocol은 생략하겠다.

package simple.Protocol;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({ "afield1", "afield2" })
public class AProtocol implements Protocol {
// 필드를 직접 출력하려면 public 필드로 선언해야 한다.
private String aProtocolField1;
private String aProtocolField2;

// 반드시 기본 생성자가 있어야 함
public AProtocol() {}

public AProtocol(String aProtocolField1, String aProtocolField2) {
this.aProtocolField1 = aProtocolField1;
this.aProtocolField2 = aProtocolField2;
}

// 출력하려는 필드를 private으로 선언했다면, public getter가 있어야 한다.
// 이때, Serializae 시 getXXXX에서 XXXX가 key이며, 필드의 값이 value가 된다.
public String getAField1() {
return aProtocolField1;
}

public String getAField2() {
return aProtocolField2;
}


@Override
public void execute() {
System.
out.println(this.getClass().getSimpleName() + ".execute()");
System.out.println("aProtocolField1 = " + aProtocolField1);
System.out.println("aProtocolField2 = " + aProtocolField2);
}
}

package simple.Protocol;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({ "bfield1", "bfield2", "bfield3" })
public class BProtocol implements Protocol {
private String bProtocolField1;
private String bProtocolField2;
private String bProtocolField3;

public BProtocol() {}

public BProtocol(String bProtocolField1, String bProtocolField2, String bProtocolField3) {
this.bProtocolField1 = bProtocolField1;
this.bProtocolField2 = bProtocolField2;
this.bProtocolField3 = bProtocolField3;
}

public String getBField1() {
return this.bProtocolField1;
}

public String getBField2() {
return this.bProtocolField2;
}

public String getBField3() {
return this.bProtocolField3;
}

@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
System.out.println("bProtocolField1 = " + bProtocolField1);
System.out.println("bProtocolField2 = " + bProtocolField2);
System.out.println("bProtocolField3 = " + bProtocolField3);
}
}


이제 출력 결과를 확인해보자.


값은 모두 null이 들어가있었다.

이는 ObjectMapper에서 Protocol에 정의된 어노테이션을 바탕으로

A~CProtocol이라는 서브 클래스를 인식해서 제대로 타게팅 한 것은 성공했지만만, 필드를 넣는데는 실패했다는 말이다.


jackson-databind는 Serialize시에 public getter를 통해 키-밸류를 생성한다는 것이라고 했다.


deserialize 역시 비슷하다. string을 object로 변환할 때 setter를 통해 값을 넣는다.

그래서 인자가 있는 생성자가 있음에도 불구하고 기본생성자를 만들라고 오류가 나는 것이다.


왠지 public 필드를 선언해도 넣어줄 것 같지만, Serialize와 동일한 이유(캡슐화 파괴)로 사용하지 않고

public setter를 사용해서 값을 넣어보자.


Serialize에서와 동일하게 setXXXX 라는 setter 메서드를 만들어야 하는데,

왠지 키 값이 XXXX라고 되어 있으면 setXXXX 로 이름을 맞춰주어야 할 것 같으니 그렇게 해보자.


AProtocol 기준으로 getter는 getAField1~2() 이므로, setAField1~2()라고 정의해보자.


다음은 수정된 AProtocol이다.

package simple.Protocol;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({ "afield1", "afield2" })
public class AProtocol implements Protocol {
// 필드를 직접 출력하려면 public 필드로 선언해야 한다.
private String aProtocolField1;
private String aProtocolField2;

// 반드시 기본 생성자가 있어야 함
public AProtocol() {}

public AProtocol(String aProtocolField1, String aProtocolField2) {
this.aProtocolField1 = aProtocolField1;
this.aProtocolField2 = aProtocolField2;
}

// 출력하려는 필드를 private으로 선언했다면, public getter가 있어야 한다.
// 이때, Serializae 시 getXXXX에서 XXXX가 key이며, 필드의 값이 value가 된다.
public String getAField1() {
return aProtocolField1;
}

public String getAField2() {
return aProtocolField2;
}
// 입력하려는 필드를 private으로 선언했다면, public setter가 있어야 한다.
// 이때, Deserializae 시 "XXXX":"1" 이라는 json node가 있다면 setXXXX를 설정해줘야 제대로 1이라는 값이 필드의 값으로 들어간다.
public void setAField1(String aField1) {
this.aProtocolField1 = aField1;
}

public void setAField2(String aField2) {
this.aProtocolField2 = aField2;
}

@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
System.out.println("aProtocolField1 = " + aProtocolField1);
System.out.println("aProtocolField2 = " + aProtocolField2);
}
}


Main을 다시 수행해보자.


역시 AProtocol 쪽에는 값이 제대로 들어온다. BProtocol, CProtocol도 동일하게 setter를 작성해보고 다시 Main을 실행시켜보자.



이로서 Deserialize까지 정상적으로 되는 것을 알 수 있다.



이제 앞에서 제껴놨던 생성자 문제를 다시 보자.


현재 A~CProtocol에는 2개의 생성자가 오버로딩되어있다.


하나는 NoArgsConstructor다. 이는 jackson-databind에서 Deserialize에 사용된다. (Serialize에는 필요없다)

하나는 AllArgsConstructor다. 이는 Main에서 개발자가 A~CProtocol을 생성할 때 사용된다.


뭔가 일관성이 없고, jackson-databind에서는 자바빈 패턴을 추구하는 것 같다.


일관성을 위해 AllArgsConstructor를 없애고, Main에서도 setter를 사용해서 프로퍼티 값을 세팅하도록하자.


단, Main에서 Protocol이라는 상위 인터페이스로 값을 받고 있으므로

setter를 사용해서 프로퍼티를 넣으려면 setter method chaining 방식으로 넣어줘야 한다.


다음은 모든 Constructor를 제거한 AProtocol이다. (B,CProtocol 생략)

package simple.Protocol;

import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({ "afield1", "afield2" })
public class AProtocol implements Protocol {
// 필드를 직접 출력하려면 public 필드로 선언해야 한다.
private String aProtocolField1;
private String aProtocolField2;

// 출력하려는 필드를 private으로 선언했다면, public getter가 있어야 한다.
// 이때, Serializae 시 getXXXX에서 XXXX가 key이며, 필드의 값이 value가 된다.
public String getAField1() {
return aProtocolField1;
}

public String getAField2() {
return aProtocolField2;
}
// 입력하려는 필드를 private으로 선언했다면, public setter가 있어야 한다.
// 이때, Deserializae 시 "XXXX":"1" 이라는 json node가 있다면 setXXXX를 설정해줘야 제대로 1이라는 값이 필드의 값으로 들어간다.
public AProtocol setAField1(String aField1) {
this.aProtocolField1 = aField1;
return this;
}

public AProtocol setAField2(String aField2) {
this.aProtocolField2 = aField2;
return this;
}

@Override
public void execute() {
System.out.println(this.getClass().getSimpleName() + ".execute()");
System.out.println("aProtocolField1 = " + aProtocolField1);
System.out.println("aProtocolField2 = " + aProtocolField2);
}
}


자바에서는 생성자가 명시적으로 선언되지 않으면 기본적으로 NoArgsConstructor가 제공된다.


따라서 기본생성자만 사용한다고 한다면 굳이 쓰지 않아도 된다.


따라서 NoArgsConstructor, AllArgsConstructor 모두 삭제해도 무방하다.



다음은 변경된 Main이다.

package simple;

import message.Message;
import simple.Protocol.AProtocol;
import simple.Protocol.BProtocol;
import simple.Protocol.CProtocol;
import simple.Protocol.Protocol;

public class Main {
public static void main(String[] args) {
MessageProtocolConverter messageProtocolConverter = new MessageProtocolConverter();

System.out.println("-------------Serialize-------------");
Protocol protocolA = new AProtocol()
.setAField1("1")
.setAField2("2");
Message messageA = messageProtocolConverter.protocolSerialize(protocolA);
System.out.println(messageA.getMessage());

Protocol protocolB = new BProtocol()
.setBField1("1")
.setBField2("2")
.setBField3("3");
Message messageB = messageProtocolConverter.protocolSerialize(protocolB);
System.out.println(messageB.getMessage());

Protocol protocolC = new CProtocol()
.setCField1("1")
.setCField2("2")
.setCField3("3")
.setCField4("4");
Message messageC = messageProtocolConverter.protocolSerialize(protocolC);
System.out.println(messageC.getMessage());

System.out.println("-------------Deserialize-------------");
Protocol deserializedProtocolA = messageProtocolConverter.messageDeserialize(messageA);
deserializedProtocolA.execute();

Protocol deserializedProtocolB = messageProtocolConverter.messageDeserialize(messageB);
deserializedProtocolB.execute();

Protocol deserializedProtocolC = messageProtocolConverter.messageDeserialize(messageC);
deserializedProtocolC.execute();
}
}


setter를 메서드 체이닝 방식으로 변경해서 바로 Protocol로 받을 수 있게 변경하였다.

굳이 이렇게 하지 않고,  다음과 같이 해도 상관은 없다.


중요한 것은 인자를 넘길 때 구체적인 서브 클래스를 넘기는 것이 아니라, Protocol 타입의 인자를 넘기는게 중요한 것이다.

/*Protocol protocolA = new AProtocol()
.setAField1("1")
.setAField2("2");*/
AProtocol protocolA = new AProtocol();
protocolA.setAField1("1");
protocolA.setAField2("2");
Protocol protocol = protocolA;
Message messageA = messageProtocolConverter.protocolSerialize(protocol);


출력 결과는 다음과 같다.


이로써 jackson-databind를 사용해 2-depth hierarchy에 대한 Polymorphic Serialize / Deserialize 구현이 끝났다.


실질적으로 변환을 구현하는 MessageProtocolConverter는 특정 서브 Protocol(A~CProtocol)을 알 필요가 없다.

이후에 DProtocol, EProtocol 등이 추가되는 경우에는 Protocol의 매핑 값만 변경해주면 된다.

MessageProtocolConverter는 수정할 필요가 없다.


다음에는 jackson-databind를 사용해서 3-depth hierarchy에 대한 Polymorphic Serialize / Deserialize를 알아보자.



 - 참고

https://stackoverflow.com/questions/30362446/deserialize-json-with-jackson-into-polymorphic-types-a-complete-example-is-giv

https://www.baeldung.com/jackson-exception

Comments