요약
정수 상수 대신 열거 타입을 사용하자. 열거 타입이 더 가독성 좋고, 안전하다.
필요한 원소를 컴파일 타임에 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자.
-> 열거 타입에서 메서드 구현 방식으로는 상수별 메서드 구현, 전략 열거 타입 패턴이 있다.
열거 타입
열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입이다. 예시로 사계절, 태양계의 행성, 카드 게임의 카드 종류 등이 있다.
정수 열거 패턴은 되도록이면 사용하지 말자.
왜냐하면 타입 안전을 보장할 수 있는 방법이 없고, 표현력도 좋지 않고, 코드가 깨지기 쉽기 때문이다.
public static final int APPLE_FUJI = 0;
public static final int ORANGE_NAVEL = 0;
- 타입 안전 보장 못함
- APPLE_을 사용하고 싶은데 실수로 ORANGE_를 사용하더라도 잘못 입력했단 걸 미리 확인할 수 없음
- 표현력 좋지 않음
- 이름으로 상수를 구분하면 값 출력이나 디버깅 시 숫자만 표현되어 해당 숫자가 어떤 것을 의미하는지 파악하기 어렵다. 위 코드를 예시로 0으로 확인되는 것의 값이 APPLE인지 ORANGE인지 알 수 있는 길이 없다
- 코드 깨지기 쉬움
- 상수 값 변경 시 관련 클라이언트도 재컴파일 해야하는 번거로움이 생길 수 있다.
열거 타입 사용하자.
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
- 열거 타입 특징
- 자바 열거 타입 자체는 클래스
- 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 설정
- 열거 타입은 public 생성자를 제공하지 않음
- 열거 타입으로 만들어진 인스턴스는 하나만 존재 → 인스턴스가 통제됨
- 열거타입은 싱글턴을 일반화한 형태
- 싱글턴은 원소가 하나뿐인 열거 타입
- 열거 타입 장점
- 컴파일타임 타입 안정성 제공
- ex) Apple 타입이면 3가지 중 하나임을 보장, 아니면 컴파일 오류 발생
- 새로운 상수를 추가하거나 순서를 바꿔도 재컴파일 하지 않아도 됨
- 열거 타입 각자 이름공간이 있어 이름이 같은 상수도 평화롭게 공존 가능
- 컴파일타임 타입 안정성 제공
데이터와 메서드를 갖는 열거 타입
각 상수와 연관된 데이터를 해당 상수 자체에 내재시키고 싶을 때 인스턴스 필드와 메서드를 이용하면 된다. 필드와 메서드의 접근 제어자는 필요한 경우가 아니라면 웬만해선 private, package-private(default)로 선언해야 한다.
public enum Planet {
EARTH(123, 456);
private final double mass;
private final double radius;
private final double surfaceGravity;
private static final double G = 1;
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; }
}
EARTH와 관련된 mass, radius 값은 변하지 않으므로 열거 타입에 상수로 저장해 놓으면 물체 무게(mass)만 입력하면 지구에서의 물체 무게를 바로 구할 수 있다.
for (Planet p : Planet.values()) {
System.out.printf("%s에서의 무게는 %f이다.", p, p.surfaceWeight(10));
}
열거 타입을 이용하면 행성 관련 정보를 입력하지 않고 짧은 코드로 원하는 값을 출력할 수 있다. 또한 열거 타입에서 상수가 새로 추가되거나 제거되는 변경이 발생하더라도 컴파일 오류가 발생하기 때문에 안전하게 대응할 수 있다.
상수별 메서드 구현
상수별 메서드 구현은 열거 타입에서 apply라는 추상 메서드를 선언하고, 각 상수에 맞게 재정의하는 방법을 의미한다.
- 상수별 메서드 구현을 권장하는 경우
- 상수를 특정 데이터와 연관지어야 하는 경우
- 상수별로 메서드를 실행해야 하는 경우
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;},
public abstract double apply(double x, double y);
}
위 코드처럼 작성하면 새로운 상수를 추가할 때 재정의 메서드를 빼먹지 않고 추가할 수 있을 것이다. 그래도 만약 재정이 메서드를 작성하지 않았다면 컴파일 오류로 알려주기 때문에 안전성을 보장한다.
- 장점
- 재정의 메서드 빼먹지 않을 수 있음
- 상수 옆에 바로 재정의 메서드가 있어서 바로 확인이 가능하다
- 컴파일 오류
- 만약 메서드를 작성하지 않는다면 컴파일 오류로 타입 안정성을 보장할 수 있다
- 재정의 메서드 빼먹지 않을 수 있음
- 단점
- 열거 타입 상수끼리 코드 공유가 어렵다
근무시간 계산 - 열거 타입 상수끼리 코드 공유 어려운 경우
enum PayrollDay {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate) {
int basePay = minuteWorked * payRate;
int overtimePay;
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 + overtiePay;
}
열거 타입으로 비교적 간단한 코드이지만 이 코드에는 관리 관점에서 문제가 있다.
HOLIDAY 같은 새로운 값을 열거 타입에 추가하려면 HOLIDAY를 추가하고, 관련 로직을 switch()에 따로 추가해줘야 한다. 그렇지 않으면 휴일 비율이 아닌 평일 비율로 계산될 수 있기 때문이다.
이 문제를 해결하기 위해서는 전략 열거 타입 패턴을 이용해 코드를 작성하면 된다.
전략 열거 타입 패턴
- 전략 열거 타입 패턴을 권장하는 경우
- 열거 타입 상수 일부가 같은 동작을 공유하는 경우
enum PayrollDay {
MONDAY(WEEKDAY), SATURDAY(WEEKEND), THANKSGIVINGDAY(HOLIDAY);
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);
}
}
}
- 잔업 수당 계산은 private 중첩 열거 타입인 PayType을 이용한다.
- PayrollDay 생성자에서 PayType을 선택해 관련 로직을 진행한다.
→ 이렇게 할 경우 코드는 좀 복잡해질 수 있지만, 더 안전하고 유연하게 코드를 작성할 수 있다.
'Dev Language > EffectiveJava' 카테고리의 다른 글
[EffectiveJava]명명패턴 대신 애너테이션을 사용하라 (0) | 2025.01.31 |
---|---|
[EffectiveJava]비트 필드 대신 EnumSet을 사용하라 (2) | 2025.01.25 |
[EffectiveJava] 이왕이면 제네릭 메서드로 만들라 (0) | 2025.01.11 |
[EffectiveJava]이왕이면 제네릭 타입으로 만들라 (0) | 2025.01.05 |
[EffectiveJava] 로 타입(raw type)은 사용하지 말라 (4) | 2024.12.29 |