Enum VS Map

  • https://medium.com/@nikitashahu/enum-vs-hashmaps-for-storing-constants-204ef4a1a8d7  
  • code navigator의 도움을 받을 수도 있고,
  • 없는 enum value를 잘못 참조했을 경우 compile time에 잡아낼 수도 있다는 장점도 있음.
  • 다만 외부 API 요청에 대한 응답 코드나, 내 API에 대한 요청 코드는 enum으로 정의하는 것 보다 String으로 받고 ``kt Map<String, 필요정보>`` 조회하는게 나은 경우가 있음 (fault tolerance)
    • enum에 정의 되어 있지 않은 값이 응답 코드로 들어올 수 있기 때문에 이에 대한 처리를 생각해 주어야 한다.
      • 기본적으로는 Exception 발생하게 되어 있다.
      • Exception 발생 안하게끔 `` @JsonEnumDefaultValue``로 디폴트 enum value로 매핑할 수 있다.
    • 하지만 디폴트값 매핑하는 경우 응답 코드에 대한 정보가 Enum으로 변환되면서 사라지므로, 로깅이 필요하다면 일단 응답을 Map으로 받아서 로깅 후 객체 변환해야 한다.
      • 즉, [json -> Obj] 바로 매핑하면 유실되니, 로깅을 위해 [json -> Map -> Obj] 해야만 한다.
      • 아니면 Enum Deserializer를 직접 구현해 로깅을 끼워 넣거나.
    • 반면 String으로 받는 경우 [json -> Obj] 가 가능하고, 추가 정보가 필요한 경우 Map에서 조회하면 된다.

 

enum의 단점은?

  • code를 찾아내기 위해 복합키를 사용해야 하는 경우
    • vs DB) 코드테이블을 사용한다면 PK를 복합키로 지정하여 constraint를 이용한 무결성을 보장할 수 있다. (개발자가 DB에 새로운 코드를 추가하는 순간 알게된다.)
    • 그러나 enum에 무언가를 추가하는건, enum 이름 기반 unique만 검사하기 때문에... enum 필드에 복합키를 넣어야 한다. 그래서 이에 대한 체크를 넣더라도, 어쨌든 runtime에 가서야 enum에 값이 잘못 추가되었다는 것을 알게된다.
    • vs Map도 마찬가지다. Map은 key 자리에 복합키를 넣어줄 수 있고 이 key가 중복되면 덮어쓰거나 한다. 그러나 enum은 중복된 필드값 그대로 가지고 있기 때문에, 신경써주지 않으면 1개 값이 튀어나올 줄 알았는데 2개가 튀어나온다거나. 하는 에러가 발생할 수 있다.

 

 

code 1 : 현금, 2 : 카드 라는 정보를 관리하는 방법

  • DB에 code table을 만들어서 관리
    • 어드민 메뉴로 새로운 코드를 추가하는 것이 용이함. (삭제/수정은 constraint를 잘 써야 무결성 유지가 가능함)
    • 단점은 코드에서 code, name을 String으로 받아와 관리해야 한다. 로직은 또 따로 두어야 한다.
    • 매번 DB에서 읽어오는게 부담일 수 있다. 고작 이거 구분값 읽어오자고 DB를? 이라는 생각이 들 수도.
  • DB에 있는 코드 표를 enum으로 아예 옮겨버리는 방법 (배민 enum 활용기)
    • 코드 상에서 code와 name을 enum 타입으로 묶어서 들고다닐 수 있다. + 로직까지.
    • 매번 DB에서 읽어오는건 부담스러우나, enum으로 변환하는건 아주 마음이 가볍다.
  • 기타
    • DBMS function을 이용해 코드 변환 (매번 조인보다 낫다)
    • redis에 코드 테이블 올려두고 사용하는 방법
    • 객체 내부에 Holder를 두어 같은 내용으로 두 번 resolve 하지 않도록 캐시처럼 사용하는 방법
      • 하지만 이건 DB가 업데이트 되면 캐시도 업데이트 해주어야 한다는 문제점이 있어 좋지 않다.

 

enum을 쓰든, DB code table 을 참조하든 둘 중 하나만 하는 것이 좋은가?

  • 관리 포인트가 늘어나기 때문에 둘 중 하나만 쓰는게 좋아 보일 수 있다. 수정 사항이 발생했을 때 DB, enum 둘 다 반영해야 하므로.
  • 하지만 운영하다 보면 아래와 같은 상황이 발생하게 되는데...
    • INSERT INTO SELECT 구문을 사용하고 싶을 때. (bulk insert)
    • DB에서 해당 code table을 여기저기서 조인하여 사용해야 하는 경우
      • DB code table이 없다면 바로 query에서 처리 못하고 매번 app단으로 불러와야 함. 매우 불편
    • 해당 code table을 빈번하게 조회해야 하는 경우 (enum이 없다면 매번 code 변환을 위해 query 수행해야 함)
  • 운영 하다 보면 DB table을 사용하는게 더 편할 때도 있고, enum을 사용하는게 더 편할 때도 있다.
  • 억지로 한 쪽에서만 관리하기 보다는 상황에 따라 DB와 enum 둘 다 만들어 선택지를 주는 것이 적절해보인다.

 

