티스토리 뷰

1. Jackson

Ajax 통신을 할때는 Response Body에 데이터만 담아서 클라이언트로 전송하게된다. 이때 가장 많이 사용하는 포맷은 json 형태일텐데 자바 객체를 json 형태로 변환해주는 여러 라이브러리중 하나인 jackson을 살펴보려한다.


설정은 거의 할게없다. 스프링 프로젝트를 생성한 후 jackson-databind 의존성을 추가해준다.


pom.xml

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.4.RELEASE</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.8</version>
</dependency>
</dependencies>


그리고 컨트롤러를 하나 구현한다.

@RestController
public class JsonController {
@GetMapping("")
public Person test(){
return new Person("LichKing", 29);
}
}


Person 클래스는 이런 간단한 클래스다.

@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
}


스프링이 구동만 가능할정도로 설정만하고 서버를 띄운다. 참고로 json으로 변환하기위한 추가설정은 현재 없다.

Controller에 보면 애노테이션이 @RestController로 되어있는데 이 경우는 응답을 Response Body에 담아서 전달하겠다는 의미이다. @ResponseBody와 @Controller가 합쳐진형태라고 보면 된다.

서버를 띄웠으면 로컬로 접속해보자.



뙇! json 형태의 데이터가 넘어온다. 별다른 설정없이 jackson에 대한 의존성이 걸려있으면 스프링은 자동으로 jackson을 사용해서 객체를 json형태로 변환한다.


@Test
public void test1() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(content().string(containsString("LichKing")));
}


테스트 케이스로 실행할때도 물론 통과한다.


자, 그럼 간략한 jackson 소개는 이정도로하고 포스팅의 본론으로 들어가자. 자바의 객체를 json으로 변환하는 작업을 serialize(직렬화), json을 자바의 객체로 변환하는걸 deserialize(역직렬화)라고 한다.

이때 우리가 아무런 설정도 안했는데 person의 필드인 name과 age가 serialize된건 jackson에 기본적인 serializer가 설정되어있기때문이다.


2. Custom Serializer

Person에 생일 필드를 추가해보자.

@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
private LocalDate birthDay;
}


자바8에 추가된 LocalDate 타입을 사용했다. 뭐 딴건 모르겠고 난 그냥 yyyy-MM-dd 형태로의 출력을 바란다.


@GetMapping("")
public Person test(){
return new Person("LichKing", 29, LocalDate.of(1990, 2, 2));
}


컨트롤러도 수정해주고..

@Test
public void 생일() throws Exception {
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(content().string(containsString("1990-02-02")));
}


테스트 케이스를 돌리니 실패한다.



브라우저에 직접 때리니 이런게 반환된다. LocalDate는 생각보다 많은걸 들고있는 놈인가보다. 그게 중요한게 아니고 난 저런게 필요하지않다.


public class LocalDateSerializer extends JsonSerializer<LocalDate> {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

@Override
public void serialize(LocalDate localDate, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeString(DATE_FORMAT.format(localDate));
}
}


이런경우 커스텀한 serialize를 위해 jackson은 JsonSerializer라는 추상클래스를 제공하고있다. 변환하길 원하는 타입을 제네릭으로 지정해주고 추상메서드를 구현해주면된다. 구현은 크게 어려운게 없다.


Serializer를 구현해주면 Person 클래스를 이렇게 수정하자.


@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
@JsonSerialize(using = LocalDateSerializer.class)
private LocalDate birthDay;
}


Serialize를 어떤 Serializer를 통해 할건지 지정해주는 설정이다. 이를 지정해주고 테스트케이스를 실행하면 개발자의 마음을 안정시켜주는 녹색불이 들어온다.


3. Custom Deserializer

이제 json을 자바 객체로 변환하는 역직렬화에 대해 알아보자. 일단 LocalDate는 뭔가가 복잡하다는게 위에서 드러났으니 생일을 제외한 테스트케이스를 작성해본다.


