1. 문제
이번에 기존 모듈에서 관리하던 enum을 별도의 공통 모듈로 분리하게 되었다.
원래는 해당 enum 클래스가 api의 request, response DTO에 들어가게 되는 경우를 고려해 enum이 UPPER_SNAKE_CASE가 아닌, 기존에 정해진 컨벤션대로 snake_case를 준수하게 하기 위해 @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)를 적용해 사용하고 있었다.
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
enum class Status {
ACTIVE,
INACTIVE,
}
하지만, 공통 모듈로 이 enum class를 이동하면서부터는 보편성을 위해 공통모듈에 이 @JsonNaming 어노테이션을 추가하지 않았다.
따라서, 기존 모듈에서 이 공통 모듈을 적용할 때 별도 처리를 해주지 않는다면, api payload에도 모두 대문자로 나가게 된다.
해결 방법으로 몇 가지 선택지를 떠올려 보았다.
1) @JsonProperty를 직접 사용 (비효율적)
enum class Status {
@JsonProperty("active_status")
ACTIVE,
@JsonProperty("inactive_status")
INACTIVE;
}
- 각 항목에 @JsonProperty를 지정하는 방법.
- 공통 모듈을 수정해야 하므로 불가능.
2) ObjectMapper의 PropertyNamingStrategies 설정 (전체 적용)
@Bean
fun objectMapper(): ObjectMapper = ObjectMapper().apply {
propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE
}
- 전체적으로 snake_case를 적용하지만, 특정 클래스만 예외적으로 설정하기 어렵다.
- 공통 모듈의 다른 클래스에도 영향을 줄 수 있음.
고민과 여러 비교 끝에, 이러한 문제를 해결할 방법으로 Jackson Mixin을 고려하게 되었다.
2. Jackson Mixin이란?
Spring Boot에서는 Jackson을 기본 JSON 직렬화/역직렬화 라이브러리로 사용한다.
일반적으로 Jackson의 애너테이션(@JsonProperty, @JsonIgnore, @JsonNaming 등)은 직렬화 대상 클래스에 직접 선언한다.
하지만 클래스 수정이 불가능하거나, 공통 모듈 등에서 직접 애너테이션을 적용할 수 없는 경우, Mixin을 활용하면 기존 클래스를 수정하지 않고도 Jackson 설정을 적용할 수 있다.
Mixin은 특정 클래스에 대한 Jackson 설정을 별도의 클래스에 정의한 후, ObjectMapper에서 이를 연결하는 방식으로 동작한다.
Jackson Mixin은 원래부터도 내부 라이브러리 또는 서드파티 라이브러리의 클래스를 수정하지 않고도 Jackson의 직렬화/역직렬화 설정을 적용하기 위해 등장했기 때문에, 이를 활용하면 공통 모듈과 같이 직접 수정이 어려운 구조에서도 유연한 JSON 변환이 가능하다.
3. Jackson Mixin 적용하기
1) Mixin 정의
Mixin 클래스에서는 원본 클래스의 구조를 유지하면서 필요한 Jackson 애너테이션만 추가한다.
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
abstract class StatusMixin
혹은, Serializer, Deserializer를 정의하고 MixIn으로 연결해주는 방법도 가능하다.
class StatusSerializer : JsonSerializer<Status>() {
override fun serialize(value: Status?, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(value?.name?.lowercase(Locale.getDefault()))
}
}
class StatusDeserializer : JsonDeserializer<Status>() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Status {
return Status.valueOf(p.text.uppercase(Locale.getDefault()))
}
}
abstract class StatusMixin {
@JsonSerialize(using = StatusSerializer::class)
@JsonDeserialize(using = StatusDeserializer::class)
abstract fun getStatus(): Status
}
2) ObjectMapper에 Mixin 등록
Spring의 Jackson2ObjectMapperBuilderCustomizer를 사용해 Mixin을 등록한다. (주로 JacksonConfig 내에서 관리)
@Bean
fun objectMapperCustomizer(): Jackson2ObjectMapperBuilderCustomizer {
return Jackson2ObjectMapperBuilderCustomizer { builder ->
builder.mixIn(Status::class.java, StatusMixin::class.java)
}
}
이렇게 하면 Status enum을 사용할 때 자동으로 snake_case가 적용된다.
4. Jackson Mixin의 장단점
[장점]
- 원본 클래스를 수정하지 않고도 직렬화 설정 가능
- 특정 클래스에만 선택적으로 적용 가능 (전역 설정의 단점 극복)
- 공통 모듈 분리 시에도 유연한 직렬화 유지 가능
- 라이브러리나 외부 모듈의 클래스를 수정하지 않고도 원하는 Jackson 설정을 적용 가능
[단점]
- 직렬화/역직렬화 설정이 별도의 Mixin 클래스에 있기 때문에, 코드를 직관적으로 이해하기 어려움
- Mixin 적용 시 리플렉션이 사용되므로, 애너테이션보다 약간의 성능 저하가 발생할 수 있음
우리 프로젝트에서는 장점이 단점을 상회한다고 판단하였기에 MixIn을 도입했다.
프로젝트의 특성에 맞게 사용하면 될 것 같다.
'Backend' 카테고리의 다른 글
[Kotlin] 성능을 고려한 Util 설계하기 (feat. 정규식) (1) | 2025.03.22 |
---|---|
Spring MVC vs. WebFlux (feat. Kotlin) (0) | 2025.03.01 |
스프링부트, 빈의 Lazy 로딩 (@Lazy) (0) | 2025.02.16 |
DB Connection Pool에 대해 (0) | 2025.01.31 |
사람들은 왜 자바가 아닌 코틀린에 열광할까? (0) | 2025.01.05 |