1. json 역직렬화(feat. jackson)
2. json 역직렬화 성능 측정(feat. jackson, gson, JSONObject)
개요
spring 프레임워크를 사용하고 있다면 request dto 객체를 만들고 @RequestBody 어노테이션을 붙이면 자동으로 json desirialization을 통해 객체 매핑하여 요청값을 처리할 수 있다.
사내에서는 spring이 아닌 다른 공통 프레임워크를 사용하고 있는데, fixed length 방식으로 요청/응답을 처리하고 있으므로 json 형식의 데이터는 취급하고 있지 않다. 다만, 다양한 모듈의 요청/응답(메서드 파라미터와 응답값을 포함)에 대한 송수신 객체를 유사 wrapper 클래스로 처리하고 있는데, json 역직렬화에 대한 api 지원이 마땅치가 않았다.
raw string으로 전달되는 json 포맷의 데이터를 역직렬화하기 위해서는 어떤 방식으로 처리해야 할까를 고민하면서 jackson, gson, JSONObject 사용에 대한 의사결정이 필요했다.
jackson
jackson은 json 데이터 직렬화/역직렬화 라이브러리이다. 위에서 말했듯이 요청 객체 매핑 시에 별 고민없이 사용하는 @RequestBody annotation의 동작에 관여한다. github jackson 공식 프로젝트를 보면, 다음과 같이 설명되어 있다.
Java를 위한 최고의 JSON parser로, Java(JVM 플랫폼 포함) 데이터 처리 도구 모음이다. json parser/generator, 데이터 바인딩(POJO와 JSON 간 변환), csv/xml/yaml 형식 등의 데이터도 처리한다.
3개의 핵심 모듈(Streaming, Annotations, Databind)이 존재한다. Streaming은 jackson-core로 저수준 streaming api를 구현하여 성능상 이점이 있다. annotations는 표준 주석에 대한 api를 제공한다. databind는 객체 매핑 기능을 제공한다.
크게 보면 높은 성능, 직렬화/역직렬화 시 강력한 커스터마이징, 객체 매핑 기능을 제공한다고 정리할 수 있다. 이 글에서는 1.spring에서의 jackson 사용 흐름, 2.성능 최적화가 어떻게 되어 있는가? 두 가지를 중점적으로 살펴보고자 한다.
spring에서 jackson 활용
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
//...
protected ObjectMapper defaultObjectMapper;
//...
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
//...
return objectReader.readValue(inputStream);
}
}
디버깅을 해보면 역직렬화 시 AbstractJackson2HttpMessageConverter가 주요 역할을 하고 있다. 여기에서 request input stream 데이터를 읽어 객체와 매핑하는데, 간략하게 주요 메서드만 보면 다음과 같다. 이 때 'AbstractJackson2HttpMessageConverter가 어디에서 나왔는가?'라는 의문이 있을 수 있다.
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
//...
protected Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) {
ServletServerHttpRequest inputMessage = this.createInputMessage(webRequest);
Object arg = this.readWithMessageConverters(inputMessage, parameter, paramType);
//...
}
}
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
//...
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) {
//...
if (message.hasBody()) {
HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterClass);
Object var34;
switch (converterTypeToUse) {
case BASE -> var34 = converter.read(targetClass, msgToUse);
case GENERIC -> var34 = ((GenericHttpMessageConverter)converter).read(targetType, contextClass, msgToUse);
};
//...
}
}
위에 Jackson2HttpMessageConverter로 converting 할 것을 결정하는 객체가 RequestResponseBodyMethodProcessor 클래스인데, 아래 메서드가 이미 등록된 jackson converter로 메시지를 읽는 것이다. 그러면 converter가 종류가 여러가지일텐데, 왜 하필 jackson converter로 결정이 되었을까? 이는 AbstractMessageConverterMethodArgumentResolver 구현 클래스에서 message와 parameter를 가지고 메시지 타입에 따라 판단한 것이다. 이 processor에는 9개의 conveter가 등록되어 있고, 그 중에서 Jackson2HttpMessageConverter를 선택한 것이다. 그렇다면 이 resolver(@RequestBody annotation을 사용한 메서드를 처리하는 Processor)는 어디에서 선택된 것일까?
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList();
//...
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
}
resolver들을 관리하는 composite 패턴의 구현 클래스가 있고, 필드로 resolver 리스트가 있는데 기본적으로 31개의 resolver가 등록되어 있다. RequestParamMethodArgumentProcessor, RequestResponseBodyMethodProcessor, RequestAttributeMethodArgumentProcessor 등이 그것인데, 흔히 사용하는 어노테이션 기반의 처리를 여기에서 담당하고 있는 것이다.
즉, spring application context가 load될 때 등록되었던 bean들 중 요청 메시지에 따라 Resolver가 Processor를 선택하고, Processor가 Conveter를 선택해 메시지를 해석하고 있는 것이다.
성능(streaming api)
jackson은 성능적으로 우수하다고 하는데, 이는 streaming api를 활용함으로써 생기는 이점이다. json 데이터를 직렬화/역직렬화(generator, parser) 할 때 메모리에 모두 로드해두는 것이 아닌 순차적으로 읽고 처리하면서, 이미 처리한 데이터는 메모리에서 flush 시켜 메모리 사용량을 줄일 수 있다는 것이다.
한 가지 의문이 든 점은, 어차피 stream buffer에 담겨온 요청 데이터를 메모리에 로딩해두는 것일텐데, 추가적인 메모리 사용량이 줄어드는 것이 맞나?란 생각이 들었다.
다음 글에서는 gson과 org.json에서 제공하는 api를 활용해 json parsing을 하는 과정을 알아보고 각 라이브러리의 성능 측정을 통해 비교해볼 예정이다.
'java' 카테고리의 다른 글
json 역직렬화 성능 측정(feat. jackson, gson, JSONObject) - 2 (0) | 2025.01.20 |
---|---|
중복 거래 방지를 위한 uuid 생성(UUIDv7, time-based uuid) (0) | 2024.11.11 |
동시성 이슈 해결하기(feat. spring(java), MySQL, Redis(Redisson)) (0) | 2024.06.24 |
LocalDateTime, Instant, OffsetDateTime, ZonedDateTime 사용법 차이 (0) | 2024.05.19 |
[Java] http 요청과 응답 구현하기 - 2(apache tomcat의 http 처리) (0) | 2023.04.17 |