[Java] Enum 소화하기
by 배부른코딩로그클린 코드의 필수 enum, 자꾸 enum enum 하는데 이넘이 대체 왜? ㅋㅋ...
Enum이란?
enum은 열거형(enumerated type)이라고 부른다. 열거형은 서로 연관된 상수들의 집합을 의미하는데, 즉 서로 관련된 상수를 편리하게 선언하고 관리하기 위해 enum을 사용한다.
Enum의 등장배경
enum이 등장하기 전까진 정수 열거 패턴을 사용했다. 하지만, 정수 열거 패턴에는 큰 문제점이 존재했다.
private final int SPRING = 1;
private final int SUMMER = 2;
private final int FALL = 3;
private final int WINTER = 4;
- 가독성이 떨어지며, 관리가 용이하지 않다.
- 정수 열거 패턴을 위한 네임스페이스 공간이 없어 타입 세이프하지 않다.
ex) 실수로 값이 변경됐을 때, 타입 세이프하지 않기 때문에 SPRING과 SUMMER가 같은 값이 될 수 있음.
private final int SPRING = 1; private final int SUMMER = 1; // 오타 System.out.println(SPRING == SUMMER) // TRUE
- 상수인 정수는 문자열로 출력하기 어렵다. 문자열로 출력하기 위해 메서드를 따로 만들어야 한다.
- 정수 열거 그룹의 모든 상수를 순회할 방법이 없다. (복잡도 큼)
enum의 등장은 정수 열거 패턴의 단점들을 모두 보완할 수 있게 되었다.
public enum Season {
SPRING, SUMMER, FALL, WINTER
}
- 정수 열거 패턴의 단점들을 모두 개선했다. → enum, Better Grammar!
- 타입 비교가 가능하기 때문에 타입 세이프한 클린 코딩이 가능하다. → IDE 타입 체크 가능, 생산성 UP!
- 읽기 쉽고 명료한 코드를 작성을 할 수 있다. → 생산성 및 유지보수 Good!
Enum 뜯어보기
열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입이다.
- 다른 언어와 달리 단순한 정수 값이 아니다.
- Java의 enum 타입은 클래스이다.
- enum 상수 하나당 인스턴스가 만들어지며, 각각 public static final이다.
- 컴파일 타임 안전성을 제공한다.
public enum Season {
SPRING, SUMMER, FALL, WINTER
}
// public class Season {
// public static final Season SPRING = new Season("SPRING");
// public static final Season SUMMER = new Season("SUMMER");
// public static final Season FALL = new Season("FALL");
// public static final Season WINTER = new Season("WINTER");
//
// private String name;
//
// private Season(String name) {
// this.name = name;
// }
// }
필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자!
enum은 계절, 요일, 체스 말 등 본질적으로 열거 가능한 타입들이 포함된다. 그리고 메뉴 목록, 연산 코드, 명령줄 플래그 등 허용하는 값 모두를 컴파일 타임에 이미 알고 있을 때도 쓸 수 있다. 열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다. 열거 타입은 나중에 상수가 추가돼도 바이너리 수준에서 호환되도록 설계되었다.
Enum 활용하기
가장 단순한 형태
아래의 예제는 enum의 가장 단순한 형태이다. for문과 .value()를 사용해 enum을 순회할 수 있다.
enum Season { SPRING, SUMMER, FALL, WINTER }
// for (Season s : Season.values()) System.out.println(s);
// SPRING SUMMER FALL WINTER
데이터와 메서드가 있는 형태
아래의 예제와 같이 데이터와 메서드가 있는 형태를 만들 수도 있다. (이해를 위한 한글)
enum 동전 {
십원(10), 오십원(50), 백원(100), 오백원(500);
동전(int 값어치) { this.값어치 = 값어치; }
private final int 값어치;
public int 값어치() { return 값어치; }
}
switch와 함께 사용하기
switch와 함께 사용한다면 모든 타입을 나열해야 할 수 있고, 마지막의 throw와 같은 불필요한 코드가 있어야 한다.
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("Unknown op: " + this);
}
}
switch의 대안으로 상수별로 다르게 동작하는 코드 구현
이펙티브 자바 3/E 에서는 위와 같은 switch의 대안으로 다음과 같은 방식을 소개한다.
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);
}
@Override toString을 통해 보기 좋게 만들기
toString을 오버라이드하여 위의 코드를 다음과 같이 출력하기 좋게 변경할 수 있다.
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; }
};
private final String symbol;
Operation(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
public abstract double apply(double x, double y);
}
HashMap 대신 EnumMap 사용하기
EnumMap은 EnumSet처럼 HashMap보다 안정적이고 효율적이다.
Map<Planet, String> enumMap = new EnumMap<>(Planet.class);
EnumMap을 동기식으로 사용할 필요가 있을 경우 Collections.synchronizedMap을 사용한다.
Map<EnumKey, V> m
= Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));
interface를 통해 확장하기
다음은 이펙티브 자바 3/E 아이템 38의 예제이다.
enum이 interface를 구현하게 하는 방법을 쓰고 있다.
public interface Operation {
double apply(double x, double y);
}
public enum BasicOperation implements 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; }
};
private final String symbol;
BasicOperation(String symbol) { this.symbol = symbol; }
@Override
public String toString() { return this.symbol; }
}
Value값에 해당하는 enum 객체 가져오기
enum을 사용하다 보면, 특정 값과 일치하는 enum을 찾는 메서드가 필요한 경우가 종종 생긴다.
다음과 같이 stream을 통해 구현이 가능하다.
enum Operation {
PLUS("+"),
MINUS("-"),
TIMES("*"),
DIVIDE("/");
private String operation;
Operation(String operation) { this.operation = operation; }
static Operation findBy(String operation) {
return Arrays.stream(values())
.filter(v -> operation.equals(v.operation))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(operation + ", 지원하지 않는 연산자입니다!"));
}
}
간혈적으로 사용하는 케이스인 경우, 위와 같은 방식으로 해도 무방하다.
하지만, 열거형 타입이 많고 빈번하게 호출된다면, 미약한 성능 개선이 필요하다.
enum Operation {
PLUS("+"),
MINUS("-"),
TIMES("*"),
DIVIDE("/");
private String operation;
Operation(String operation) { this.operation = operation; }
private static final Map<String, Operation> valueToEnumMap = new HashMap<>();
static {
for (Operation oper : values()) {
valueToEnumMap.put(oper.value(), oper);
}
}
public Operation findBy(String operation) {
return valueToEnumMap.get(operation);
}
public String value() {
return operation;
}
}
클린 코드: Code Readability 향상
일반적으로 자주 사용하는 함수 인자 패턴이다.
public class EnumExample {
public enum RouteSection { ADMIN, SELLER, CUSTOMS };
public String route(RouteSection section, String json) {
switch (section) {
case CUSTOMS:
doCustoms(json);
break;
case CUSTOMS:
doCustoms(json);
break;
case CUSTOMS:
doCustoms(json);
break;
default:
doRouteException(json);
break;
}
}
public void request() {
route(ADMIN, "{ type: admin }");
route(CUSTOMS, "{ type: buyer }");
route(SELLER, "{ type: seller }");
}
}
안티 패턴: ordinal 메서드의 사용
Java API 문서에서는 enum의 ordinal 메서드에 대해 다음과 같이 말한다.
Most programmers will have no use for this method.
It is designed for use by sophisticated enum-based data structures, such as EnumSet and EnumMap.
대부분의 프로그래머는 이 메서드를 쓸 일이 없다.
이 메서드는 EnumSet과 EnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.
ordinal 메서드는 단지 해당 상수가 몇 번째인지를 리턴할 뿐이고, 쓸모가 없다.
이 값에 의존하는 코드를 작성하는 것도 좋은 선택이 아니다.
쓰지 않는 것이 좋다.
Enum 소화하기
정말 중요한 개념이자 활용도 높은 문법이라고 생각된다. Enum 하나로 코드가 정말 깔끔해지는 경험을 자주하게 되는데, 그 때마다 정확한 사용법을 몰라 사용하기 어려웠고 고민하다 포기하기도 했다. 이번 정리를 통해 enum을 다양한 곳에서 적극적으로 사용하고자 한다. 항상 사용 시 주의해야할 점과 안티 패턴들을 확인하자.
출처
- 조슈아 블로크 저, 이펙티브 자바 Effective Java 3/E
- 기계인간, https://johngrib.github.io/wiki/java-enum/#fn:effective-define
- 오픈튜토리얼스, https://opentutorials.org/course/2517/14151
- 노력남자, https://effortguy.tistory.com/23
Last updated. 2022.05.09
'Java' 카테고리의 다른 글
[Java] Lombok 소화하기 (0) | 2022.06.07 |
---|---|
[Java] Null Safe한 자바 컬렉션 정렬 (0) | 2022.05.24 |
[Java] YAML 활용하기 (0) | 2022.05.04 |
[Java] @Required 직접 만들어보기 (0) | 2022.04.23 |
[Java] Array, List, Set, Map 선언과 초기화 (1) | 2021.11.05 |
블로그의 정보
배부른코딩로그
배부른코딩로그