[글또] 7. [Java , Spring Boot] - Jackson과 Gson

jinvicky·2025년 1월 7일
0

Intro


Map 또는 직렬화된 Json 문자열을 객체로 형변환하기 위해서는 objectMapper 또는 gson 객체를 사용할 수 있다.
채팅방 개발에 두 가지 의존성 라이브러리를 모두 사용해 보면서 Jackson과 Gson의 설정, 사용법, 장단점, 이슈에 대해 정리해보자.

탐구의 시작

채팅방 만들면서 JSON 형식 데이터를 처리하기 위해 Gson을 도입했다.

  • Jackson보다 설정이 쉽다고 함
  • 대규모에는 Jackson이 낫고, 소규모는 Gson이 더 편하다

구글링한 두 의견을 듣고 Gson으로 Cloudinary 업로드와 채팅 핸들러에 형변환을 사용했다.
채팅방 리스트를 조회하는데 생각지도 못한 에러를 만났다. Gson이 자바의 LocalDateLocalDatetime을 인식하지 못한다는 것이다;;

Gson은 LocalDate(time)를 몰라

이를 해결하기 위해서는 별도의 타입 어댑터를 GsonConfig에 등록해주어야 한다.

	@Bean
    public GsonHttpMessageConverter gsonHttpMessageConverter() {
        Gson gson = new GsonBuilder()
                .setFieldNamingPolicy(com.google.gson.FieldNamingPolicy.IDENTITY)
                .registerTypeAdapter(LocalDate.class, new LocalDateAdapter())
                .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
                .create();
        return new GsonHttpMessageConverter(gson);
    }

별도로 타입 어댑터를 @Bean 어노테이션으로 등록하는 것이 아니라 저 GsonHttpMessageConverter에서 .registerTypeAdapter()를 통해서 등록해야 한다.

LocalDateTimeAdapter.java (LocalDate는 클래스 타입만 바꾸시면 됩니다)

public class LocalDateTimeAdapter extends TypeAdapter<LocalDateTime> {

    private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

    @Override
    public void write(JsonWriter jsonWriter, LocalDateTime localDateTime) throws IOException {
        jsonWriter.value(localDateTime.format(formatter));
    }

    @Override
    public LocalDateTime read(JsonReader jsonReader) throws IOException {
        return LocalDateTime.parse(jsonReader.nextString(), formatter);
    }
}

👉 https://velog.io/@leverest96/바보같은-gson에게-LocalDate나-LocalDateTime-알려주기

2024년 12월에 포스팅된 컬리 기술 블로그를 보면 에러 메세지는 나와 달랐지만 LocalDate에 대해서 Gson 커스텀 어댑터를 적용한 부분을 참고할 수 있다.

👉 https://helloworld.kurly.com/blog/75-java-module-with-gson-serialization/

자 이제 LocalDate 타입 에러를 수정했다. 그런데 또 다른 불편한 점이 생겼다.

Gson은 getter를 JSON에 포함하지 않아

