GSON - Composite pattern class

2023. 1. 24. 22:57Development

https://wenys.tistory.com/11

GSON

https://github.com/google/gson

Google에서 내어놓은 open source library이다, 용도는 간단합니다.

java class object <-> json

json은 워낙 유명한 데이터 저장 포멧이므로 특별히 설명은 하지 않으려고 합니다.
java에서는 기본적으로 serialize(직렬화)라는 기능이 제공되어집니다. 이 기능은 java object를 dump를 뜨는 것 처럼 그대로 byte 포멧으로 바꾸어주고, 다시 역직렬화도 가능합니다.
다만 여러 제약사항이 존재하는 데, deserialize로만 복원이 가능하며 class가 가진 맴버 변수들의 변화에 민감해서 장기 저장용이나 serialize하는 곳과 deserialize하는 곳이 분리되어서 별도로 운영되어 진다면 이를 맞추어 관리하는 것도 상당히 불편합니다.

그래서 현재 개인적으로 진행하는 작은 프로젝트에서 특정 class 정보를 간단히 저장하고, 프로그램 재시작시 다시 class 형태로 복원을 하고자 하는 데, 아래와 같은 이유로 gson을 채택하기로 결정했습니다.

  1. 저장 포멧이 text의 json 타입이므로 어디로든 Persistent로의 저장이 간편하다.
  2. json 타입이므로 혹여 java가 아닌 다른 언어로도 해당 정보를 복원할 수 있다.
  3. Serialize 처럼 class의 instance 자체를 통으로 저장하고, 복원할 수 있어서 간편하다.

저장을 하고자하는 class는 아래에서 Composite pattern을 이용해서 만들었던 Rule set입니다.

[Design pattern - Composite pattern]

[아래부터 진행될 내용은 위 posting의 연장선상에서 진행되므로, 사용할 class의 구조와 이해도를 위해서 한번 읽어주시기를 권장드립니다.]

Library import

해당 프로젝트는 우선 Android에서 사용되어질 프로젝트라서 Gradle build를 사용 중이므로 dependancy를 추가해줍니다.

dependencies {
    implementation 'com.google.code.gson:gson:2.8.6'
}

Serialize

기본적으로 recursive object 참조가 없다면 encoding은 간단합니다.
먼저 앞선 posting에서 만들어둔 class들을 조합하여 object하나를 생성해봅니다.
Ruleset을 만들어줄 function 입니다.

static IRule getRuleSet1() {
    RuleGroup mainRule = new RuleGroup();
    mainRule.setAndConnection(true);

    RuleGroup subRule = new RuleGroup();
    subRule.setAndConnection(false);
    RuleMsg msg1Rule = new RuleMsg();
    msg1Rule.setMsg("삼성카드");
    msg1Rule.setType(RuleMsg.CHECK_TYPE.CONTAIN);
    subRule.addRule(msg1Rule);
    RuleMsg msg2Rule = new RuleMsg();
    msg2Rule.setType(RuleMsg.CHECK_TYPE.CONTAIN);
    msg2Rule.setMsg("삼성 카드");
    subRule.addRule(msg2Rule);

    mainRule.addRule(subRule);

    RuleMsg msg3Rule = new RuleMsg();
    msg3Rule.setMsg("승인");
    msg3Rule.setType(RuleMsg.CHECK_TYPE.CONTAIN);
    mainRule.addRule(msg3Rule);

    return mainRule;
}

return 되는 mainRule은 간략히 아래와 같은 구조를 갖게 됩니다.

MainRule: RuleGroup
    - isAndCondition : bool
    - rules : IRule[]
        - subRule : RuleGroup
            - isAndCondition : bool
            - rules : IRule[]
                - msg1Rule : MsgRule
                    - text : String
                    - check_type : enum
                - msg2Rule : MsgRule
                    - text : String
                    - check_type : enum
        - msg3Rule : MsgRule
            - text : String
            - check_type : enum

gson으로 Serialize해보겠습니다.

Gson engson = new GsonBuilder().create();
IRule ruleSet1 = RuleSetGenerator.getRuleSet1();
String text = engson.toJson(ruleSet1, RuleGroup.class);
System.out.println(text);

출력되는 text를 json 포멧으로 좀 보기좋게 나열하면 아래와 같습니다.

{ 
    "type":"type_group",
    "rules":[ 
        { 
            "type":"type_group",
            "rules":[ 
                {   // 1번
                    "type":"type_msg",
                    "check_type":"CONTAIN",
                    "text":"삼성카드"
                },  // 2번
                {
                    "type":"type_msg",
                    "check_type":"CONTAIN",
                    "text":"삼성 카드"
                }
            ],
            "isAndConnection":false
        },
        {
            "type":"type_msg",
            "check_type":"CONTAIN",
            "text":"승인"
        }
    ],
    "isAndConnection":true
}

