programming language/Java

[Java] @annotation을 통한 Response 데이터 변경하기 (feat : jackson)

공대키메라 2023. 12. 1. 17:13

일반적으로 많은 회사에서 사용자의 개인 정보를 정책에 따라서 암호화해서 관리하고 있을 것이다.

 

이것을 또한 조회 시에 마스킹 처리도 해야 할 수 있다.

 

그래서 이러한 사항에 대해서 알아보던 중에 정보를 정리해서 어떻게 사용하는지 알아보려고 한다.

 

필자는 복잡한 것은 싫으니 최소한으로 활용할 수 있는 방법을 아~주 간단하게 보려고 한다.

 

그럼 후비고오~

 


 

1. Serialize and Deserialize (직렬화와 역직렬화)

우선 다시 직렬화와 역직렬화에 대해 간단히 알아보려고 한다.

 

직렬화

객체, 데이터 구조 등의 메모리 내 복잡한 구조를 전송 가능한 형태로 변환하는 과정 (예 : JSON, XML, 바이트 스트림 등).

데이터를 파일에 저장하고거나, 네트워크를 통해 다른 시스템으로 전송하기 위해 필요.

 

역직렬화

직렬화된 데이터 형태를 원래의 객체나 데이터 구조로 복원하는 과정.

 

직렬화를 통해 데이터를 표준화된 포맷으로 변환하고, 역직렬화를 통해 이 데이터를 다시 원래의 형태로 복원합니다. 이러한 과정을 통해 다양한 시스템 간의 데이터 호환성과 통신이 가능해지는 것이다.

 

2. Jackson 활용한 간단한 데이터 변환

 

필자는 예시를 진행하기 위해 gradle 환경에서 진행했다.

우선 jackson의 dependency를 추가한다.

implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.1'

 

jackson 라이브러리를 사용하기 위한 DTO 를 생성한다.

 

public class TestDTO {

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    public static class Request {
        private String name;
        private String telno;
    }

}

 

 

필자는 참고로 롬복을 사용하고 있다.

 

그리고 다음처럼 데이터를 생성해서 결과를 찍어봤다.

public class TestMain {

    @Test
    void testDTO() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        TestDTO.Request test1 = new TestDTO.Request("test1", "01011112222");

        //Object -> Json
        String s = mapper.writeValueAsString(test1);
        System.out.println("json string value = " + s);

        //Json -> Object
        TestDTO.Request request = mapper.readValue(s, TestDTO.Request.class);
        System.out.println("object value = " + request);


    }

}

 

결과는 다음과 같다.

json string value = {"name":"test1","telno":"01011112222"}
object value = TestDTO.Request(name=test1, telno=01011112222)

 

3.  객체 내에 객체 포함

이번에는 조금 다르게 TestDTO.java와 TestMain.java를 수정하겠다.

 

TestDTO.java 수정

public class TestDTO {
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    public static class Request {
        private String name;
        private String telno;
        private int no;

        private User owner;
    }

    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public static class User {
        private String height;
        private String weight;
    }
}

 

TestMain.java 수정

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class TestMain {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        TestDTO.Request request = new TestDTO.Request("name", "01011112222", 1, new TestDTO.User("183","75"));

        //Object -> Json
        String s = mapper.writeValueAsString(request);
        System.out.println("json string value = " + s);

        //Json -> Object
        TestDTO.Request newReq = mapper.readValue(s, TestDTO.Request.class);
        System.out.println("object value = " + newReq);
    }

}

 

결과는 다음과 같다.

 

json string value = {"name":"name","telno":"01011112222","no":1,"owner":{"height":"183","weight":"75"}}
object value = TestDTO.Request(name=name, telno=01011112222, no=1, owner=TestDTO.User(height=183, weight=75))

 

참고로 직렬화 과정중에서 Jackson과 같은 라이브러리들은 객체의 필드 값을 얻기 위해 getter 메소드를 사용한다.

직렬화 하려는 항목에 대해서는 Getter method가 필요하다는 말이다. 그래서 필자는 @Getter를 사용했다.

 

또한, 역직렬화 과정에서 @NoArgsConstructor도 유용할 텐데 Jackson과 같은 라이브러리들은 객체를 생성할 때 기본 생성자(NoArgsConstructor)를 사용하기 때문이다.

 

3.  Custom Deserializer 사용하기 

그러면 여태 Jackson 잘 사용하고 있는데 왜 custom deserializer가 나오는지 궁금한가?

이것이 필요한 이유는 다음과 같다.

 

  1. 속성 불일치 해결
  2. 복잡한 데이터 변환
  3. 유연한 데이터 처리

다음 json 데이터로 테스트를 진행할 것이다.

 

 

public class TestMain {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();

        String testStr = "{\"name\":\"name\",\"telno\":\"01011112222\",\"no\":1,\"who\":3";

        TestDTO.Request request1 = mapper.readValue(testStr, TestDTO.Request.class);

        System.out.println("request1 = " + request1);

    }

}

 

결과는 다음과 같다.

 

Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "who" (class com.example.springbatch.jsontest.TestDTO$Request), not marked as ignorable (4 known properties: "no", "name", "telno", "owner"]) at [Source: (String)"{"name":"name","telno":"01011112222","no":1,"who":3"; line: 1, column: 52] (through reference chain: com.example.springbatch.jsontest.TestDTO$Request["who"]) at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:61) at com.fasterxml.jackson.databind.DeserializationContext.handleUnknownProperty(DeserializationContext.java:1127) at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:2036) at
(중간 생략...)

 