@Test
public void 역직렬화() throws Exception {
mockMvc.perform(post("/")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content("{\"name\":\"LichKing\",\"age\":29}"))
.andExpect(content().string(containsString("{\"name\":\"LichKing\",\"age\":29,\"birthDay\":null")));
}

(사실 테스트케이스 작성하는게 익숙치 않아서...올바른게 잘 작성된 테스트케이스인지는 나도 잘 모른다.. 테스트 케이스를 이용해 테스트를 하는거에 의의를 두고 연습하고있다.)


@PostMapping("")
public Person post(@RequestBody Person person){
return person;
}


역직렬화 테스트는 post로 요청을 전송하게 구현했으며, 테스트 케이스는 성공한다. 이제 생일까지 추가해보자.


@Test
public void 역직렬화_생일() throws Exception {
mockMvc.perform(post("/")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.content("{\"name\":\"LichKing\",\"age\":29, \"birthDay\":\"1990-02-02\"}"))
.andExpect(content().string(containsString("{\"name\":\"LichKing\",\"age\":29,\"birthDay\":\"1990-02-02\"")));
}

테스트 케이스는 실패하게되는데 이런 로그가 보인다.


JsonMappingException: Can not construct instance of java.time.LocalDate: no String-argument constructor/factory method to deserialize from String value ('1990-02-02') 


저 문자열을 LocalDate로 변환할 수 없다고한다. 이제 Deserializer를 구현할 차례다.


public class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

@Override
public LocalDate deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return LocalDate.parse(jsonParser.getText(), DATE_FORMAT);
}
}


@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
@JsonSerialize(using = LocalDateSerializer.class)
@JsonDeserialize(using = LocalDateDeserializer.class)
private LocalDate birthDay;
}


Serializer랑 똑같다. 구현해주고 Person에 애노테이션을 추가했다. 테스트케이스는 기분좋게 성공한다.


4. Config

우리가 원했던건 다 끝난것같지만 하나 궁금한게 생긴다. 지금은 Person밖에 없지만 추후 클래스가 많아지고 프로젝트의 기본 날짜 클래스를 LocalDate를 사용한다면 모든 클래스에 저 애노테이션을 기본적으로 달아줘야하는걸까? LocalDate에 대한 기본 Serializer, Deserializer를 지정하고 저 애노테이션을 다 없애보자.


public class CustomObjectMapper extends ObjectMapper {
public CustomObjectMapper(){
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(LocalDate.class, new LocalDateSerializer());
simpleModule.addDeserializer(LocalDate.class, new LocalDateDeserializer());

registerModule(simpleModule);
}
}


포스팅에서는 등장한적이 없지만 Jackson은 내부적으로 ObjectMapper라는 애를 써서 직렬화, 역직렬화를 한다. ObjectMapper를 확장해서 Module에 우리가 구현한 객체들을 추가해준다.


@Configuration
@EnableWebMvc
@ComponentScan("com.yong")
public class AppConfig extends WebMvcConfigurerAdapter {
@Bean
public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter(){
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setObjectMapper(new CustomObjectMapper());

return converter;
}

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jackson2HttpMessageConverter());
}
}


MessageConverter 빈을 생성해서 주입해준다.


@Data
@AllArgsConstructor
public class Person {
private String name;
private int age;
private LocalDate birthDay;
}


Person에서 애노테이션은 전부 삭제한다.



애노테이션이 사라졌음에도 정상적으로 우리가 원하는대로 직렬화된걸 확인할 수 있다.


5. 마치며

코드는 깃헙에서 확인할 수 있다. https://github.com/LichKing-lee/custom-jackson#jackson-custom

'Java' 카테고리의 다른 글

Garbage Collector  (0) 2017.07.05
DTO와 VO  (8) 2017.07.01
jackson custom serializer, deserializer 만들기  (3) 2017.04.19
Java8#06. time package  (1) 2017.03.04
servlet mapping /와 /* 차이점  (3) 2016.12.27
Reflections 라이브러리를 이용한 패키지 탐색  (0) 2016.12.24
공유하기 링크
TAG
댓글
댓글쓰기 폼