아주 간단하게 key값은 variable name이 되고 value는 해당 값으로 ... json 포멧으로 변형이 되었다. 아주 심플하고 쓰기 좋군요! 짝짝!!

Deserialize

자 이제 이를 반대로 deserialize해보겠습니다.

engson.fromJson(text, IRule.class);

java.lang.RuntimeException: Unable to invoke no-args constructor for interface com.wen-ys.msgsecretary.job.IRule. Registering an InstanceCreator with Gson for this type may fix this problem.

아래와 같은 에러가 발생합니다. 어찌보면 당연한 현상일 수 있습니다.

Serialize하는 단계에서는 RuleGroup이 갖고있는 member variable의 list안에 instance들이 IRule이라는 interface로 추상화 되어있긴 하지만, 실제 implements된 class들의 instance들 입니다. 그러므로 Memory에 올라와 있는 값들을 class member meta 정보와 함께 json 포멧으로 만들기만 하면 됩니다. 하지만 반대의 경우 Deserialize 단계에서 IRule에 매핑을 하려고 하면 1, 2번들이 어떠한 class로 매핑되어야할 지에 대한 정보가 없습니다. 이렇게 Polymorphism을 이용해서 구현되어진 class들은 모두 유사한 문제에 당면할 수 밖에 없는 것 입니다.

이 상황을 해결하기 위해서 의도적으로 만들어둔게 IRule interface를 구현한 class들이 type(class 소스는 위 posting을 참조하자)을 갖도록 했습니다. 이 값을 이용해서 어떠한 class로 복원할지를 결정할 수 있도록 할 것입니다.

gson의 경우 custom type의 deserialize 기능도 제공이 됩니다. 물론 반대도 지원하지만 현 상황에서는 굳이 필요는 없습니다.


RuleDeserializer.java

public class RuleDeserializer implements JsonDeserializer<IRule> {

    private Gson gson = new GsonBuilder().registerTypeAdapter(IRule.class, this).create();

    private Map<String, Class<? extends IRule>> ruleTypeClass = new HashMap<String, Class<? extends IRule>>(){
        {
            put(IRule.TYPE_MSG, RuleMsg.class);
            put(IRule.TYPE_SENDER, RuleSender.class);
            put(IRule.TYPE_GROUP, RuleGroup.class);
        }
    };

    @Override
    public IRule deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        JsonObject ruleObject = json.getAsJsonObject();
        JsonElement eType = ruleObject.get("type");
        Class<? extends IRule> cType = ruleTypeClass.get(eType.getAsString());
        return gson.fromJson(json, cType);
    }
}

구현 해본 CustomDeserializer 입니다.

하나하나 뜯어서 살펴보면...

private Gson gson = new GsonBuilder().registerTypeAdapter(IRule.class, this).create();

먼저 해당 CustomDeserializer는 자체 gson object를 갖고 있습니다.
이를 이용해서 내부 deserialize를 시도하게 됩니다. 처음 위에서 serialize 할 때의 gson object 와의 차이점을 보면 registerTypeAdapter를 이용해서 custom adapter를 설정해주었습니다. IRule.class를 deserialze할 경우 'this' 본 클래스를 사용하라는 의미입니다. 이는 제일 밑에서 한번 더 설명하도록 하겠습니다.

    private Map<String, Class<? extends IRule>> ruleTypeClass = new HashMap<String, Class<? extends IRule>>(){
        {
            put(IRule.TYPE_MSG, RuleMsg.class);
            put(IRule.TYPE_SENDER, RuleSender.class);
            put(IRule.TYPE_GROUP, RuleGroup.class);
        }
    };

기본 member 변수로 Map을 만들어서 각 Rule type별로 어떠한 class를 사용하여 deserialize를 할지를 매핑 해두었습니다.

    @Override
    public IRule deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        JsonObject ruleObject = json.getAsJsonObject();
        JsonElement eType = ruleObject.get("type");
        Class<? extends IRule> cType = ruleTypeClass.get(eType.getAsString());
        return gson.fromJson(json, cType);
    }

마지막으로 해당 interface인 JsonDeserializer를 implements하기 위해 구현한 method입니다. 파라메터로 전달되는 object에서 type 값을 가져온 다음, type에 매핑되는 class를 Map에서 확인한 후, 해당 class로 deserialize를 하는 과정입니다.

좀 복잡한 개념이 될 수도 있는 데, deserialize 하는 type이 만약 RuleGroup이 될 경우 이 RuleGroup은 내부에 member로 IRule list를 갖고 있기 때문에 여기서 사용되는 gson또한 IRule을 Deserialize할 수 있는 custom adapter를 알고 있어야 합니다.
그래서 초기에 member변수로 gson을 생성시 this를 adapter로 연결해주는 것 입니다.

그럼 동작을 시켜보겠습니다.