field명과 꼭 일치하지 않는 경우에도 우리가 마음데로, 즉 복잡한 데이터 변환 혹은 유연한 데이터 처리를 위해서 

custom deserializer를 만들 예정이다.

 

TestDTO.java 수정

package com.example.springbatch.jsontest;

import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.*;

public class TestDTO {
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    @JsonDeserialize(using = TestDTODeserializer.class)
    public static class Request {
        private String name;
        private String telno;
        private int no;
        private User owner;
    }

    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public static class User {
        private String height;
        private String weight;
    }
}

 

TestDTODeserializer.java 추가

public class TestDTODeserializer extends StdDeserializer<TestDTO.Request> {

    public TestDTODeserializer() {
        this(null);
    }

    protected TestDTODeserializer(Class<?> t) {
        super(t);
    }

    @Override
    public TestDTO.Request deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
        JsonNode node = p.getCodec().readTree(p);
        String name = node.get("name").asText();
        String telno = node.get("telno").asText();
        int who = (Integer) node.get("who").numberValue();
        TestDTO.User user = new TestDTO.User("", Integer.toString(who));
        return new TestDTO.Request(name, telno, 0, user);

    }
}

 

TestMain.java 수정

public class TestMain {
    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        String testStr = "{\"name\":\"name\",\"telno\":\"01011112222\",\"no\":1,\"who\":3}";
        TestDTO.Request request1 = mapper.readValue(testStr, TestDTO.Request.class);
        System.out.println("request1 = " + request1);
    }
}

 

결과

request1 = TestDTO.Request(name=name, telno=01011112222, no=0, owner=TestDTO.User(height=, weight=3))

 

4. 특정 field만 변환하기 - ContextualDeserializer 사용

만약에 (내가 만일 하늘이라면~) 특정 field만 선택해서 값을 변환하고 싶다면 어떻게 해야 할까?

이런 경우에는 별도의 custom annotation을 선언해서 사용하면 좋을 것이다.

 

이런 경우에는 ContextDeserializer를 사용해서 적절한 Deserializer를 해당 field 변환 시 사용하는 것이다.

 

필자는 String형식의 field 위에 @TestAnno가 붙은 친구들 뒤에 "_test"라고 붙이려고 한다.

 

TestDTO.java 수정

public class TestDTO {

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @ToString
    public static class Request {
        private String name;
        @TestAnno
        private String telno;
        @TestAnno
        private int no;
        private User owner;
    }

    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString
    public static class User {
        private String height;
        private String weight;
    }

}

 

TestAnno.java 수정

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JsonDeserialize(using = NewDeserializer.class)
public @interface TestAnno {
}

 

NewDeserializer.java 수정

public class NewDeserializer extends StdDeserializer<String> implements ContextualDeserializer {

    private boolean isUsed;

    // 기본 생성자
    public NewDeserializer() {
        super(String.class);
    }

    protected NewDeserializer(boolean isUsed) {
        super(String.class);
        this.isUsed = isUsed;
    }

    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
        String value = p.getValueAsString();

        // isUsed가 true일 경우, 값 뒤에 "_test" 추가
        if (isUsed) {
            return value + "_test";
        }
        return value;
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
        if (property != null && property.getAnnotation(TestAnno.class) != null) {
            return new NewDeserializer(true);
        }
        return new NewDeserializer(false);
    }
}

 

TestMain.java

public class TestMain {

    @Test
    void test() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.addDeserializer(String.class, new NewDeserializer());
        mapper.registerModule(module);
        String testStr = "{\"name\":\"name\",\"telno\":\"01011112222\",\"no\":1}";
        TestDTO.Request request1 = mapper.readValue(testStr, TestDTO.Request.class);
        System.out.println("request1 = " + request1);
    }

}

 

여기서 주의할 점이 mapper에 우리가 새로이 만든 custom deserializer를 등록해야만 ObjectMapper가 @TestAnno 에 대해 NewDeserializer를 사용할 수 있다.

 

커스텀 deserializer 등록 부분을 제거하면 우리가 원하는 "_test"데이터가 붙지 않고 휭~ 지나갈 것이다.

 

결과는 다음과 같다.

request1 = TestDTO.Request(name=name, telno=01011112222_test, no=1, owner=null)

 

필자는 deserializer를 사용 했는데, 여태 작업들은 serializer에게도 유사하게 적용된다.

다만 serializer는 java에서 json 으로 변환 시에, deserializer는 json에서 java로 변환 시에 사용을 한다고 가정을 했다.

 

또 알고 넘어가면 좋은 점은 ObjectMapper를 빈으로 등록해서 빼서 사용해도 좋을 것 같다는 생각이다.

다른 개발자들이 간단하게 어노테이션만 적용하면 바로 변환이 될 수 있도록 말이다!

 


 

이번 시간에는 직렬화, 역직렬화시에 유용한 변환 방법에 대하 알아보았다.

 

문제시 댓글 부탁 헤어~ (모발 모발~)

 

출처

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

https://siyoon210.tistory.com/185

https://multifrontgarden.tistory.com/206

https://hianna.tistory.com/631#jackson2

https://circlee7.medium.com/jackson-contextualdeserializer-2f0d20f08ce0

https://d2.naver.com/helloworld/0473330