예전 포스팅에서 Java 직렬화(Serialization) 를 간단히 정리한 적이 있다.
그 때 정리한 내용이 직렬화/역직렬화가 무엇잇가에 대한 기본적인 내용이라면 이번엔 좀 더 상세히 풀어보려고 한다.

먼저 내가 하려고 하는 것을 간단히 그림으로 그려보았다.

일반적인 SpringBoot 프로젝트라면 Spring에서 기본으로 제공하는 Jackson(com.fasterxml.jackson)을 사용하면 된다.

근데 나는 kotlin + boot로 프로젝트를 구성했고 kotlin에는 kotlinx.serialization 이라는 공식 직렬화가 존재하기 때문에 어떤 것을 사용할지 고민을 했다.

kotlinx.serialization가 Jackson과의 가장 큰 차이점은 컴파일 타임 최적화로 리플렉션을 사용하지 않아 상대적으로 빠르다는 것이다.

결국 서비스 내부 처리 로직에서는 kotlinx.serialization을 사용하고 Mongo DB 로의 저장과 같은 Spring Data Mongo가 개입하여 Jackson을 사용하는 곳에서는 kotlinx.serialization를 사용하지 않기로 결정했다.

문제

근데 실제 어플리케이션은 위의 예시보다 항상 복잡하고 나 역시 문제에 봉착했는데 조금 더 구체화해보면 데이터의 구조는 아래와 같다.

json string이 항상 같은 형식이 아니라 type에 따라 type_meta의 포맷이 바뀔 수 있는 구조이다. 즉 다형성이 보장되어야 한다.

해결

그래서 kotlinx.serialization 에서는 직렬화시에 다형성을 보장할 수 있도록 @Polymorphic 이라는 어노테이션을 제공한다.
그리고 아래처럼 로직에서 json의 설정부에 다형성 클래스 구조의 subclass를 선언하여 직렬화시 참고하도록 알려주면 된다.

val json = Json {
                  ignoreUnknownKeys = true
                  classDiscriminator = "clip_type"
                  serializersModule = SerializersModule {
                                polymorphic(People::class) {
                                    subclass(Director::class)
                                    subclass(Actor::class)
                                }
                  }
            }

이렇게 역직렬화 문제는 해결했다.

또 다른 문제

그리고 곧바로 두번째 문제가 생겼는데 바로 Mongo DB에서 데이터를 조회하는 하는 부분이었다.

Mongo DB에서 find시 Movie 객체에 값을 맵핑해줘야 하는데, sealed class인 TypeMeta에 어떤 값을 맵핑해줘야 하는지 알 수 없다는 것이다.

Mongo DB의 기본 MappingMongoConverter는 TypeMeta가 추상클래스로 간주되기 때문에 직접 인스턴스화가 불가능하다.

해결

따라서 CustomConverter를 구현해서 MongoDB가 TypeMeta를 적절한 서브클래스로 변환하도록 직접 컨버터를 등록해줘야 한다.

// custom converter 생성
@Component
@ReadingConverter
class TypeMetaReader : Converter<Document, TypeMeta> {
    override fun convert(source: Document): TypeMeta? {
        val type = source.getString("type") ?: return null
        return when (type) {
            "DIRECTOR" -> objectMapper.convertValue(source, DIRECTOR::class.java)
            "ACTOR" -> objectMapper.convertValue(source, ACTOR::class.java)
            else -> throw IllegalArgumentException("Unknown clip_type: $type")
        }
    }
}

// custom converter 등록
@Bean
fun configureMongoConverter(): MappingMongoConverter {
    mappingMongoConverter.setCustomConversions(MongoCustomConversions(listOf(TypeMetaReader())))
    mappingMongoConverter.afterPropertiesSet()
    return mappingMongoConverter
}

여기까지 해주면 Mongo DB 조회시에도 다형성으로 구성된 Document 클래스로의 정상적인 맵핑이 가능해진다.

결론

직렬화를 위해 kotlinx.serialization을 쓸지 Jackson을 쓸지 고민해보면서 좋은 공부가 되었다.

다만 다형성을 지원하기 위해 SerializersModule을 설정하고 conveter를 만드는 것이 최적의 해결법인지는 모르겠다. 새로운 데이터 type이 생기면 해당 serializersModule과 converter까지 수정해줘야하기 때문이다.

길은 하나가 아니다. 다만 최적의 길을 찾는 것은 개발자 스스로의 몫이다.
결국은 내가 아는 만큼 보이고 아는 만큼 개발에 적용할 수 있다는 것을 또 한번 깨닫게 되었다.

profile
Java 백엔드 개발자입니다. 제가 생각하는 개발자로서 가져야하는 업무적인 기본 소양과 현업에서 가지는 고민들을 같이 공유하고 소통하려고 합니다.

0개의 댓글