사실이 아니라 공부한 내용과 생각을 정리한 글입니다. 언제든 가르침을 주신다면 감사하겠습니다.
1. jackson 역직렬화 기본 동작과 kotlin data class
1-1. jackson 역직렬화 기본 동작
1. 기본생성자가 있을 때
- 기본생성자를 호출해 객체를 생성 후 setter를 통해 필드를 초기화한다.
2. 기본생성자가 없을 때
- 역직렬화 대상 객체의 필드 이름을 파라미터로 받는 생성자가 있는지 찾아본다.
- 필드 이름을 파라미터로 받는 생성자가 있다면, 해당 생성자를 통해 객체를 생성 후 아직 초기화되지 않는 필드는 setter를 통해 초기화한다.
- 이 경우에는 @JsonCreator와 @JsonProperty를 통해 역직렬화를 위한 생성자와 필드를 명시해줘야 한다.
3. 기본생성자도 없고 필드 이름을 파라미터로 받는 생성자도 없을 때
- 역직렬화에 실패하고 예외를 던진다.
3번 경우 정말 역직렬화에 실패하는지 테스트코드를 작성해 보았다.
public class JacksonBasicTest {
private ObjectMapper objectMapper = new ObjectMapper();
@Test
public void fail() throws JsonProcessingException {
MakeAccountDtoCommand command = new MakeAccountDtoCommand("soungho", "1234");
ImmutableAccountDto givenAccountDto = new ImmutableAccountDto(command);
String json = objectMapper.writeValueAsString(givenAccountDto);
InvalidDefinitionException exception = assertThrows(InvalidDefinitionException.class, () -> {
objectMapper.readValue(json, ImmutableAccountDto.class);
});
assertEquals(exception.getMessage(), "Cannot construct instance of `com.soungho.studyolle.JacksonBasicTest$ImmutableAccountDto` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)\n" +
" at [Source: (String)\"{\"email\":\"soungho\",\"password\":\"1234\"}\"; line: 1, column: 2]");
}
public static class ImmutableAccountDto {
private String email;
private String password;
ImmutableAccountDto(MakeAccountDtoCommand command) {
this.email = command.getEmail();
this.password = command.getPassword();
}
public String getEmail() {
return this.email;
}
public String getPassword() {
return this.password;
}
}
public static class MakeAccountDtoCommand {
private String email;
private String password;
MakeAccountDtoCommand(String email, String password) {
this.email = email;
this.password = password;
}
public String getEmail() {
return this.email;
}
public String getPassword() {
return this.password;
}
}
}
테스트가 성공했다는 것은 역직렬화 과정에서 예외가 발생했다는 의미이다. 예외의 내용은 아래와 같다.
Cannot construct instance of `com.soungho.studyolle.JacksonBasicTest$ImmutableAccountDto` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"email":"soungho","password":"1234"}"; line: 1, column: 2]
대충 해석해 보면, 역직렬화를 위해서는 대상 객체를 생성해야 하는데 이를 위한 마땅한 생성자를 찾지 못해서 실패했다는 내용이다.
1.2 kotlin data class와 jackson 역직렬화
그렇다면 kotlin data class의 경우 jackson을 사용한 역직렬화가 잘 될까?
테스트코드를 작성해 봤다.
class JavaJacksonTest {
private val javaObjectMapper = ObjectMapper()
@Test
fun `자바 objectMapper 의 data class 역직렬화는 실패한다`() {
val givenAccountDto = AccountDto("soungho", "1234")
val jsonAccountDto = javaObjectMapper.writeValueAsString(givenAccountDto)
val thrownException = assertThrows<InvalidDefinitionException> {
javaObjectMapper.readValue(jsonAccountDto, AccountDto::class.java)
}
assertEquals(thrownException.message, "Cannot construct instance of `com.soungho.studyolle.JavaJacksonTest\$AccountDto` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)\n" +
" at [Source: (String)\"{\"email\":\"soungho\",\"password\":\"1234\"}\"; line: 1, column: 2]")
}
data class AccountDto(
val email: String,
val password: String
)
}
테스트가 성공했다는 것은 역직렬화 과정에서 예외가 발생했다는 의미이다. 예외의 내용은 아래와 같다.
Cannot construct instance of `com.soungho.studyolle.JavaJacksonTest$AccountDto` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (String)"{"email":"soungho","password":"1234"}"; line: 1, column: 2]
여기서도 마찬가지로 역직렬화에 사용할 생성자를 찾지 못해 예외가 발생했다!
data class는 기본 생성자를 만들지 않고 모든 프로퍼티를 갖는 생성자 하나를 만들기 때문에 @JsonCreator와 @JsonProperty 없이는 역직렬화하지 못한다.
참고로 위 테스트코드에서 AccountDto를 아래와 같이 수정하면 역직렬화에 성공한다.
data class AccountDto(
@JsonProperty("email")
val email: String,
@JsonProperty("password")
val password: String
)
그렇다면, kotlin에서 data class에 대한 역직렬화가 필요할 때마다 @JsonProperty와 같은 부가 어노테이션을 사용해야 할까? 이러한 부가 어노테이션 사용은 kotlin 이 추구하는 간결성에 반하는 요소이고 다행히도 kotlin에서는 이에 대한 해결책을 제공한다.
2. kotlin에서 jackson 역직렬화
fun jacksonObjectMapper(): ObjectMapper = jsonMapper { addModule(kotlinModule()) }
class KotlinJacksonTest {
private val kotlinObjectMapper = jacksonObjectMapper()
@Test
fun `코틀린 objectMapper 의 data class 역직렬화는 성공한다`() {
val givenAccountDto = AccountDto("soungho", "1234")
val jsonAccountDto = kotlinObjectMapper.writeValueAsString(givenAccountDto)
val parsedAccountDto = kotlinObjectMapper.readValue(jsonAccountDto, AccountDto::class.java)
assertEquals(parsedAccountDto.email, givenAccountDto.email)
}
data class AccountDto(
val email: String,
val password: String
)
}
이 경우에는 data class에 대해 @JsonProperty와 같은 부가 어노테이션 없이도 역직렬화에 성공하는 것을 확인할 수 있다.
즉, kotlin에서 data class에 대한 역직렬화도 자연스럽고 간결하게 사용할 수 있도록 jackson과 관련한 extension을 마련해 둔 것을 알 수 있다.
그렇다면 코프링 환경에서는 어떨까?
spring 환경에서 jackson을 사용하는 가장 대표적인 요소는 controller에서 json으로 들어온 요청을 객체로 역직렬화하는 부분이다.
아래에서 이 부분에 대해 kotlin과 spring 이 어떻게 통합되었는지 살펴보자.
3. spring 과의 통합
spring-boot-starter-web을 의존성으로 가지면 spring 애플리케이션 기동 시 Jackson2ObjectMapperBuilder#configure 메서드가 호출되면서 http message를 파싱 하기 위한 objectMapper에 대한 설정이 적용된다. configure 코드를 살펴보면 아래와 같은 코드가 있다.
public void configure(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
MultiValueMap<Object, Module> modulesToRegister = new LinkedMultiValueMap<>();
if (this.findModulesViaServiceLoader) {
ObjectMapper.findModules(this.moduleClassLoader).forEach(module -> registerModule(module, modulesToRegister));
}
else if (this.findWellKnownModules) {
registerWellKnownModulesIfAvailable(modulesToRegister);
}
// ... 생략
}
registerWellKnownModulesIfAvailable 메서드를 따라가 보면 아래와 같은 코드가 나온다.
private void registerWellKnownModulesIfAvailable(MultiValueMap<Object, Module> modulesToRegister) {
// ... 생략
// Kotlin present?
if (KotlinDetector.isKotlinPresent()) {
try {
Class<? extends Module> kotlinModuleClass = (Class<? extends Module>)
ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", this.moduleClassLoader);
Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass);
modulesToRegister.set(kotlinModule.getTypeId(), kotlinModule);
}
catch (ClassNotFoundException ex) {
// jackson-module-kotlin not available
}
}
}
spring 기동시에 kotlin 환경으로 기동하고 있는지 확인해서 kotlin 환경이라면 kotlin 모듈을 modulesToRegister에 등록하는 것을 확인할 수 있다.
이렇게 modulesToRegister에 등록된 모듈들은 아래 코드에서 실제 objectMapper에 적용된다.
public void configure(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
MultiValueMap<Object, Module> modulesToRegister = new LinkedMultiValueMap<>();
if (this.findModulesViaServiceLoader) {
ObjectMapper.findModules(this.moduleClassLoader).forEach(module -> registerModule(module, modulesToRegister));
}
else if (this.findWellKnownModules) {
registerWellKnownModulesIfAvailable(modulesToRegister);
}
if (this.modules != null) {
this.modules.forEach(module -> registerModule(module, modulesToRegister));
}
if (this.moduleClasses != null) {
for (Class<? extends Module> moduleClass : this.moduleClasses) {
registerModule(BeanUtils.instantiateClass(moduleClass), modulesToRegister);
}
}
List<Module> modules = new ArrayList<>();
for (List<Module> nestedModules : modulesToRegister.values()) {
modules.addAll(nestedModules);
}
objectMapper.registerModules(modules); // <- 여기서 적용!!!
// ... 생략
}
objectMapper.registerModules(modules) 코드를 발견하였는가?
결국 spring-boot-starter-web 의존성이 있다면, spring 이 기동 되는 시점에 objectMapper를 생성하면서 kotlin extension을 적용하는 것을 알 수 있다.
참고
- https://shanepark.tistory.com/364
- https://www.baeldung.com/kotlin/jackson-kotlin
- https://velog.io/@park2348190/Jackson-ObjectMapper%EC%97%90%EC%84%9C-%EA%B8%B0%EB%B3%B8-%EC%83%9D%EC%84%B1%EC%9E%90-%EC%97%86%EC%9D%B4-Deserialization-%ED%95%98%EA%B8%B0
- https://kapentaz.github.io/java/Builder%EB%A1%9C-Jackson-Deserialize%ED%95%98%EA%B8%B0/#
'kotlin' 카테고리의 다른 글
[Kotlin in Action] 7.5: by 키워드를 통해 위임 프로퍼티 우아하게 구현하기 (0) | 2023.05.29 |
---|---|
[Kotlin in Action] 6장: 코틀린 타입 시스템 > 6.1 널과 관련한 이야기 (0) | 2023.03.18 |
[Kotlin in Action] 5장: 람다로 프로그래밍 (0) | 2023.03.05 |
[Kotlin in Action] 4장: 클래스, 객체, 인터페이스 (0) | 2023.02.11 |
[Kotlin in Action] 3장: 함수의 정의와 호출 (0) | 2023.02.08 |
댓글