Post

Enum VS String - 외부 API 요청에 대한 응답 수신 코드로 enum을 쓰는게 좋을까?

상황 1) 외부 API 요청에 대한 응답 수신 코드로 enum을 쓰는게 좋을까?

요약 ) 외부 API 요청에 대한 응답 코드는 String으로 정의하고 enum 변환하는게 더 유연하고 안전하다. (fault tolerance)
enum에 정의 되어 있지 않은 값이 응답 코드로 들어올 수 있기 때문이다. (e.g., 예고 없이 갑자기 추가된 응답 코드)

미정의 응답 코드 수신 시 가능한 동작

enum에 정의되어 있지 않은 값이 응답 코드로 들어왔을 때 동작에 따른 구분은 아래와 같다.

1-1. Exception 발생 (기본 동작)

  • 미정의 코드가 들어왔을 때 기본 동작은 Exception이다.
  • 미정의 응답 코드가 추가되면 API 전체가 실패해야 하는 경우라면, 기본 동작으로 두어도 무방하다.
  • 로깅 : 기본 Exception 메시지에서 @JsonValue 기준 값이 로깅된다.
    1
    2
    3
    4
    
    Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: 
    Cannot deserialize value of type `support.externalclients.point.dto.PgId` from String "95005": 
    not one of the values accepted for Enum class: 
    [80133, 80006]
    

1-2. null 매핑

  • jackson에 READ_UNKNOWN_ENUM_VALUES_AS_NULL 옵션 주면, 미정의 응답 코드가 들어왔을 때 enum field에 null 할당해준다.
  • 로깅 : 되지 않는다.

1-3. default enum value 매핑

  • @JsonEnumDefaultValue로 디폴트 enum value로 매핑할 수 있다.
  • 로깅 : 되지 않는다.
1
2
3
4
public enum EnumType {
    A, B,
    @JsonEnumDefaultValue UNKNOWN;
}

로깅 누락

  • 1-2, 1-3의 경우, 수신한 미정의 응답 코드가 로깅 되지 않는다.
  • 어떤 값으로 들어왔었는지 로그를 통해 확인 할 수 없는데, 문제 시 즉각 대처를 위해 로그는 찍어주는 것이 좋다.

a. 응답 코드 타입을 Enum으로 유지하면서 로깅

  1. 일단 응답을 Map으로 받아서 로깅 후 객체 변환. 즉, [json -> Obj] 바로 매핑하면 유실되니, 로깅을 위해 [json -> Map -> Obj]
  2. Enum Deserializer를 직접 구현해 로깅을 끼워 넣는 방법

b. 응답 코드 타입을 String으로 변경하여 로깅 후 enum 변환

  • 별거 아닌데 번거로운 위 두 가지 방법 사용하는 것 보다, DTO에서 필드의 타입을 Enum이 아닌 String으로 변경하는 방법이 더 낫다.
  • String으로 받는 경우 [json -> Obj] 가 가능하다.
  • DTO를 로깅하고, Domain Model로 변환 할 때 Enum.valueOf(String) 해주면 된다.

BigDecimal도 DTO에서는 String으로 받는 것이 나은 경우가 많은데,
commissionRate = BigDecimal(feeRtN8.insert(1, "."))
이렇게 String->BigDecimal로 변환하면서 추가적인 처리를 해야되는 케이스가 있기 때문이다.

이 처럼 external layer에 대한 DTO 분리는 다양한 상황에서 매우 유용하다.
[Spring] MVC Layering Architecture : DTO와 Domain Model을 분리해야 하는 이유

애초에 응답 코드를 enum으로 처리해야만 하는 경우가 많지는 않다.

enum을 정의한다는 것은 응답 코드와 관련된 여러가지 데이터와 로직(람다)을 묶어주고 다형성을 사용하겠다는 의미인데,
응답 코드를 그렇게 까지 다루어야 하는 경우가 잘 없다.

응답 코드에 따른 처리 로직을 묶어주어야 한다면, 처리 로직을 DTO 안에 method로 정의하는 정도로도 충분한 경우가 대부분이다.
물론 이렇게 처리하면 분기는 생기지만, 보통은 분기를 DTO 안으로 숨길 수 있다.

내가 수신 할 때는 fault tolerance하게 처리하지만, 반대로 내가 내려주는 응답을 수정 할 때는 클라이언트에 이런 처리가 안되어 있을거라 가정해야 한다.
내가 추가하는 응답 코드, 필드 변화로 인해 수신처 API 전체가 실패 할 수도 있을거라고 가정하고,
각 사용처에 응답 코드 추가 및 필드 변화에 대해 공유한 다음 반영해야 한다.

실제로 응답 코드로 enum을 사용하는 경우가 많아서…
삭제/변경이 아니라 추가이므로 괜찮을거라 생각했는데 문제가 되는 경우가 종종 있다.


상황 2 ) method param Type 자체를 Enum으로 정의할 것인지?아니면 String으로 받고 validation check를 할 것인지?

상황 : loan을 실행할 대상은 반드시 Wallet이 존재해야 한다. & loan 대상은 Wallet의 subset이다.

방법 1. LoanTarget이라는 Enum을 정의하고 이를 파라미터로 받는 방법

1
2
3
4
enum class LoanTarget(val wallet: Wallet, ...) {
    A(Wallet.A, ...)
}
fun loan(target: LoanTarget) { ... }

방법 2. Enum 정의 없이, param은 String으로 받고 validation check를 하는 방법

1
2
3
4
fun loan(target: String) {
    if (!target in Wallet.names) { ... }
    ...
}

방법2 가 아니라, (중복에도 불구하고) enum에 link 하도록 설계하는 방법1을 사용한 이유는

  1. 컴파일 타임에 실수를 알아챌 수 있다.
    • 예를 들어 Loan 대상인데 Wallet에는 누락되어 있는 경우. (LoanTarget은 반드시 Wallet의 subset이어야 한다는 제약조건)
    • 방법 1은 enum 추가하면서 알아챌 수 있지만, 방법 2는 런타임에 Exception 발생해야 알 수 있다. (심지어 if 안에서 적절히 알림을 주지 않는다면, 영영 알 수 없을 수도.)
  2. String을 Wallet, Symbol 로 캐스팅 하지 않고 member로 접근할 수 있기 때문에 코드 자체가 조금 더 의미를 가지게 된다.
  3. if 로직의 경우 이름으로 연결하겠다는 얘기인데, 상대적으로 fragile 하며 이름을 다르게 가져가야 하는 경우 수용이 안된다.

상황 3) 내가 외부 API 호출 할 때 사용할 요청 코드 타입은?

  • enum 사용하는 것이 장점이 있다.
  • DTO에도 enum을 써서 이름을 매겨주면, 확실히 더 보기 편한건 있다.
  • 위 보단 아래가 한 눈에 바로 들어오니까. (O는 @JsonValue로 처리)
    1
    2
    
    recipients.all { it.recipientType == CosRequest.CosRecipientType.O })
    recipients.all { it.recipientType == CosRequest.CosRecipientType.수신인 })
    

같은 관점에서 DTO에 @JsonProperty 써주는게 좋은 것 같다. code가… coin 이던가 market 이던가? 를 DTO만 봐도 바로 알 수 있으니까. 이런게 유지보수 하기가 더 편하다.

This post is licensed under CC BY 4.0 by the author.