본문 바로가기
스프링부트

jackson for kotlin part3. 삽질기 (feat. jacksonObjectMapper)

by pius712 2024. 3. 10.

 

https://pius712.tistory.com/19

 

jackson for kotlin part2. 커스터마이징

앞선 글에 이어서, 추가로 커스터마이징에 대해서 글을 쓰려고 한다. https://pius712.tistory.com/11 jackson for kotlin part1. 동작 원리 kotlin 을 사용하면, kotlin 용의 jackson library를 설치해야한다. implementation(

pius712.tistory.com

 

위 글을 쓰면서, 잠깐 삽질을 했었다. 

 

글을 쓰면서, 동작 확인차 테스트 코드를 만들어서 돌리는데 설정이 안먹히는 것이었다. 

 

 

문제의 발단

serialize 를 설정해놓고 아래와 같이 테스트 코드를 돌렸다. 테스트 실패.

// 설정 
@Configuration
class SerializerConfig {
    @JsonComponent
    class VendorSerializer : JsonSerializer<Vendor>() {
        override fun serialize(
            value: Vendor,
            gen: JsonGenerator,
            serializers: SerializerProvider
        ) {
            gen.writeStartObject()
            gen.writeFieldName("vendor")
            gen.writeString(value.name.lowercase())
            gen.writeEndObject()
        }
    }
}

// 테스트 실패
@Test
fun `vendor serializer`() {
    val writeValueAsString = jacksonObjectMapper()
		    .writeValueAsString(Vendor.KAKAO_PAY)
    Assertions.assertThat(writeValueAsString)
			    .isEqualTo("""{"vendor":"kakao_pay"}""")
}

문제의 원인을 추측해보고자 여러가지 시도를 해보았다.

  1. 테스트가 SpringBoot 테스트를 안해서 그렇군?
  2. @SpringBootTest 추가 ⇒ 실패
  3. 이렇게 설정하는 게 아닌가? 그 외의 설정방식들 ⇒ 실패
  4. controller 응답 ⇒ 성공
  5. objectMapper 주입 ⇒ 성공

아.. jacksonObjectMapper 가 범인이구나?

jacksonObjectMapper 에는 적용이 안돼 - 도대체 왜?

테스트에서 objectMapper 를 inject 받았을때와 kotlin 의 jacksonObjectMapper를 사용할때 동작이 다르게 나타난다.

  1. object mapper 주입 - 성공
@SpringBootTest()
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class JacksonTest(
    val objectMapper: ObjectMapper
) {

	// 테스트 성공
	@Test
	fun `vendor serializer`() {
    val writeValueAsString = objectMapper.writeValueAsString(Vendor.KAKAO_PAY)
    Assertions.assertThat(writeValueAsString).isEqualTo("""{"vendor":"kakao_pay"}""")
	}
}
  1. jacksonObjectMapper 사용 - 실패
// 테스트 실패
@Test
fun `vendor serializer`() {
    val writeValueAsString = jacksonObjectMapper()
			    .writeValueAsString(Vendor.KAKAO_PAY)
    Assertions.assertThat(writeValueAsString)
		    .isEqualTo("""{"vendor":"kakao_pay"}""")
}

왜 안될까? 비밀은 jacksonObjectMapper 자체에 있다.

우선 이걸 알기전에 어떻게 컨트롤러에서는 되는지 알아봐야하는데, 이는 ObjectMapper 가 어떻게 spring context에 등록되는지 알아야한다.

참고

https://pius712.tistory.com/11

https://pius712.tistory.com/19

우선, object mapper 는 스프링의 auto configuration 과정에서, kotlin module 을 등록을 하게 된다.

@JsonComponent 로 등록한 클래스의 경우, JsonComponentModule 이 등록될 때, 해당 어노테이션을 추가한 serializer, deserializer 들이 모두 등록이 된다.

그래서 controller 에서 response 가 내려갈 때는 HttpMessageConverter 의 Jackson Conveter 가 동작하게 되는 것이다.

그렇다면 왜 jacksonObjectMapper() 로 만든 매퍼는 동작하지 않을까?

jacksonObjectMapper 해부

jacksonObjectMapper 함수 코드를 보자. 벌써부터 감이 온다.

JsonMapper 빌더를 통해서 인스턴스를 생성한다. spring context 의 objectMapper 에서 가져오는 것이 아니다.

fun jsonMapper(initializer: JsonMapper.Builder.() -> Unit = {}): JsonMapper {
    val builder = JsonMapper.builder()
    builder.initializer()
    return builder.build()
}

fun jacksonObjectMapper(): ObjectMapper 
		= jsonMapper { addModule(kotlinModule()) }

애초에, jackson autoconfiguration 에서 등록되는 jackson2ObjectMapper 로 생성되는 objectMapper와 다른 녀석이다.

LoggerFactory 의 경우, spring context 에 캐싱된 로거 인스턴스를 가져오기 때문에 이것도 비슷하게 동작할 것이라고 착각했던 것이다.

JsonMapper 는 Spring context 와 관련이 없고, jacksonObjectMapper는 jackson core 라이브러리의 JsonMapper 를 인스턴스화하면서 kotlin 모듈만 등록하는 함수였던 것이다.

즉, serialize 는 spring context 에 등록을 해서 싱글톤 objectMapper를 커스터마이징 하는 것이 었는데,

jacksonObjectMapper 는 별개의 인스턴스를 생성하던 것이었다. 그래서 serialize가 먹히지 않았던 것이다.