본문 바로가기

Spring

Spring MVC에서 MappingJacksonValue 활용하기

들어가면서

Spring MVC를 사용해 애플리케이션을 개발하는 중이였다.

개발을 하다 응답 DTO 필드 중 일부를 운영 환경에서 노출하지 않고 싶은 상황이 생겼다. 이를 어떻게 개발하면 좋을지 고민했다. 그 과정에서 MappingJacksonValue를 알게 되었고, 이를 이용해 문제를 해결했다.

 

이번글에서는 MappingJacksonValue를 이용해 응답 DTO에서 특정 필드를 노출하지 않는방법과 동작원리를 공유하겠다.

 

MappingJacksonValue ?

MappingJacksonValue는 Spring Framework에서 제공하는 라이브러리로, 응답 DTO를 HTTPResponseBody에 직렬화 하기 전 내가 원하는 로직을 추가해 직렬화 과정을 커스터마이징 할 수 있다.

A simple holder for the POJO to serialize via MappingJackson2HttpMessageConverter along with further serialization instructions to be passed in to the converter.On the server side this wrapper is added with a ResponseBodyInterceptor after content negotiation selects the converter to use but before the write.On the client side, simply wrap the POJO and pass it in to the RestTemplate.

 

 

MappingJacksonValue Sample Code

MappingJacksonValue를 이용해 특정 필드를 무시하고 직렬화 하는 예시코드이다.

 

Code

@RestController
public class HelloController {

    @GetMapping("/hello")
    public ResponseEntity<MappingJacksonValue> getHello() {
        Hello hello = Hello.builder()
            .id(UUID.randomUUID().toString())
            .msg("hello")
            .timeStamp(LocalDate.now())
            .build();

        return ResponseEntity.ok(wrapResponseWithCustomFilter(hello));
    }

    private MappingJacksonValue wrapResponseWithCustomFilter(Object response) {
        SimpleBeanPropertyFilter simpleBeanPropertyFilter = SimpleBeanPropertyFilter.serializeAllExcept("id");
        FilterProvider filterProvider = new SimpleFilterProvider().addFilter("helloFilter", simpleBeanPropertyFilter);

        MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(response);
        mappingJacksonValue.setFilters(filterProvider);
        return mappingJacksonValue;
    }

    @JsonFilter("helloFilter")
    @Getter
    static class Hello {
        private String id;
        private String msg;
        private LocalDate timeStamp;

        @Builder
        public Hello(String id, String msg, LocalDate timeStamp) {
            this.id = id;
            this.msg = msg;
            this.timeStamp = timeStamp;
        }
    }
}

(Github)

 

위 코드에서 가장 핵심은 wrapResponseWithCustomFIlter 메소드이다. 해당 함수에서 MappingJacksonValue에 특정 필드를 무시하고 직렬화하는 filter를 추가했다. 덕분에 응답값에 id는 노출되지 않는다. 아래는 결과 응답값이다.

 

 

HTTP Response

GET http://localhost:8080/hello

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 01 Mar 2024 07:30:17 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "msg": "hello",
  "timeStamp": "2024-03-01"
}

 

위 샘플 코드는 간단한 예시이다. 위 코드를 이해하고 각자의 상황에 맞게 사용하기 위해서는 코드의 동작원리를 명확히 이해하는게 좋다.

코드를 통해 동작원리를 살펴보겠다.

 

MappingJacksonValue 동작원리

MappingJacksonValue 클래스를 부터 살펴보겠다.

public class MappingJacksonValue {

	private Object value;

	@Nullable
	private Class<?> serializationView;

	@Nullable
	private FilterProvider filters;
    
    //skip
    
 }

MappingJacksonValue는 원본 객체인 value에 더해 serializationView와 filters를 맴버로 갖고 있다. 이를 보면 value를 ResponseBody에 write 할 때 serializationView 또는 filters가 있다면 각각을 적용한 뒤 ResponseBody에 write 한다는걸 예측할 수 있다.

 

그럼 실제로 그렇게 동작하는지 코드로 살펴보자.

 

본격적으로 코드를 보기전에 Spring MVC에 대한 이해가 없다면 공식문서를 읽고 오는걸 추천한다.

 

Spring MVC 아키텍처에서 그러하듯, 요청이 들어오면 다음과 같은 플로우로 요청이 처리된다.

1. 요청에 알맞은 핸들러 선택

2. 핸들러에 처리 위임

3. 핸들러로 처리한 응답값에 알맞는 응답 핸들러 선택

4. 핸들러에서 응답 처리

 

실제로 우리가 관심있는 부분은 3, 4번이기 때문에 그 부분 위주로 살펴보겠다.

 

요청이 들어와 핸들러에서 요청이 처리되면, HandlerMethodReturnValueHandlerComposite 클래스의 handleRetureValue에 의해 반환값이 처리된다. HandlerMethodReturnValueHandlerComposite은 적절한 핸들러를 찾은뒤 응답값 처리를 위임하는 역할을 한다.

HandlerMethodReturnValueHandlerComposite

위 이미지에서 볼 수 있듯이, 응답값 타입이 ResponseEntity(HttpEntity의 자식 클래스)기 때문에 핸들러로 HttpEntityMethodProcessor가 선택되었다.

 

HttpEntityMethodProcessor는 ResponseEntity 객체를 resolve해 HttpResponse에 값을 쓰는 작업을 한다. 이때 ResponseEntity의 body 타입에 맞는 HttpMessageConverter를 사용한다.

우리는 body 타입이 MappingJacksonValue기 때문에 converter로 MappingJackson2HttpMessageConverter가 선택되었다.

(아래 그림은 HttpMessageConverter로 MappingJackson2HttpMessageConverter가 선택되는 코드이다.)

AbstractMessageConverterMethodProcessor의 writeWithMessageConverters

 

MappingJackson2HttpMessageConverter에서는 ObjectWriter를 이용해 객체를 직렬화한다. 이때 직렬화하려는 객체의 타입이 MappingJacksonValue라면 ObjectWriter에 MappingJacksonValue에서 정의한 filter를 설정해준다.

AbstractJackson2HttpMessageConverter의 writeInternal

 

실제로 직렬화 과정에서는 filter를 이용해 직렬화된다.

BeanSerializerBase의 serializeFiltersFiltered

 

 

마무리

이번 글을 통해 MappingJacksonValue를 이용해 직렬화 커스터마이징 하는 방법과 동작원리에 대해 정리봤다. 이 내용이 도움이 되길 바란다.