보통 Spring Boot에서 iv로 명시하지 않아도 getter를 명시하면 vo에 해당 값이 포함되었다.
당연히 그걸 기대하고 아래와 같이 작성했다. 나는 응답값에 timeDiffFromNow 필드를 기대했다.

	@Schema(description = "최근 채팅 시간")
    private LocalDateTime ltsTime;
    
    /**
     * 채팅방 생성 시간과 현재 시간의 차이를 포맷된 문자열로 반환
     * @return 시간 차이 문자열
     */
    public String getTimeDiffFromNow() {
        return DateUtil.formatTimeDiffFromNow(this.ltsTime);

어? 근데 응답값에 포함되지 않는다.

Jackson의 직렬화

Spring Boot는 기본적으로 Jackson으로 객체를 JSON으로 직렬화하는데, 이때 VO에 정의된 모든 getter들을 확인한다. 이 getter들의 반환값들로 JSON 필드를 구성하는 것이다.
따라서 getTimeDiffFromNow() 를 통해 timeDiffFromNow 가 응답값에 포함되었던 것이다.

Gson은 설계 방식 자체가 객체의 필드를 직접 직렬화하는 것을 기반으로 한다. 따라서 getter를 읽지 않고 필드 자체를 읽어서 JSON에 포함하기 때문에 timeDiffFromNow 값이 포함되지 않았던 것이다.

👉 https://stackoverflow.com/questions/6203487/why-does-gson-use-fields-and-not-getters-setters

Jackson의 Parsing Error

나는 이 상황에서 Gson이 불편했지만 Jackson이 불편한 경우도 있다.
역직렬화할 때 getter만 선언한 경우 인식되지 않은 필드라며 파싱 에러를 내기 때문이다.
이 때는 @JsonIgnore 어노테이션을 해당 getter에 추가함으로써 해결한다.

👉 https://kim-solshar.tistory.com/85

Gson에서 특정 필드 제외하기

Gson은 필드를 기준으로 JSON을 형변환하며, 값이 null이면 해당 필드는 응답값에서 제외된다.
특정 필드를 제외할 수 없을까?

  1. GsonConfig에서 설정을 추가한다. @Expose 어노테이션이 없는 필드들은 응답값에서 제외하겠다는 뜻이다.
Gson gson = new GsonBuilder()
        .excludeFieldsWithoutExposeAnnotation()
        .create();
  1. 응답값에 포함하고 싶은 필드에만 @Expose 어노테이션을 추가한다.

@Expose 어노테이션을 getter에 적용하면 되지 않을까? 아쉽지만 필드에만 적용 가능하다.

이제 되었나? 싶은데 또 chatMsg가 아니라 chat_msg로 값이 출력된다 (이 무슨?)
GsonConfig에서 네이밍 전략을 잘못 설정해서 언더스코어가 아닌 변수들을 언더스코어로 바꿔버렸다;;

Gson에서 _가 들어간 변수명 처리

언더스코어 필드명을 카멜케이스로 변환하고 싶다면 해당 iv에 @SerializedName 을 붙여준다.
(getter에 붙여도 봤는데 에러는 안 나지만 응답값에 포함되지도 않았다. 은은하게 아무것도 안 한다)

 @SerializedName("chat_room") 
 private String chatRoom;

Jackson ObjectMapper에서 언더스코어 처리

Jackson을 통해서 언더스코어를 카멜로 바꾸려면 아래처럼 네이밍 전략을 객체 생성시 설정한다.
(스토리지 업로드 코드로, public_idpublicId로 바꾸기 위함이다.)

// ... 중략
Map map = cloudinary.uploader().upload(fileBytes, ObjectUtils.asMap("resource_type", resourceType));
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE);
        return objectMapper.convertValue(map, CloudinaryResponse.class);

Outro


처음에는 LocalDate 타입 에러로 시작했다가 Jackson과 Gson의 차이점과 어느 것을 선택할 지에 대한 기준에 대해 조사하고 알아보는 시간을 가질 수 있었다.

결과적으로 난 Gson을 일괄 제거하고 Jackson으로 변경했다ㅋㅋㅋ.

(jackson과 jackson-datatype-jsr310 -> java 8의 날짜 시간 API와 jackson을 사용하기 위함)

Gson이 성능상으로 확실한 이점 등을 제공하는 게 아니라면 Spring Boot의 기본을 사용하지 않을 이유가 없기 때문이다. 또한 getter 방식을 나는 현재 필요로 하고, Gson 도입으로 추가 학습시간을 소모하고 싶지 않았다 (우선순위 설정)

getter로 인한 미인식 필드 -> 파싱에러 이슈에 대해서도 ignore 설정을 객체 생성에 추가하면 파싱 에러를 방지할 수 있다.

objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

물론 추가로 getter로 생성된 필드도 응답값에 포함되는 것은 같다.
현재 나로서는 때때로 ignore 설정을 사용한 Jackson을 사용할 것이다.

profile
Front-End와 Back-End 경험, 지식을 공유합니다.

0개의 댓글