본문 바로가기

개발/Java

Java enum을 역방향 매핑(reverse mapping) 시켜보자

역방향 매핑(reverse mapping)이란?


Java의 enum을 사용하면 멤버별로 숫자나 추가적인 코드를 매핑시킨 후, 멤버로부터 이를 꺼내 쓸 수 있다.

 

그런데 enum을 사용하다 보면, 반대로 숫자 혹은 코드가 있을 때 이에 해당하는 enum 멤버를 만들어 사용해야 하는 경우가 존재하는데, 이렇게 반대로 매핑하는 것을 역방향 매핑이라고 한다

 

TypeScript의 역방향 매핑과 Java에서의 구현


Typescript에서는 Numeric Enum인 경우 역방향으로 참조가 가능하도록 트랜스파일이 된다.

 

즉, TypeScript로 Enum을 만들면 아래와 같은 JavaScript 코드가 자동으로 생성된다.

// TypeScript로 작성한 코드
enum MudoMemberBirthEnum {
  JAESEOK = 72,
  MYUNGSOO = 70,
  JUNHA = 71,
  HAHA = 79,
}

 

// JavaScript로 트랜스파일된 코드
var MudoMemberBirthEnum;
(function (MudoMemberBirthEnum) {
    MudoMemberBirthEnum[MudoMemberBirthEnum["JAESEOK"] = 72] = "JAESEOK";
    MudoMemberBirthEnum[MudoMemberBirthEnum["MYUNGSOO"] = 70] = "MYUNGSOO";
    MudoMemberBirthEnum[MudoMemberBirthEnum["JUNHA"] = 71] = "JUNHA";
    MudoMemberBirthEnum[MudoMemberBirthEnum["HAHA"] = 79] = "HAHA";
})(MudoMemberBirthEnum || (MudoMemberBirthEnum = {}));

// 아래와 같이 양방향으로 참조가 가능하다
console.log(MudoMemberBirthEnum["JAESEOK"]) // 72
console.log(MudoMemberBirthEnum[72]) // "JAESEOK"

그러나 아쉽게도, Java에서는 이런 역방향 매핑을 제공해 주지 않는다!

 

따라서 역방향 매핑이 필요하다면,  TypeScript의 방식을 참조해 각 멤버의 역방향 멤버를 직접 작성하여야 한다

enum GradeEnum {
    EXCELLENT("A"),
    GREAT("B"),
    GOOD("C"),

    A("EXCELLENT"),
    B("GREAT"),
    C("GOOD");

    String code;
    GradeEnum(String code) {
        this.code = code;
    }

    String code() {
        return this.code;
    }
}


public class EnumTest {
    @Test
    void test() {
        System.out.println(GradeEnum.A.code()); // EXCELLENT
    }
}

 

역 매핑 직접 작성의 문제점


위와 같이 역방향 매핑을 하나하나 작성하는 것은 간단하지만 몇가지 문제점이 존재한다.

1. Numeric Enum을 역방향 매핑하려고 하는 경우 컴파일 에러로 인해 매핑할 수 없다.


나이, 연도 등 숫자에 대한 정보를 저장하는 Numeric Enum은 아래와 같이 UNEXPECTED TOKEN 에러가 발생하고 역방향 매핑이 불가능하다

enum MudoMemberBirthEnum {
    JAESEOK(72),
    MYUNGSOO(70),
    JUNHA(71),
    HAHA(79),
    
    72("JAESEOK"); // UNEXPECTED TOKEN
    MudoMemberBirthEnum(int birth) {
    }
}

 

타입을 String으로 고쳐도 마찬가지로 역방향 매핑이 불가능하다

enum MudoMemberBirthEnum {
    JAESEOK("72"),
    MYUNGSOO("70"),
    JUNHA("71"),
    HAHA("79"),
    
    "72"("JAESEOK"); // UNEXPECTED TOKEN
    MudoMemberBirthEnum(String birth) {
    }
}

 

2. 역방향 매핑을 위해 넣어놓은 값들이 자동완성에서 의미를 혼란스럽게 한다


아래와 같이 역방향 매핑을 명시하면, 자동완성에 역방향 매핑도 함께 등장하는데 이로 인해 개발자에게 혼란을 주게 된다

enum GradeEnum {
    EXCELLENT("A"),
    GREAT("B"),
    GOOD("C"),

    A("EXCELLENT"),
    B("GREAT"),
    C("GOOD");

    String code;
    GradeEnum(String code) {
        this.code = code;
    }

    String code() {
        return this.code;
    }
}

A와 EXCELLENT 중 어떤 것을 선택해야 할까?

3. Enum의 멤버수가 많아지면 그만큼 역방향 멤버도 늘어나므로, 반복 작업이 늘어나고 실수할 여지가 생긴다


멤버가 10개 미만인 경우에는 역방향 멤버를 직접 적는 것이 크게 부담되지 않는다.

 

하지만 멤버 수가 늘어날수록 작업량이 늘어나고, 그만큼 실수할 여지가 많아진다.

 

Java Map을 이용한 개선


위의 문제점들을 Map을 이용해 좀 더 깔끔한 방식으로 개선할 수 있다. 코드와 설명은 아래와 같다

enum GradeEnum {
    EXCELLENT("A"),
    GREAT("B"),
    GOOD("C");

    String code;
    GradeEnum(String code) {
        this.code = code;
    }
    
    String code() {
        return this.code;
    }


    private static final Map<String, GradeEnum> reverseMap = // ---------- (1)
            Arrays.stream(GradeEnum.values())
                    .collect(Collectors.toMap(GradeEnum::code, gradeEnum -> gradeEnum));


    public static GradeEnum fromCode(String code){ // -------------- (2)
        return reverseMap.get(code);
    }
}

(1) code를 key로, 해당 code에 연결된 Enum을 value로 갖는 map을 만든다

(2) fromCode 메서드는 code를 인자로 받아 map을 탐색해 해당하는 Enum을 반환한다

 

아래처럼 간단히 역방향 호출이 가능하다

@Test
void test() {
    System.out.println(GradeEnum.fromCode("A")); // EXCELLENT
}

 

위 방법도 단점이 하나 있는데, 모든 Enum마다 static map을 메모리상에 유지하고 있어야 한다는 점이다.

 

static map을 제거하고 싶다면 fromCode 메서드 호출시점에 동적으로 map을 만들고 탐색할 수도 있다

enum GradeEnum {
    EXCELLENT("A"),
    GREAT("B"),
    GOOD("C")
    ;

    String code;
    GradeEnum(String code) {
        this.code = code;
    }

    String code() {
        return this.code;
    }
    
    public static GradeEnum fromCode(String code){
        Map<String, GradeEnum> reverseMap = Arrays.stream(GradeEnum.values())
                .collect(Collectors.toMap(GradeEnum::code, gradeEnum -> gradeEnum));
        return reverseMap.get(code);
    }
}

 

만약 여러 개의 멤버가 같은 code를 가지는 경우, 런타임에 IllegalStateException이 발생하게 된다

GOOD("C"), BAD("C")처럼 중복된 코드를 집어넣었을 경우