TWTW - ObjectMapper

진주원(JooWon Jin)·2023년 9월 14일
0

TWTW

목록 보기
1/8
post-thumbnail

2개 이상의 ObjectMapper 사용

문제 설명

Untitled

  • 구체적인 지명에 대한 정보는 Kakao Maps API를 사용하고 자동차 경로에 대한 정보는 Naver Maps API를 사용해야 하는 상황이다.

    • 각각의 WebClientConfig에서는 서로 다른 설정의 ObjectMapper를 주입받아 사용한다.
      • Kakao Maps API는 SNAKE_CASE
      • Naver Maps API는 CAMEL_CASE
  • ObjectMapper란

    JSON 형식에 대하여 응답을 직렬화하고 요청을 역직렬화 할 때 사용하는 기술

ObjectMapper 타입의 Bean을 2개 등록하기 때문에 주입을 받는 대상(WebClient)에서 해당 Bean에 대한 정보를 알아야 한다.

2개 이상의 동일한 타입의 빈 - @Qualifier

  • @Qualifier
    • @Qualifier 어노테이션을 사용하여 별도의 명칭을 정해주면 2개 이상의 동일한 타입의 빈에 대하여 구분하여 주입을 할 수 있다.

Spring @Qualifier Annotation

문제가 발생한 코드

  1. KakaoWebClientConfig
@Configuration
public class KakaoWebClientConfig {
    private static final String HEADER_PREFIX = "KakaoAK ";
    private final ObjectMapper objectMapper;
    
    public KakaoWebClientConfig(@Qualifier("kakaoObjectMapper") ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Bean(name = "KakaoWebClient")
    public WebClient webClient(
            @Value("${kakao-map.url}") final String url,
            @Value("${kakao-map.key}") final String authHeader) {
        final ExchangeStrategies exchangeStrategies =
                ExchangeStrategies.builder()
                        .codecs(
                                configurer -> {
                                    configurer.defaultCodecs().maxInMemorySize(-1);
                                    configurer
                                            .defaultCodecs()
                                            .jackson2JsonDecoder(
                                                    new Jackson2JsonDecoder(objectMapper));
                                })
                        .build();

        return WebClient.builder()
                .exchangeStrategies(exchangeStrategies)
                .baseUrl(url)
                .defaultHeader(HttpHeaders.AUTHORIZATION, HEADER_PREFIX + authHeader)
                .build();
    }
}
  1. NaverWebClientConfig
@Configuration
public class NaverWebClientConfig {
    private static final String HEADER_CLIENT_ID = "X-NCP-APIGW-API-KEY-ID";
    private static final String HEADER_CLIENT_SECRET = "X-NCP-APIGW-API-KEY";
    private final ObjectMapper objectMapper;

    @Autowired
    public NaverWebClientConfig(@Qualifier("naverObjectMapper") ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Bean(name = "NaverWebClient")
    public WebClient webClient(
            @Value("${naver-map.url}") final String url,
            @Value("${naver-map.id}") final String clientId,
            @Value("${naver-map.secret}") final String secretKey) {

        final ExchangeStrategies exchangeStrategies =
                ExchangeStrategies.builder()
                        .codecs(
                                configurer -> {
                                    configurer.defaultCodecs().maxInMemorySize(-1);
                                    configurer
                                            .defaultCodecs()
                                            .jackson2JsonDecoder(
                                                    new Jackson2JsonDecoder(objectMapper));
                                })
                        .build();

        return WebClient.builder()
                .exchangeStrategies(exchangeStrategies)
                .baseUrl(url)
                .defaultHeader(HEADER_CLIENT_ID, clientId)
                .defaultHeader(HEADER_CLIENT_SECRET, secretKey)
                .build();
    }
}
  1. KakaoObjectMapperConfig
@Configuration
public class KakaoObjectMapperConfig {

    @Qualifier("kakaoObjectMapper")
    @Bean
    public ObjectMapper kakaoObjectMapper() {
        return new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
                .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
    }
}
  1. NaverObjectMapperConfig
@Configuration
public class NaverObjectMapperConfig {

    @Qualifier("naverObjectMapper")
    @Bean
    public ObjectMapper naverObjectMapper() {
        return new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
                .setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE);
    }
}

Error Code

Parameter 0 of method mappingJackson2HttpMessageConverter in org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration$MappingJackson2HttpMessageConverterConfiguration required a single bean, but 2 were found:
	- kakaoObjectMapper: defined by method 'kakaoObjectMapper' in class path resource [com/twtw/backend/config/mapper/KakaoObjectMapperConfig.class]
	- naverObjectMapper: defined by method 'naverObjectMapper' in class path resource [com/twtw/backend/config/mapper/NaverObjectMapperConfig.class]
  • 위 에러 코드는 2개 이상의 동일한 Bean 타입에 대해 무엇을 주입해서 사용해야 할지 모를 때 나오는 것이다 …..

분명히 각 WebClientConfig와 ObjectMapperConfig 파일에 @Qualifier 어노테이션을 사용하여 주입 대상을 명시해주었다.. 그렇다면 위 에러 코드는 왜 발생한 것일까????

  • 에러코드를 유심히 보자 오류가 발생한 대상이 무엇인가?
    • mappingJackson2HttpMessageConverter

MappingJackson2HttpMessageConverter

public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {

	private static final List<MediaType> problemDetailMediaTypes =
			Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON);

	@Nullable
	private String jsonPrefix;

	/**
	 * Construct a new {@link MappingJackson2HttpMessageConverter} with a custom {@link ObjectMapper}.
	 * You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
	 * @see Jackson2ObjectMapperBuilder#json()
	 */
	public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
		super(objectMapper, MediaType.APPLICATION_JSON, new MediaType("application", "*+json"));
	}
}
  • Jackson ObjectMapper를 사용할 경우 위와 같은 클래스를 사용한다.
  • 생성자를 유심히 살펴보면 ObjectMapper 타입의 파라미터를 주입받는 것을 알 수 있다.

Why?

  • @Qualifier 어노테이션을 사용할 경우 주입을 받아 사용하는 대상에서도 똑같이 어노테이션을 명시한다면 해당 빈을 우선적으로 파악하여 주입이 정상적으로 수행될 것이다.
  • 하지만 @Qualifier 어노테이션이 명시되어 있지 않은 대상이라면???
    • 2가지 이상의 주입 빈에 대한 우선순위를 결정하지 못한다.

해결 방법

  • @Primary
    • 2개 이상의 동일한 타입의 빈에 대한 구분을 해주는 방법에는 여러가지가 있다.
      • Field Name
      • @Qualifier
      • @Primary
    • @Primary 어노테이션을 사용한다면 해당 빈을 우선적으로 주입받아 사용한다.
      • @Qualifier보다는 우선순위가 뒤에 있지만 위의 문제 경우에는 @Qualifier가 적용될 수 없기 때문에 @Primary의 사용이 더 올바른 방법이다.
  • 2개의 ObjectMapper에 대하여 1개의 ObjectMapper에 @Primary 어노테이션을 사용한다.
@Configuration
public class KakaoObjectMapperConfig {
    @Primary
    @Qualifier("kakaoObjectMapper")
    @Bean
    public ObjectMapper kakaoObjectMapper() {
        return new ObjectMapper()
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
                .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
                .setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
    }
}
profile
Young , Wild , Free

0개의 댓글