열거 타입(Enum) 활용하기

📌 열거 타입(Enum)과 애너테이션(Annotation)

💡
자바에서 특수한 목적의 참조 타입으로, 열거 타입(Enum)과 애너테이션(Annotation)이 존재한다. 이번 글에서는 열거 타입을 올바르게 사용하는 방법에 대해서 알아보고자 한다.

📌 열거 타입(Enum)이 무엇일까?

결론부터 말하자면 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입을 말한다. 열거 타입은 필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합인 경우에 사용하는 것이 좋다. 그리고 열거 타입의 특징으로는 아래와 같다.
1.
열거 타입은 클래스이다.
2.
필드는 public static final로 공개한다.
3.
밖에서 접근할 수 있는 생성자를 제공하지 않는다.
4.
클라이언트가 인스턴스를 새롭게 생성하거나 확장할 수 없어, 만들어진 인스턴스는 딱 하나만 존재하는 것을 보장한다. 즉, 상수 하나당 자신의 인스턴스는 하나씩 만드는 셈이다.

📌 열거 타입은 무엇을 해결하고자 등장했을까?

열거 타입 등장 전, 정수 열거 패턴(int enum pattern)의 문제점

다음과 같이 정수 상수를 한 묶음 선언해서 사용하곤 했다. 이러한 패턴을 정수 열거 패턴(int enum pattern) 기법이라 한다.
public static final int APPLE_A = 0; public static final int APPLE_B = 1; public static final int APPLE_C = 2; public static final int ORANGE_A = 0; public static final int ORANGE_B = 1; public static final int ORANGE_C = 2;
Java
이와 같은 기법은 아래와 같은 단점이 존재한다.
1.
타입 안전을 보장할 방법이 없다.
2.
표현력이 좋지 않다. 이로 인해 디버깅하는 데 썩 도움이 되지 않는다.
3.
동등 연산자로 비교하더라도 컴파일러는 경고하지 않는다.
4.
그룹에 해당하는 모든 상수를 순회하는 방법이 마땅치 않다.

열거 타입 등장 전, 문자열 열거 패턴(string enum pattern)의 문제점

정수 대신 문자열 상수를 사용하는 패턴을 문자열 열거 패턴(string enum pattern) 기법이라 한다. 그러나 해당 기법은 이전의 정수 열거 패턴보다 더 나쁠 수 있다. 해당 기법은 문자열 상수의 이름 대신 문자열 값을 그대로 하드코딩하게 될 수 있다. 하드코딩으로 인해 오타가 존재해도 컴파일러는 잡아낼 수 없기 때문에 치명적인 버그가 생길 수 있다.

📌 열거 타입으로 인해 어떤 이점을 얻을 수 있을까?

타입 안전성을 제공받을 수 있다.

아래와 같이 달을 나타내는 열거 타입이 있다면, 다른 타입의 값을 넘긴다면 컴파일 오류가 난다. 다른 열거 타입의 값끼리 동등성(== 연산자)을 비교하는 꼴과 같기 때문이다.
public enum Month { JANUARY, FEBRUARY, MARCH, ... }
Java

toString 메서드로 적절한 문자열로 표현할 수 있다.

열거 패턴이 등장하기 전, 위에서 언급했었던 기법으로는 표현력이 부족했었다. 그러나 열거 타입은 이를 toString 메서드를 구현함으로써 아래와 같이 단점을 보완할 수 있다.
enum Month { JANUARY, FEBRUARY, MARCH, ... @Override public String toString() { return "It's " + this.name() + " now."; } }
Java

메서드나 필드 추가 및 인터페이스를 구현할 수 있다.

