Item 34 - int 상수 대신 열거 타입을 사용하라
정수 열거 패턴(int enum pattern)
열거 타입이 자바에 추가되기 전엔 정수 상수를 열거하는 취약한 방법을 사용했다.
1
2
3
4
5
6
7
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
정수 열거 패턴(int enum pattern) 기법에는 단점이 많다. 😓
- 타입 안전을 보장하지 못한다.
- 표현력도 좋지 않다.
- 오렌지를 건네야할 메서드에 사과를 보내고 동등 연산자로 비교하더라도 컴파일러는 모름.
- 상수의 값이 바뀌면 클라이언트도 다시 컴파일해야 한다.
- 문자열로 출력하기가 까다롭다.
열거 타입 (enum type)
위의 단점들을 해소시켜주는게 자바의 열거 타입이다.
1
2
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
생긴건 다른 언어의 열거 타입과 비슷해 보이지만, 자바의 열거 타입은 다르다.
- 완전한 형태의 클래스이다. (단순히 정수 값인 다른 언어의 열거 타입보다 강력하다.)
- 열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스로 구성된다.
- 싱글톤으로 구현되어 있다. (public static final)
- 컴파일타임 타입 안정성을 제공한다.
- 특정 열거 타입을 받아야하는 자리에 다른 타입의 값을 넘기려하면 컴파일 오류가 발생한다.
- 열거 타입에 새로운 상수를 추가하거나 순서를 바꿔도 컴파일 하지 않아도 된다.
- 열거 타입에 임의의 메서드나 필드를 추가할 수 있으며 인터페이스를 구현할 수도 있다.
- Object 메서드들, Comparable, Serializable을 높은 품질로 구현해놓았다.
열거 타입에 메서드나 필드 추가하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // 질량(단위: 킬로그램)
private final double radius; // 반지름(단위: 미터)
private final double surfaceGravity; // 표면중력(단위: m / s^2)
// 중력상수(단위: m^3 / kg s^2)
private static final double G = 6.67300E-11;
// 생성자
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
- 열거 타입 각 상수를 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
- 열거 타입은 근본적으로 불변이라 모든 필드는 final이어야 한다.
- 필드를 public으로 선언해도 되지만, private으로 두고 별도의 public 접근자 메서드를 두는게 낫다.
열거 타입의 상수별 메서드 구현
열거 타입의 상수마다 다르게 동작을 구현하고 싶다면?
(1) switch문으로 분기하는 방법 - 이대로 만족하는가? 😰
1
2
3
4
5
6
7
8
9
10
11
12
13
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); // 런타임 오류
}
}
- 위 코드는 잘 동작하지만, 깨지기 쉬운 코드이다.
- 새로운 상수가 추가될 경우 해당 case문을 추가해야 하므로 변경 가능성이 많다.
- 새로 추가한 연산을 수행하려 할 때 컴파일은 잘되지만 런타임 오류가 난다.
위의 방법말고 상수마다 동작을 다르게 하기 위한 더 좋은 방법이 있다.
(2) 상수별 메서드 구현을 활용한 열거 타입 👍
1
2
3
4
5
6
7
8
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); // 추상 메서드
}
apply
는 추상 메서드이기 때문에 재정의하지 않으면 컴파일 오류를 내준다.
상수별 메서드 구현을 상수별 데이터와 결합할 수도 있다.
- 생성자를 이용해 데이터와 결합하여 편리하게 사용했다.
1
2
3
4
5
6
7
8
9
10
11
12
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; }};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
public abstract double apply(double x, double y);
}
- 위 코드와 toString()을 잘 이용하면 다음과 같은 아주 편리한 코드를 만들 수 있다.
1
2
3
4
5
6
7
8
9
10
for (Operation op : Operation.values()) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
/** 출력
* 2.000000 PLUS 4.000000 = 6.000000
* 2.000000 MINUS 4.000000 = -2.000000
* 2.000000 TIMES 4.000000 = 8.000000
* 2.000000 DIVIDE 4.000000 = 0.500000
*/
전략 열거 타입 패턴
- 상수별 메서드 구현시, 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.
- 값에 따라 분기하여 코드를 공유하는 열거 타입을 구현해보았다.
- 아래 코드는 변경 사항이 많은 위험한 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
SATURDAY, SUNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
PayrollDay(PayType payType) { this.payType = payType; }
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;
int overtimePay;
// case 문을 날짜별로 두어 계산 수행
switch(this) {
case SATURDAY: case SUNDAY: // 주말
overtimePay = basePay / 2;
break;
default: // 주중
overtimePay = minutesWorked <= MINS_PER_SHIFT ?
0 :(minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
위 코드를 전략 열거 타입 패턴으로 바꿔보자.
- 중첩 열거 타입을 사용하고, 생성자에서 열거 타입 상수 중 적절한 것을 선택하도록 한다.
- 계산을 전략 열거 타입에 위임했다.
- switch 문이나 상수별 메서드 구현이 필요없게 된다.
- 안전하면서, 유연하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
enum PayrollDay {
MONDAY(WEEKDAY),
TUESDAY(WEEKDAY),
WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY),
FRIDAY(WEEKDAY),
SATURDAY(WEEKEND),
SUNDAY(WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// 전략 열거 타입
enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ?
0 : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
}
열거 타입을 언제 사용할까?
- 필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이면 항상 열거 타입을 사용하자.
- 열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다.
💡 핵심 정리
- 열거 타입은 정수 상수보다 안전하고, 가독성도 좋고, 강력하다.
- 열거 타입의 각 상수를 특정 데이터와 연결짓거나 상수별로 동작을 다르게 할 경우
- 명시적 생성자나 메서드가 쓰인다.
- 하나의 메서드가 상수별로 다르게 동작해야 할 경우
- switch 문 대신 상수별 메서드 구현을 사용하자. (추상 메서드 재정의)
- 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.
참고 🕶️
This post is licensed under CC BY 4.0 by the author.