[Spring] Jackson 이란 (+Json)

Benjamin·2023년 9월 24일
0

Spring

목록 보기
5/5

Json

Jackson을 알려면, 우선 Json이 무엇인지 알아야한다.
(여기서는 Json이 주 내용이 아니므로 기본적인 내용만 다루겠다)

Spring 개발을 하다보면 데이터를 전달하고싶을 때가 있다.
이럴 때에는 보통 데이터 구조를 표현하는 방식인 XML 또는 JSON 형태로 많이 보낸다.

데이터의 구조를 표현하는 이유는 데이터 표현도 있지만, 사실상 데이터를 사용하는 대상이 편하게 사용하기 위해서이다.

그리고 Jackson은 JSON 데이터 구조를 처리해주는 라이브러리이다.

JSON 구조

  1. JSON 데이터는 이름과 값의 쌍으로 이루어집니다.
  2. JSON 데이터는 쉼표(,)로 나열됩니다.
  3. 객체(object)는 중괄호({})로 둘러쌓아 표현합니다.
  4. 배열(array)은 대괄호([])로 둘러쌓아 표현합니다.

문법

"데이터이름": 값

데이터의 이름도 문자열이므로, 항상 큰따옴표("")와 함께 입력해야 한다.

데이터 타입

데이터의 값으로는 다음과 같은 타입이 올 수 있다.

  1. 숫자(number)
  2. 문자열(string)
  3. 불리언(boolean)
  4. 객체(object)
  5. 배열(array)
  6. NULL

데이터 작성

Json 데이터는 어떻게 작성할까?

원초적으로는 아래처럼 직접 하나하나 작성할 수 있다.

String JSON = "\"{"+
   "\"name\": \"" + person.getName() + "\","+
   "\"job\": \"" + person.getJob() + "\""+
"}\"";

항상 저렇게 코딩 할 순 없으니, JSON 변환용 클래스를 따로 만들고 그 클래스안에 저장된 멤버변수를 이용하여 JSON 데이터를 출력하는 클래스를 이용한다.

대표적인 클래스가 Google 이 만든 GSON 또는 SimpleJSON 등이 있다.

SimpleJSON을 예로 든다면, 윗 코딩이 아래와 같이 바뀐다.

JSONObject jsonObject = new JSONObject();
jsonObject.put("name", person.getName());
jsonObject.put("job", person.getJob());
String JSON = jsonObject.toString();

해당 방식은 지금도 많이 쓰이고, 물론 Spring 컨트롤러에 위와 같이 코딩하여 리턴하더라도, 충분하다.
그렇다면, Jackson은 무엇을 더 제공하길래 Spring은 Jackson을 더 선호하는 것일까?


Jackson

  • JSON 데이터 구조를 처리해주는 라이브러리이다.
  • XML, YAML, CSV 등의 다른 형식의 데이터를 지원하는 data-processing 툴
  • 스트림 방식으로 속도가 빠르고 유연하다.
  • annotation 방식으로 사용이 가능하며 각종 문서화와 유효성 체크를 쉽게 가능하게 해준다.

Jackson과 기존 GSON or SimpeJSON과의 차이?

사실 차이는 없다.

Jackson도 ObjectMapper API를 사용하여, GSON or SimpeJSON과 같이 객체에 데이터를 셋팅해줘야 하는건 마찬가지다.

특별한 점은 Spring 프레임워크와 Jackson의 관계로부터 장점이 있다.

Spring 3.0 이후로부터, Jacskon과 관련된 API를 제공함으로써, Jackson라이브러리를 사용할때, 자동화 처리가 가능하게 되었습니다.

덕분에, JSON데이터를 직접 만들거나 GSON or SimpleJSON방식과 같이 직접 key와 value를 셋팅하는 방식에서 한단계 더 발전한 방식이 가능해졌다.

아래에서 Jackson의 동작 원리를 알아보자.

Jackson 은 어떻게 동작하는가?

Spring은 @ResponseBody를 사용하여 컨트롤러의 리턴 값을 HTTP 응답 본문으로 변환할 때 MessageConverter를 활용한다. 또한 이를 활용하여 컨트롤러가 리턴하는 객체를 후킹 할 수 있다.

Jackson은 JSON데이터를 출력하기 위한 MappingJacksonHttpMessageConverter를 제공한다.
만약 우리가 스프링 MessageConverter를 MappingJacksonHttpMessageConverter로 등록한다면, 컨트롤러가 리턴하는 객체를 다시 뜯어(자바 리플렉션 사용), Jackson의 ObjectMapper API로 JSON 객체를 만들고 난 후, 출력하여 JSON데이터를 완성한다.

더욱 편리해진 점은, Spring 3.1 이후로 만약 클래스패스에 Jackson 라이브러리가 존재한다면, ( 쉽게 말해 Jackson을 설치했느냐 안했느냐 ) 자동적으로 MessageConverter가 등록된다는 점이다.

