-
MSA 환경에서 enum에 대한 위험성 줄여나가기Spring Framework 2024. 11. 16. 16:05
개요
소프트웨어를 개발하다 보면 상수들의 집합을 관리하기 위해 enum을 활용하곤 합니다.
ProductType을 enum으로 관리하게 된다면 여러 상품들을 enum으로 관리할 수 있습니다.
여러 서버에서 ProductType이라는 enum을 관리해야 한다면 어떤 일이 발생할 수 있을까요?
여러 서버에서의 불일치
ProductType의 enum이 두 서버에서 관리된다면 혹은 백개의 서버에서 내부적으로 관리된다면 어떨까요?
새로운 Product가 추가되었을 때 관심사의 밖 혹은 추가하는것을 잊어서 여러 서버에서 차이가 발생하게 됩니다.
여러 서버에서의 불일치가 생기고, 신규 서버가 생성될때도 ProductType의 enum을 매번 추가해줘야 하는 불편함이 있을 것 같습니다.
공통 라이브러리로 관리하자
만약 enum을 공통 라이브러리로 관리하고 여러 서버에서는 해당 라이브러리를 가져오는 방법을 사용해 볼 수 있습니다.
1. 신규 서버가 생성될때도 라이브러리만 추가하면 enum을 컴파일 타임에 활용할 수 있어집니다.
2. 라이브러리 버전업에 따라 최종적 일관성을 보장할 수 있습니다.
두 가지 관점에서 공통 라이브러리를 활용해 보는 것이 합리적으로 보입니다.
하지만 위 그림처럼 여러 서버에서 라이브러리의 버전이 불일치하게 되면 어떨까요?
A서버에서 Product 서버에게 전체 상품을 조회하게 되면 직렬화 예외가 발생할 가능성이 있습니다.
A 서버에서는 모르는 ProductType이 Product 서버에게 json 응답으로 내려오게 될 것이며 이를 직렬화하려다 예외가 발생합니다.
코드로 예제를 보면 아래와 같습니다.
@FeignClient( name = "product", url = "http://localhost:8080", dismiss404 = true, ) interface GetNewProduct { // 버전에 따라 역직렬화 문제가 발생할 가능성이 있음 @GetMapping("/v1/new-product") fun getNewProductWithEnum(): Product // 역직렬화 문제가 발생하지 않음 @GetMapping("/v2/new-product") fun getNewProductWithString(): String }
역직렬화 에러를 막기 위해서는 ProductType을 직접적으로 활용하지 않고 String으로 받은 뒤 enum으로 변환하도록 할 수 있습니다.
fun safeValueOf(name: String): Product { return values().find { it.name == name } ?: 정의되지않음 }
변환하는 과정에서 valueOf를 활용하면 여전히 문제가 발생할 수 있기 때문에 enum에 메서드를 하나 정의하고 활용해야 합니다.
괜찮아 보이는 해결책이지만 우려되는 점이 있습니다.
개발자가 ProductType을 활용할 때 역직렬화 시 문제가 발생할 수 있음을 인지하고 enum 타입이 아닌 String으로 받은 후 safeValueOf 메서드를 호출해야 한다는 것입니다.
코드 리뷰등으로 방어할 수 있겠지만 이는 온전히 해당 기능을 개발하는 개발자의 역량에 의존합니다.
개발자의 역량에 의존하지 않고 시스템이 보장하도록 만들면 어떨까요?
시스템으로 보장하기
enum class Product { PRODUCT_A, PRODUCT_B, @JsonEnumDefaultValue 정의되지않음; }
이를 위해서 jackson 라이브러리는 @JsonEnumDefaultValue 어노테이션을 제공합니다.
이 기능을 통하여 더 이상 역직렬화 문제는 발생하지 않습니다.
다만 ObjectMapper에서 정의되지 않은 enum을 기본값으로 정의하는 옵션 활성화가 필요합니다.
@Bean fun objectMapper(): ObjectMapper{ return jacksonObjectMapper() .enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) }
여러 서버에서는 의도적으로 예외를 발생시키도록 해당 옵션을 disabled 하거나 기본 enum을 받도록 enable 하여 커스텀해볼 수 있습니다.
하지만 100 개의 서버에 모두 ObjectMapper 설정을 추가하기는 번거롭습니다.
추가로 Kafka 등을 활용한다면 추가로 ObjectMapper의 설정을 세팅해주어야 합니다.
두번째 방법으로는 @JsonCreator를 활용하여 ObjectMapper 설정을 하지 않을 수 있습니다.
@JsonCreator는 역직렬화를 수행할 때 이를 구현하고 있는 팩토리에게 위임할 수 있습니다.
enum class Product { PRODUCT_A, PRODUCT_B, @JsonEnumDefaultValue 정의되지않음; companion object{ @JsonCreator @JvmStatic fun safeValueOf(name: String): Product{ // 로깅을 넣거나, System Property로 커스텀 기능 제공 return values().find { it.name == name } ?: 정의되지않음 } } }
@JsonCreator를 활용하게 되면 여러 서버에서 커스텀할 여지는 사라지게 되지만 메서드에 로깅등의 여러 부가기능을 넣을 수 있으며 SystemProperty 등으로 커스텀 기능을 만들 수 있으므로 개인적으로는 더 좋은 선택지라고 생각합니다.
역직렬화의 문제를 해결했지만 여전히 우려되는 점들은 남아있습니다.
역직렬화에 기존에는 예외가 발생하여서 문제가 없었던 코드들이 정의되지 않음으로 세팅되며 내부 로직에 예상하지 못한 동작을 발생시킬 수 있습니다.
예를 들어 ProductType enum을 제어할때 else 등이 정의되어 있다면 잘못 핸들링될 가능성이 있습니다.
하지만 safeValueOf를 활용했을 때도 동일한 문제를 가지며, 기존에 문제가 발생했던 UseCase도 전체 상품을 조회할 때 생기는 버전 불일치 문제였으므로 트레이드오프를 감수할 만하다고 생각합니다.
'Spring Framework' 카테고리의 다른 글
Spring Bean 이름은 왜 소문자로 시작할까? (0) 2024.11.03 분산시스템에서 로깅 트레이싱 전파는 어떻게 이루어질까? (0) 2024.10.26 Spring Boot Distributed Scheduling (0) 2024.06.30 프로젝트에 Feature Flag 적용하기 (0) 2024.06.01 Spring Batch 대신 @Scheduled 활용해보기 (1) 2024.05.08