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"}""")
}
문제의 원인을 추측해보고자 여러가지 시도를 해보았다.
- 테스트가 SpringBoot 테스트를 안해서 그렇군?
- @SpringBootTest 추가 ⇒ 실패
- 이렇게 설정하는 게 아닌가? 그 외의 설정방식들 ⇒ 실패
- controller 응답 ⇒ 성공
- objectMapper 주입 ⇒ 성공
아.. jacksonObjectMapper 가 범인이구나?
jacksonObjectMapper 에는 적용이 안돼 - 도대체 왜?
테스트에서 objectMapper 를 inject 받았을때와 kotlin 의 jacksonObjectMapper를 사용할때 동작이 다르게 나타난다.
- 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"}""")
}
}
- 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가 먹히지 않았던 것이다.
'스프링부트' 카테고리의 다른 글
스프링 AOP - 톺아보기 (1) | 2024.06.06 |
---|---|
나는 테스트에서 @Transactional 붙이지 않겠다.. (feat. 동시성 삽질기) (0) | 2024.05.02 |
jackson for kotlin part2. 커스터마이징 (0) | 2024.03.10 |
영속성 컨텍스트와 트랜잭션의 관계 (1) | 2024.02.18 |
jackson for kotlin part1. 동작 원리 (1) | 2023.12.30 |