덕분에 우리는 아래와 같이 매우 편리하게 사용할 수 있다.

@RequestMapping("/json")
@ResponseBody()
public Object printJSON() {
   Person person = new Person("Mommoo", "Developer");
   return person;
}

이제는 그냥 데이터 인스턴스만 리턴 하더라도 JSON 데이터가 출력된다.
위에서 설명한 방식보다 매우 진보한 방식인걸 알 수 있다.

다만, Jackson을 더 잘쓰기 위해서는 알아야 하는 기본 지식이 몇가지 존재한다.

Jackson을 사용하기 위해 알아야 하는 기본지식

Jackson은 기본적으로 프로퍼티로 동작한다.

Java는 프로퍼티를 제공하는 문법이 없다. (멤버변수랑은 다르다)
Java의 프로퍼티는 보통 Getter 와 Setter의 이름 명명 규칙으로 정해진다.

Person 같은 경우는 Getter만 존재 하므로, Getter를 기준으로 프로퍼티를 도출 할 수 있다. 즉 Name 과 Job이 Person 프로퍼티이다.

Person의 멤버변수 이름도 똑같이 name, job이지만, 앞서 설명했듯 프로퍼티는 Getter, Setter기준이므로 멤버변수 이름을 변경하더라도 상관 없다.

갑자기 프로퍼티를 설명한 이유는 많은 라이브러리가 해당 프로퍼티 개념으로 작동하기 때문이다.

Jackson라이브러리도 마찬가지다.
JSON데이터로 출력되기 위해서는 멤버변수의 유무가 아닌 프로퍼티, 즉 Getter, Setter를 기준으로 작동한다.

따라서 예로 아래와 같이 코딩하더라도 전혀 문제가 없다.

public class Person {
   public String getName() {
       return "Mommoo";
  }
   
   public String getJob() {
       return "Developer";
  }
}

@RequestMapping("/json")
@ResponseBody()
public Object printJSON() {
   return new Person();
}

Jackson의 데이터 매핑을 Getter가 아닌, 멤버변수로 하고 싶다면?

그렇다면, 이번에는 Jackson의 매핑을 프로퍼티가 아닌 멤버변수로 할 수 있는 방법을 알아보자.

Jackson은 이와 관련해 @JsonProperty 어노테이션 API를 제공한다.
아래와 같이 멤버변수 위에 프로퍼티 이름과 함께 선언해준다면, JSON데이터로 출력된다.

public class Person {
@JsonProperty("name")
   private String myName = "Mommoo";
}

위의 예시는 {"name": "Mommoo"}로 출력된다.

그렇다면 JSON 매핑을 멤버변수로 하고 싶다면, 매번 @JsonProperty를 선언 해야 할까?

Jackson의 데이터 매핑 법칙 변경하기

Jackson은 매핑 법칙을 바꿀 수 있는 @JsonAutoDetect API를 제공한다.

위 예시와 같이 멤버변수로만 Jackson을 구성하고 싶은 경우, @JsonProperty를 일일이 붙이는 것보다 아래와 같이 설정하는 것이 더 편리하다.

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Person {
   private String myName = "Mommoo";
}

@JsonAutoDetect는 멤버변수 뿐만 아니라, Getter, Setter의 데이터 매핑 정책도 정할 수 있다.
아래의 경우는 멤버변수 뿐만 아니라, 기본정책인 Getter역시 데이터 매핑이 진행된다.

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Person {
   private String myName = "Mommoo";
   
   public String getJob() {
       return "Developer";
  }
}

Getter를 제외하고 싶다면, @JsonIgnore API를 쓰면된다.

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public class Person {
   private String myName = "Mommoo";
   
   @JsonIgnore
   public String getJob() {
       return "Developer";
  }
}

하지만, 이것조차 일일이 붙여야 하는 상황이 온다면 매핑 정책을 바꾸는게 좋다.

아래예시는 Getter정책으로 private 만 데이터 바인딩에 제외했다.

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NON_PRIVATE)
public class Person {
   private String myName = "Mommoo";
   
   public String getJob() {
       return "Developer";
  }
}

이렇게 제외범위를 설정할 수 있는데, 자세한건 아래링크를 참조하자.
https://fasterxml.github.io/jackson-annotations/javadoc/2.9/com/fasterxml/jackson/annotation/JsonAutoDetect.Visibility.html

Jackson의 데이터 상태에 따른 포함 관계 설정

만약 Jackson데이터 매핑시 NULL 값과 같은 특정 데이터 상태인 경우를 제외하고 싶다면 어떻게 해야 할까?

