Map 또는 직렬화된 Json 문자열을 객체로 형변환하기 위해서는 objectMapper 또는 gson 객체를 사용할 수 있다.
채팅방 개발에 두 가지 의존성 라이브러리를 모두 사용해 보면서 Jackson과 Gson의 설정, 사용법, 장단점, 이슈에 대해 정리해보자.
채팅방 만들면서 JSON 형식 데이터를 처리하기 위해 Gson을 도입했다.
구글링한 두 의견을 듣고 Gson으로 Cloudinary 업로드와 채팅 핸들러에 형변환을 사용했다.
채팅방 리스트를 조회하는데 생각지도 못한 에러를 만났다. Gson이 자바의 LocalDate
와 LocalDatetime
을 인식하지 못한다는 것이다;;
이를 해결하기 위해서는 별도의 타입 어댑터를 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 타입 에러를 수정했다. 그런데 또 다른 불편한 점이 생겼다.
보통 Spring Boot에서 iv로 명시하지 않아도 getter를 명시하면 vo에 해당 값이 포함되었다.
당연히 그걸 기대하고 아래와 같이 작성했다. 나는 응답값에 timeDiffFromNow
필드를 기대했다.
@Schema(description = "최근 채팅 시간")
private LocalDateTime ltsTime;
/**
* 채팅방 생성 시간과 현재 시간의 차이를 포맷된 문자열로 반환
* @return 시간 차이 문자열
*/
public String getTimeDiffFromNow() {
return DateUtil.formatTimeDiffFromNow(this.ltsTime);
어? 근데 응답값에 포함되지 않는다.
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
나는 이 상황에서 Gson이 불편했지만 Jackson이 불편한 경우도 있다.
역직렬화할 때 getter만 선언한 경우 인식되지 않은 필드라며 파싱 에러를 내기 때문이다.
이 때는 @JsonIgnore
어노테이션을 해당 getter에 추가함으로써 해결한다.
👉 https://kim-solshar.tistory.com/85
Gson은 필드를 기준으로 JSON을 형변환하며, 값이 null이면 해당 필드는 응답값에서 제외된다.
특정 필드를 제외할 수 없을까?
@Expose
어노테이션이 없는 필드들은 응답값에서 제외하겠다는 뜻이다.Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.create();
@Expose
어노테이션을 추가한다.@Expose
어노테이션을 getter에 적용하면 되지 않을까? 아쉽지만 필드에만 적용 가능하다.
이제 되었나? 싶은데 또 chatMsg가 아니라 chat_msg로 값이 출력된다 (이 무슨?)
GsonConfig에서 네이밍 전략을 잘못 설정해서 언더스코어가 아닌 변수들을 언더스코어로 바꿔버렸다;;
언더스코어 필드명을 카멜케이스로 변환하고 싶다면 해당 iv에 @SerializedName
을 붙여준다.
(getter에 붙여도 봤는데 에러는 안 나지만 응답값에 포함되지도 않았다. 은은하게 아무것도 안 한다)
@SerializedName("chat_room")
private String chatRoom;
Jackson을 통해서 언더스코어를 카멜로 바꾸려면 아래처럼 네이밍 전략을 객체 생성시 설정한다.
(스토리지 업로드 코드로, public_id
를 publicId
로 바꾸기 위함이다.)
// ... 중략
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);
처음에는 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을 사용할 것이다.