Gson engson = new GsonBuilder().create();
IRule ruleSet1 = RuleSetGenerator.getRuleSet1();
String text = engson.toJson(ruleSet1, RuleGroup.class);
System.out.println(text);

Gson degson = new GsonBuilder().registerTypeAdapter(IRule.class, new RuleDeserializer()).create();
IRule result = degson.fromJson(text, IRule.class);

이제 IRuled을 deserialize 할 수 있는 gson을 새로 생성하겠습니다. customAdapter로 앞서 만들어둔 class를 파라메터로 전달하면.... 'new RuleDeserializer()'.

fromJson method가 이제는 Error를 발생시키지 않고 정상적으로 동작함을 확인할 수 있습니다.

Error는 발생하지 않지만, 실제로 정상적으로 Deserialize가 성공했는 지 확인해보기 위해, 앞서 posting에서 만들어둔 Rule check 코드를 재사용 해보겠습니다.

        Gson engson = new GsonBuilder().create();
        IRule ruleSet1 = RuleSetGenerator.getRuleSet1();
        System.out.println(ruleSet1.check("", "삼성카드 승인"));
        System.out.println(ruleSet1.check("", "삼성 카드 승인"));
        System.out.println(ruleSet1.check("", "삼성 카드 수락"));
        System.out.println(ruleSet1.check("", "삼성카드 취소"));
        System.out.println(ruleSet1.check("", "삼성 카드 취소"));
        System.out.println(ruleSet1.check("", "현대 카드 승인"));
        System.out.println(ruleSet1.check("", "카드취소"));

        String text = engson.toJson(ruleSet1, RuleGroup.class);
        Gson degson = new GsonBuilder().registerTypeAdapter(IRule.class, new RuleDeserializer()).create();

        IRule result = degson.fromJson(text, IRule.class);

        System.out.println(result.check("", "삼성카드 승인"));
        System.out.println(result.check("", "삼성 카드 승인"));
        System.out.println(result.check("", "삼성 카드 수락"));
        System.out.println(result.check("", "삼성카드 취소"));
        System.out.println(result.check("", "삼성 카드 취소"));
        System.out.println(result.check("", "현대 카드 승인"));
        System.out.println(result.check("", "카드취소"));
true
true
false
false
false
false
false

true
true
false
false
false
false
false

serialize 전, 후의 결과값과 같이 동일함을 알 수 있습니다.
이제 Composite pattern으로 만들어둔 Rule set object를 text 타입의 json 포멧으로 serialize 후 다시 deserialize를 거쳐 정상적으로 동일 object로 복원하는 과정을 완성 하였습니다.

+ GSON의 내부동작

추가적으로 재미있는 테스트를 해보겠습니다.

static IRule getRuleSet3() {
    RuleGroup mainRule = new RuleGroup();
    mainRule.setAndConnection(true);

    RuleSender senderRule = new RuleSender();
    senderRule.setSender("01012345678");

    mainRule.addRule(senderRule);
    mainRule.addRule(senderRule);
    mainRule.addRule(senderRule);

    senderRule.setSender("01087654321");

    return mainRule;
}

사실 이렇게 사용하는 경우는 없을 태지만 GSON 내부 동작을 보기위한 테스트 입니다.

하나의 instance를 여러번 Ruleset array에 넣었습니다.
이렇게 하면 하나의 instance인 RuleSender가 RuleGroup.rules Array안에서 여러번 참조되는 형태가 됩니다.

{ 
    "type":"type_group",
    "rules":[ 
        { 
            "type":"type_sender",
            "match":"01087654321"
        },
        { 
            "type":"type_sender",
            "match":"01087654321"
        },
        { 
            "type":"type_sender",
            "match":"01087654321"
        }
    ],
    "isAndConnection":true
}

이 Ruleset 보면 마지막에 한번 sender 값만 바꾸었지만, 하나의 instance이기 때문에 모두 변한 것을 볼 수 있습니다. 이 Rule을 Serialize -> Deserialize를 거치면 어떻게 될까요?

IRule test = degson.fromJson(text, IRule.class);
for(IRule r : ((RuleGroup)test).getRules()) {
    System.out.println(r.hashCode());
}

---
1654589030
466002798
33524623

결과는 각 Sender가 각각의 개체가 됩니다.
각 Object의 hashCode를 출력해보면 다른 값이 나옴을 확인할 수 있습니다.

이러한 특성 때문에 복잡한 class object 안에서 공유되는 object들이 있는 instance를 Serialize, Deserialize할 경우 기대하는 형태로 복원이 안될 수 있기 때문의 주의하여야 합니다.

위와 같은 몇가지 제약사항은 존재하지만, 분명 GSON은 지원하는 범위내에서의 class object를 저장 및 복원하는 용도로 쓰기에 매력적인 library임에는 틀림이 없다고 개인적으로 생각합니다.