Jackson은 이와 관련하여 @JsonInclude API를 제공한다.
NULL을 클래스 전반적으로 제외하고 싶다면, 클래스 위에 선언하면 됩니다.
또한 특정 프로퍼티가 NULL일때 해당 프로퍼티만을 제외하고 싶다면 역시 해당 프로퍼티위에 선언하면 된다.

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Person {
   private String myName = "Mommoo";
   
   public String getJob() {
       return "Developer";
  }
}

public class Person {
   private String myName = "Mommoo";
   
   @JsonInclude(JsonInclude.Include.NON_NULL)
   public String getJob() {
       return "Developer";
  }
}

JsonInclude.Include 속성은 NON_NULL뿐만 아니라 몇몇 개가 더 존재한다.

자세한건 아래링크를 참고하자.
https://fasterxml.github.io/jackson-annotations/javadoc/2.9/com/fasterxml/jackson/annotation/JsonInclude.Include.html

Jackson 어노테이션

@JsonAnyGetter

  • Map 필드를 다루는데 유연성을 제공
  • 이 엔티티의 인스턴스를 직렬화 할 때, 타입이 Map인 멤버변수의 Getter위에 선언하면, 모든 '키 - 값' 을 표준 일반 속성으로 가져온다. ("key":"value" 형식으로 나온다)
@Getter
@Builder
public static class ExtendableBean {
    public String name;
    private Map<String, String> properties;

    @JsonAnyGetter
    public Map<String, String> getProperties() {
        return properties;
    }
}

<결과>

//적용전
{
  "name": "yun",
  "properties": {
    "key1": "value1",
    "key2": "value2"
  }
}
//적용후
{
  "name": "yun",
  "key1": "value1",
  "key2": "value2"
}

@JsonGetter

getter 이름 기반으로 키값이 정해지는것을 어노테이션으로 제어

@Builder
public static class MyBean {
    public int id;
    private String name;

    @JsonGetter("name")
    public String getTheName() {
        return name;
    }
}

<결과>

//적용전
{
  "id": 1,
  "theName": "yun"
}
//적용후
{
  "id": 1,
  "name": "yun"
}

@JsonPropertyOrder

Json 직렬화 순서를 제어

@JsonPropertyOrder({"name", "id"})
@Builder
public static class PropertyOrder {
    private long id;
    private String name;
}

<결과>

//적용전
{
  "id": 1,
  "name": "name"
}
//적용후
{
  "name": "name",
  "id": 1
}

@JsonValue

전체 인스턴스를 직렬화할 때 사용하는 단일 메서드를 나타낸다.
예를들어, enum에서 getName 메서드에 @JsonValue를 넣어주어 이름을 통해 직렬화 할 수 있다.

말이 조금 어려울 수 있는데 예시를 보자.

public enum TypeEnumWithValue {
    TYPE1(1, "치킨"), TYPE2(2, "피자");

    private Integer id;
    private String name;

    TypeEnumWithValue(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    //@JsonValue
    public String getName() {
        return name;
    }
}

이 enum 은 TYPE1 과 TYPE2가 있다.
하단의 getName에 @JsonValue 어노테이션을 주석처리해뒀다.
만약 저 어노테이션이 없다면, 이 enum을 json으로 직렬화 할 때 enum 이름으로 직렬화가 된다.

new ObjectMapper().writeValueAsString(TypeEnumWithValue.TYPE1);

즉 이 결과가 다음과 같이 나오게 된다. (애노테이션 없을 시)

TYPE1

enum의 name 필드로 직렬화를 하고 싶다면 (치킨, 피자..) @JsonValue 어노테이션을 getName() 메서드위에 붙인다.

@JsonValue
public String getName() {
    return name;
}

이렇게 직렬화 시 나오는 필드를 결정하고, 결과적으로 다음과 같은 내용을 얻을 수 있다.

치킨

@JsonRawValue

Jackson이 속성을 그대로 직렬화하여 JSON으로 변경

@Builder
    public static class RawBean {
        public String name;

        @JsonRawValue
        public String json;
    }

<결과>

//적용전
{
  "name": "yun",
  "json": "{\n  \"attr\":false\n}"
}
//적용후
{
  "name": "yun",
  "json": {
    "attr": false
  }
}

@JsonRootName

@Builder
@JsonRootName(value = "user")
public static class UserWithRoot {
    public int id;
    public String name;
}

//objectMapper.enable(SerializationFeature.WRAP_ROOT_VALUE); 반드시 적용해야함

<결과>

//적용전
{
  "id": 1,
  "name": "yun"
}
//적용후
{
  "user": {
    "id": 1,
    "name": "yun"
  }
}

참고
http://www.tcpschool.com/json/json_basic_structure
https://mommoo.tistory.com/83
https://cheese10yun.github.io/jackson-annotation/
https://pjh3749.tistory.com/281

0개의 댓글