Spring Boot

[Spring Boot] MappingJackson2HttpMessageConverter 커스터마이징

bbyuk 2024. 8. 22. 20:07

놀랍게도 Spring Boot 관련 첫 포스팅

개인 프로젝트로 작업하다가 관련 내용들이 섞여 있었던 적도 있었지만 이론이 메인이 되는 포스팅은 처음이다.

회사 업무던 개인 프로젝트던 새로 알게 된 게 있더라도, github이나 사내 gitlab에 올리고서는 아~ 알았다~ 하고 말았었기 때문에 생겼던 문제였다.

 

오늘 회사에서 API Response로 나가는 Json의 필드가 생각한대로 셋팅이 안되는 문제를 해결하면서 알게되고 배운 부분에 대해 포스팅을 해보려고 한다.

 

1. 문제

회사 코드다보니 예제 코드로 대체한다.

import lombok.Data;

@Data
public class MyDto {
	private String oWEIRDField;
    // ...
}

 

이런 Dto가 있다고 가정해보자.

필드명이 카멜케이스 비스무리하게 정의되어 있다.

 

1. 첫 글자가 소문자

2. 첫 글자 뒤로 대문자가 2글자 이상

3. 대문자 뒤로는 우리가 아는 카멜케이스를 따른다.

 

위 Dto를 Response body에 담아 응답으로 내려주려고 한다.

 

아래의 예시 컨틀롤러처럼 @RestController를 붙여 ViewResolver가 아닌 HttpMessageConverter가 동작해 응답을 내려줄 수 있도록 구성했다.

* @ResponseBody 어노테이션을 붙이면 ViewResolver가 아닌 HttpMessageConverter가 동작

* @RestController에 @ResponseBody 어노테이션 포함

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {

	// ...

	@GetMapping("/mydto")
    	public MyDto getMyDto() {
    		MyDto myDto = new MyDto();
        	myDto.setOWEIRDField("hello");
        	return myDto;
    	}


	// ...
}

 

 

HttpMessageConverter는 인터페이스로 여러 구현체가 있고, Spring Boot에서는 아래의 우선순위대로 사용된다. 

뒤로도 더 있지만 일단 문제가 되었던 부분인 MappingJackson2HttpMessageConverter까지 알아보자.

ByteArrayHttpMessageConverter
StringHttpMessageConverter
MappingJackson2HttpMessageConverter
...

 

- ByteArrayHttpMessageConverter

클래스 : byte[]

미디어 타입 : */*

요청 예) @RequestBody byte[] data

응답 예) @ResponseBody return byte[], 쓰기 미디어 타입 : application/octet-stream

 

- StringHttpMessageConverter

클래스 : String

미디어 타입 : */*

요청 예) @RequestBody String data

응답 예) @ResponseBody return "hello world", 쓰기 미디어 타입: text/plain

 

- MappingJackson2HttpMessageConverter

클래스 : 객체 또는 HashMap, 미디어 타입 : application/json

요청 예) @RequestBody MyDto data

응답 예) @ResponseBody return new MyDto(), 쓰기 미디어 타입: application/json

 


 

이렇게 구성을 했을 때 /mydto를 호출했을 때 응답 데이터는 MappingJackson2HttpMessageConverter에 의해 Json으로 직렬화되어 아래처럼 결과를 받을 수 있을줄 알았다.

 

{
  "oWEIRDField": "hello"
  // ....
}

 

 

이미 프론트엔드쪽에서 이렇게 받도록 코드가 구성되어 있었기 때문에 Json의 키를 모두 응답 Dto의 필드명으로 일치시켜서 내려줘야 했다.

 

그렇지만 와장창 아래처럼 응답을 받았다.

{
  "oweirdField": "hello"
  // ....
}

 

엄,,,,

 

다시 거슬러 올라가서 생각을 해보면 MappingJackson2HttpMesageConverter는 내부적으로 ObjectMapper를 이용한다.

 

찾아보니 역시 ObjectMapper에 커스터마이징 할 포인트가 있었다.

ObjectMapper와 Visibility 관련된 설정에 관한 코드를 확인해보자.

 

public class ObjectMapper extends ObjectCodec implements Versioned, Serializable {
    protected final ConfigOverrides _configOverrides;
    //...

    public ObjectMapper() {
        this((JsonFactory)null, (DefaultSerializerProvider)null, (DefaultDeserializationContext)null);
    }

