열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입이다.
사계절, 태양계의 행성, 카드게임의 카드 종류 등이 좋은 예다.
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
상수를 이용한 열거 패턴 기법에는 타입 안전을 보장할 방법이 없으며, 표현력도 좋지 않다.
정수 상수는 문자열로 출력하기가 다소 까다롭다.
값을 출력하거나 디버거로 살펴보면 단지 숫자로만 보여서 썩 도움이 되지 않는다.
같은 상수 열거 그룹에 속한 모든 상수를 순회하는 방법도 마땅치 않다.
심지어 그 안에 상수가 몇 개인지도 알 수 없다.
문자열 상수는 하드코딩을 해야하고, 하드코딩한 문자열에 오타가 있어도 컴파일러는 확인 할 수없어서 런타임 버그가 발생한다.
문자열 비교에 따른 성능 저하 역시 당연한 결과이다.
이러한 단점들을 없애고, 여러 장점이 추가되는 대안이 자바의 열거 타입이다.
자바 열거 타입을 뒷받침하는 아이디어는 단순하다.
열거 타입 자체는 클래스이고, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
열거 타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final이다.
따라서 인스턴스들은 단 하나씩만 존재할 수 있다.
싱글턴은 원소가 하나뿐인 열거 타입이라고 할 수 있고, 열거 타입은 싱글턴을 일반화한 형태라고 볼 수 있다.
열거 타입은 컴파일타임에 타입 체크를 수행해서 타입 안전성을 제공하고,
toString 메서드는 출력하기에 적합한 문자열을 내어준다.
임의의 메서드나 필드를 추가할 수 있고, 인터페이스를 구현하게 할 수도 있다.
@Getter
public enum Resolutions{
P144("progressive_144",176,144),
P240("progressive_240",320,240),
P480("progressive_480",640,480),
P720("progressive_720",1280,720),
P1080("progressive_1080",1920,1080),
;
private final String progressive;
private final int width;
private final int height;
Resolutions(String progressive, int width, int height) {
this.progressive = progressive;
this.width = width;
this.height = height;
}
private static final Map<String, Resolutions> descriptions =
Collections.unmodifiableMap(Stream.of(values())
.collect(Collectors.toMap(Resolutions::getProgressive, Function.identity())));
public static Resolutions findSizeByProgressive(String progressive) {
return Optional.ofNullable(descriptions.get(progressive))
.orElse(null);
}
}
프로젝트 중 이미지 해상도와 관련해서 특정 값을 기준으로 구분해야 했던 적이 있었다.
switch case나 equals 로 해상도를 나누기엔 수정도 힘들고, 가독성도 떨어지게 되어 Enum 클래스를 활용했다.
Enum을 Map에 넣고, 값을 찾을 수 있는 메서드를 작성했다.
비교문을 사용하지 않고 해상도를 구분해, 가독성을 높였다.
후에 해상도가 추가될때도 Enum과 필드를 추가해주면 된다.
열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 순서대로 제공한다. ( values )
널리 쓰이는 열거 타입은 톱레벨 클래스로 만들고, 특정 톱레벨 클래스에서만 쓰인다면 해당 클래스의 멤버 클래스로 만든다.
라고 적혀있지만 상황에 따라 굳이 멤버클래스로 만들어야 하진 않아도 된다고 생각한다.
한 클래스의 멤버 클래스로 만들면 프로젝트 규모가 커질 때 찾기도 쉽지 않고,
클래스를 타입이나 기능 별로 모아놓는 것이 가독성을 높일 수 있을 거라고 생각하기 때문이다.
// 코드 34-6 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입 (215-216쪽)
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; }
@Override public String toString() { return symbol; }
public abstract double apply(double x, double y);
// 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽)
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
이런식으로 상수별 메서드 구현을 상수별 데이터와 결합할 수도 있다.
전략 열거 타입 패턴
// 코드 34-9 전략 열거 타입 패턴 (218-219쪽)
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
SATURDAY(WEEKEND), SUNDAY(WEEKEND);
// (역자 노트) 원서 1~3쇄와 한국어판 1쇄에는 위의 3줄이 아래처럼 인쇄돼 있습니다.
//
// MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
// SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
//
// 저자가 코드를 간결하게 하기 위해 매개변수 없는 기본 생성자를 추가했기 때문인데,
// 열거 타입에 새로운 값을 추가할 때마다 적절한 전략 열거 타입을 선택하도록 프로그래머에게 강제하겠다는
// 이 패턴의 의도를 잘못 전달할 수 있어서 원서 4쇄부터 코드를 수정할 계획입니다.
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
// PayrollDay() { this(PayType.WEEKDAY); } // (역자 노트) 원서 4쇄부터 삭제
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);
}
}
public static void main(String[] args) {
for (PayrollDay day : values())
System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
}
}
질문 1. int 상수 대신에 열거 타입을 사용해야 하는 이유는?
열거 타입은 의미 있는 상수 값을 나타내므로 코드의 가독성이 향상된다.
또한 컴파일러가 타입 체크를 수행하여 코드 안정성을 보장하고 이는 디버깅 시간을 단축시키고 런타임 에러를 방지할 수 있다.
질문 2. 열거 타입을 사용할 때 주의해야 할 점은?
열거 타입의 상수값은 변경되지 않아야 한다. 열거 타입은 고정된 상수 값을 나타내므로 값의 변경은 의미상의 충돌을 일으킬 수 있다.
열거 타입의 비교는 ' == ' 연산자를 사용하여 수행해야 한다. equals() 메서드는 == 와 동일한 결과를 반환한다.
왜냐하면 열거 타입의 정의에서 각각 열거 상수는 하나의 인스턴스로 존재하기 때문이다.
equals()는 객체의 내용을 비교하는 것이지만, 열거 타입은 인스턴스의 동일성을 확인하는 것이 주요 비교 대상이다.
따라서 열거 타입의 비교에는 항상 == 연산자를 사용하는 것이 일관성을 유지하고, 의도한 비교 동작을 보장하는 방법이다.
'JAVA > 이펙티브 자바' 카테고리의 다른 글
37. ordinal 인덱싱 대신 EnumMap을 사용하라 (0) | 2023.07.18 |
---|---|
35. ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2023.07.18 |
33. 타입 안정 이종 컨테이너를 고려하라 (0) | 2023.07.16 |
32. 제네릭과 가변인수를 함께 쓸 때는 신중하라 (0) | 2023.07.16 |
31. 한정적 와일드카드를 사용해 API 유연성을 높이라 (0) | 2023.07.16 |