열거 타입은 실제로는 클래스이다. 따라서 연관된 데이터를 상수 자체에 내재시키거나, 메서드도 추가할 수 있다. 이로 인해 고차원의 추상 개념 하나를 완벽히 표현할 수 있다.
열거 타입에서 필드를 추가할 때, 주의 할 점이 있다. 아래와 같이 모든 필드는 final 선언하는 것을 잊지말자. 열거 타입은 근본적으로 불변이기 때문이다. 그러나 필드에 접근해야 한다면, public 필드보다 public 접근자 메서드로 두자.
public enum Month { JANUARY(1), FEBRUARY(2), MARCH(3) ... private final int number; Month(int number) { this.number = number; } @Override public String toString() { ... } }
Java
그리고 열거 타입은 임의의 인터페이스를 구현할 수 있다. 열거 타입의 내부는 Object를 상속받고 있고, Comparable과 Serializable을 implements하고 있다. (링크를 참고하면, 그림과 같이 확인할 수 있다)

📌 열거 타입을 어떻게 활용할까?

[상수별 메서드 구현] 하나의 메서드를 상수별로 다르게 동작시키기

열거 타입에서 아래와 같이 switch 문을 사용하면 어떤 문제가 있을까? 이렇게 되면 새로운 상수를 추가 할 때마다 case 문을 추가해야 한다. 만약에 실수로 빼먹는 경우에 런타임 에러가 발생할 수 있다.
public enum Operation { PLUS, MINUS, TIMES, DIVIDE; public double apply(double x, double y) { switch(this) { case PLUS: return x + y; case MINUS: return x - y; case TIMES: return x * y; case DIVIDE: return x / y; } throw new AssertionError("알 수 없는 연산: " + this); } ... }
Java
이러한 단점을 보완하기 위해 열거 타입에서 상수별 메서드 구현(constant-specific method implementa-tion)을 제공한다. 해당 기능은 상수별 클래스 몸체(constant-specific class body) 즉, 각 상수에서 자신에 맞게 아래와 같이 재정의 할 수 있다.
public enum Operation { PLUS {public double apply(double x, double y) {return x + y;}}, MINUS {public double apply(double x, double y) {return x - y;}}, TIMES {public double apply(double x, double y) {return x * y;}}, DIVIDE {public double apply(double x, double y) {return x / y;}}; public abstract double apply(double x, double y); ... }
Java

[fromString] 이름에 해당하는 상수 반환하기

상수 이름을 입력받고, 이에 알맞는 상수를 반환하는 로직이 필요할 수 있다. 결론부터 말하자면 values 메서드Map을 활용해서 아래와 같이 구현할 수 있다.
private static final Map<String, Operation> stringToEnum = Stream.of(values()) .collect(toMap(Object::toString, e -> e)); public static Optinal<Operation> fromString(String symbol) { return Optional.ofNullable(stringToEnum.get(symbol)); }
Java
여기서 반환하는 값을 주의해야 한다. Optional<Operation>으로 반환하고 있다. 이는 연산이 존재하지 않을 수 있기 때문에 클라이언트에서 대처하기 위해 Optional을 사용하였다.

[전략 열거 타입 패턴] 상수 일부가 같은 동작을 공유하기

열거 타입 상수끼리 코드를 공유하기 힘들다. 이를 해결하기 위해 if-else 혹은 switch 문으로 분기처리를 하게 될 수 있다. 그러나 해당 방법은 문제가 있다. 새로운 값을 열거 타입에 추가하려면 case 문을 잊지 말고 쌍으로 추가해줘야 한다. 개발자가 이를 깜빡하는 경우에 치명적인 버그의 가능성을 높일 수 있다.
이를 해결하기 위해 '전략'을 선택하도록 구성하는 방법이 있다. 이것을 전략 열거 타입 패턴이라 부른다.
enum Month { JANUARY(A), FEBRUARY(A), MARCH(B) ... } enum Strategy { A { int apply(int x, int y) { return ~~; } }, B { int apply(int x, int y) { return ~~; } }; abstract int apply(int x, int y); }
Java

📌 참고 자료

[Item 34] int 상수 대신 열거 타입을 사용하라
TOP