    public ObjectMapper(JsonFactory jf, DefaultSerializerProvider sp, DefaultDeserializationContext dc) {
       //...
        this._configOverrides = new ConfigOverrides();
        //...
    }
}

 

public class ConfigOverrides implements Serializable {
    public ConfigOverrides() {
        this((Map)null, 
            Value.empty(),
            com.fasterxml.jackson.annotation.JsonSetter.Value.empty(),
            Std.defaultInstance(), 
            (Boolean)null);
    }
}

 

public interface VisibilityChecker<T extends VisibilityChecker<T>> {
    public static class Std implements VisibilityChecker<VisibilityChecker.Std>, Serializable {
        protected static final VisibilityChecker.Std DEFAULT;
        //...

        public static VisibilityChecker.Std defaultInstance() {
            return DEFAULT;
        }

        static {
            DEFAULT = new VisibilityChecker.Std(
                Visibility.PUBLIC_ONLY, 
                Visibility.PUBLIC_ONLY, 
                Visibility.ANY, 
                Visibility.ANY, 
                Visibility.PUBLIC_ONLY);
        }

        public Std(Visibility getter, Visibility isGetter, Visibility setter, Visibility creator, Visibility field) {
            this._getterMinLevel = getter;
            this._isGetterMinLevel = isGetter;
            this._setterMinLevel = setter;
            this._creatorMinLevel = creator;
            this._fieldMinLevel = field;
        }        
    }
}

 

VisibilityChecker.Std 내의 설정을 확인해보면

필드는 접근한정자가 public일 경우에만 ObjectMapper가 동작하도록 설정되어 있는 것을 볼 수 있다.

 

여기에서 앞서 정의한 Dto 클래스를 다시 살펴볼까?

 

import lombok.Data;

@Data
public class MyDto {
	private String oWEIRDField;
    // ...
}

 

아무런 설정을 하지 않은 우리 프로젝트의 ObjectMapper에서 타겟이 될 만한 구석은

Setter, Getter 그리고 파라미터가 없는 빈 생성자 정도가 되겠다.

 

... 롬복에 의해서 생기는 Getter의 상태가,,??

import lombok.Data;

@Data
public class MyDto {

	private String oWIERDField;
    
        /**
         * 롬복에 의해 생성되는 Getter
         * public String getOWIERDField() {
         *     return oWIERDField;
         * }
         */
}

 

Getter명을 기준으로 필드명을 유추하는 알고리즘이 내 생각과는 다르게 동작하는 것 같다 허허

이 부분을 수정하는 것 보다는 Getter대신 필드명 기준으로 직렬화/역직렬화가 되도록 처리를 하고 싶다.

 

2가지 방법이 있을 것 같다.

 

1. Visibility를 커스터마이징 한 ObjectMapper Bean을 등록해 MappingJackson2HttpMessageConverter Bean의 멤버로 주입

2. MappingJackson2HttpMessageConverter Bean 설정을 직접하여 커스터마이징한 ObjectMapper 인스턴스를 주입

 

 

현재 프로젝트에서는 ObjectMapper를 이미 빈으로 등록해서 서비스 레이어에서 사용중인 케이스가 존재했는데,

빈으로 등록되어 있는 ObjectMapper에 설정을 하면 로직에 영향을 줄 수 있으니,, MappingJackson2HttpMessageConverter Bean을 새로 설정하는 편이 좋아보인다.

 

 

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.VisibilityChecker;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

@Configuration
public class JacksonConfig {

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        ObjectMapper objectMapper = new ObjectMapper();

        // VisibilityChecker.Std를 사용하여 메서드 체이닝으로 설정
        objectMapper.setVisibility(VisibilityChecker.Std.defaultInstance()
            .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
            .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
            .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
            .withIsGetterVisibility(JsonAutoDetect.Visibility.NONE)
            .withCreatorVisibility(JsonAutoDetect.Visibility.NONE)
        );

        MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
        jsonConverter.setObjectMapper(objectMapper);

        return jsonConverter;
    }
}

 

 

다소 (중략)이 많지만 이렇게 HttpMessageConverter나 Spring MVC 구조에 대해서 공부하면서 해결을 할 수 있었다.

 

오늘 겪었던 문제 외에 최근에 프로젝트를 새로 설정하면서 수행한 Spring Security관련 설정이나 @RestControllerAdvice를 이용한 공통 예외처리 및 응답 공통 처리 관련해서도 빠른 시일내에 포스팅해야겠다,,