enum도 결국 소스코드이기 때문에, 변경이 발생한다면 다시 컴파일해야 한다. 

  • 컴파일 시간이 굉장히 오래 걸린다면 단점 이겠지만 보통의 경우는 글쎄.
  • 상수 값이 자주 변경된다면, `` .yml, .properties``같이 외부로 빼면 컴파일 없이 jar 패키징만 해도 적용 가능하다.
  • 하지만 yml은 아래와 같은 단점이 있는데. (properties는 당연히 복잡한 hierarch 관리하기에는 부적절하고)
    • 자주 변경된다면 DB에 두는 것이 낫고, syntax 힌트나 하이라이트, 링크 등을 생각하면 enum이 yml 보다 낫다.
    • yml로 관리한다해도, 이를 읽어와서 필드로 가지고 있는 객체를 만들어야 되니까 클래스는 어차피 만들어야 한다.
    • 결론 ) yml로 관리하는게 적절한 케이스는 하는 케이스는 간단한 설정값 등등이고, 복잡한 계층 구조나 연결 구조가 들어간다면 enum으로 빼는 것이 낫다. 

 

enum은 extends는 불가하지만, implements는 가능하다.

  • 공통 interface를 만들 때는, getter의 선언부가 인터페이스에 들어가줘야 해당 인터페이스 타입으로 접근했을 때 .getCode() 같은 메서드를 호출할 수 있다. (클래스 구현부에서는 @Getter를 써도 된다.)
 

예제

@Getter
@RequiredArgsConstructor
public enum PaymentCode {
    CASH(1, "현금"),
    CARD(2, "카드");

    private final int code;
    private final String koName;

    public static PaymentCode fromKoName(String text){
        for (PaymentCode code : values()){
            if (code.getKoName().equals(text)) {
                return code;
            }
        }
        throw new IllegalArgumentException();
    }
}
    /**
     * 위 코드와는 관련 없지만 함수형으로 쓰면 이런 식이 된다.
     */
    public static PaycoResponseCode from(int _code) {
        return Stream.of(values())
            .filter(responseCode -> responseCode.getCode() == _code)
            .findAny()
            .orElseThrow(NoSuchElementException::new);
    }

 

/* 기본 제공 */
log.info("{}", Payment.PaymentCode.valueOf("CARD"));     // CARD 아래와 같다. 이름 문자열 받아서 enum찾을 때 말고는 쓸일 없는 듯.
log.info("{}", Payment.PaymentCode.CARD);                // CARD 위와 같다.
log.info("{}", Payment.PaymentCode.CARD.name());         // CARD

/* @Getter */
log.info("{}", Payment.PaymentCode.CARD.getKoName());    // 카드
log.info("{}", Payment.PaymentCode.CARD.getCode());      // 2

/* 직접 정의. */
log.info("{}", Payment.PaymentCode.fromKoName("현금"));   // CASH

 

of 연산을 위한 Map을 static 변수에 담아 둘 때, duplicate key 같은 문제는 enum이 최초로 호출되는 시점에서야 발견된다.

  • 기본적으로 JVM에서 compile-time 상수가 아닌 모든 static field는 해당 class 최초 접근 시에 비로소 초기화 된다. Application 실행 시점에 초기화 되는 것이 아니다. (JVM 관련)
  • enum static에서 Duplicate Key 같은 에러가 발생할 수 있다면, 앱이 실행되고 나중에 최초 접근할 때가 되어서야 문제가 있음을 알 수 있기 때문에, TC를 추가해서 미리 CI 레벨에서 검증이 되도록 하는게 좋다.

 

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

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

 

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

```kt

enum class LoanTarget(val wallet: Wallet, ...) {

    A(Wallet.A, ...)

}

 

fun loan(target: LoanTarget) { ... }

```

 

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

```kt

fun loan(target: String) {

    if (!target in Wallet.names) { ... }

    ...

}

```

 

Wallet이나 Symbol에 대한 in check를 loan 메서드에서 수행하는 방법2 가 아니라,

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

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

 

 

'Java Stack > Java' 카테고리의 다른 글

[Effective Java] 2장 객체 생성과 파괴  (0) 2019.12.02
[Java] LocalDateTime : 날짜 시간 처리 관련  (0) 2019.07.10
[Java] Jackson ObjectMapper Serialization  (0) 2019.05.15
JVM 관련  (0) 2017.05.05
[Java] Stream API 노트  (0) 2017.03.09