🏠 int 상수 대신 열거 타입을 사용하라.
열거 타입이란 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입이다.
사계절, 태양계 행성, 카드게임 종류처럼 정해진 개수가 있다면 좋은 예시가 될 수 있다.
열거 타입이 나오기 전에는 정수 상수를 묶음으로 선언해서 사용했다.
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 BANANA_D = 0;
public static final int BANANA_E = 1;
public static final int BANANA_F = 2;
다음과 같은 정수 열거 패턴 기법은 타입 안전을 보장할 방법도 없으며, 표현력도 좋지 않다.
또한 충돌을 방지하기 위해 어쩔 수 없이 접두어를 사용해야한다.
💊 열거 타입 (enum type)
public enum Apple {A, B, C}
public enum Banana {D, E, F}
🍊 열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
또한 열거는 외부에서 접근할 수 있는 생성자를 제공하지 않기 때문에 사실상 final 클래스이다.
따라서 클라이언트가 인스턴스를 직접 생성, 확장할 수 없기에 열거 타입으로 선언된 인스턴스는 오로지 하나임이 보장된다.
🍎 열거 타입은 컴파일타임에 타입 안정성을 제공한다. 위의 Apple 열거 타입을 매개변수로 받는 메서드가 있다고 가정해보자.
null 이 아니라면, 해당 메서드가 건네받은 참조값은 Apple 의 A, B, C 값중 하나임이 보장되며 그 외 값은 컴파일오류가 발생한다.
🍏 열거 타입은 각자의 이름공간이 있기 때문에 이름이 같은 상수도 평화롭게 공존한다. 또한 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일 하지 않아도 되며 열거 타입의 toString 메서드는 적합한 출력값을 제공해준다.
🌈 열거 타입에 메서드나 필드가 필요한 경우
가볍게는 그저 상수 모음이지만, 실제로는 클래스기 때문에 메서드나 필드 값을 추가할 수 있다.
예를 들어 태양계의 여덟 행성을 열거 타입으로 정의한다고 가정해보자.
각 행상은 질량과 반지름이 있고, 이 속성을 필드 값으로 지정해 메서드를 통해 표면 중력 또한 계산해 반환할 수 있다.
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
...
private final double mass;
private final double radius;
private infal double surfaceGravity;
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 surfaceWeight(double mass) {
return mass * surfaceGravity;
}
... // Getter
}
열거 타입 상수 각각을 특정 데이터와 연결 지으려면, 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
열거 타입은 근본적으로 불변이기 때문에 모든 필드는 final 이어야한다.
또한 public 으로 공개하는 것보단, private 로 둔 후 별도의 public 접근자 메서드로 두는게 낫다.
Planet 열거 타입은 단순하지만 강력하다.
어떤 객체에서 지구의 무게를 입력 받아 여덟 행성에서의 무게를 출력하는 일을 짧은 코드로 완성할 수 있다.
열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메소드 values 를 제공하는 덕분이다.
public class WeightTable {
public static void main(String[] args) {
double earthWeight = DOuble.parseDouble(args[0]);
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.println("%s에서의 무게는 %f이다.", p, p.surfaceWeight(mass));
}
}
상수들 중 하나를 삭제하거나 추가되도 문제없다. 만약 삭제한 상수를 참조하는 클라이언트에서는 컴파일 오류가 발생한다.
패키지 내에서만 사용하고 싶다면 private 혹은 package-private 로 선언하면 된다.
널리 쓰이는 열거 타입은 톱 레벨 클래스로 만들고, 특정 톱 레벨 클래스에서만 사용된다면 멤버 클래스로 만들면 된다.
☀️ 상수마다 메서드의 동작이 달라져야 한다면 ?
계산기는 사칙연산이 뭐냐에 따라 다르게 동작한다. 이를 열거타입으로 같은 메소드를 통해 구현하려면 어떻게 해야할까 ?
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; }
},
DEVIDE("/") {
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);
}
이러한 상수별 메서드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 존재한다.
만약 급여명세서에서 쓸 요일을 표현하는 열거 타입을 만든다고 가정해보자.
열거 타입은 직원의 기본 임금과, 일한 시간이 주어지면 일당을 계산해주는 메서드를 가지고 있다.
주중에 오버타임이 발생한다면 잔업 수당이 추가로 주어지며, 주말에는 반드시 잔업 수당이 추가로 주어져야 한다.
상수별 메서드 구현에서 주중과 주말 휴가 등을 포함해 급여를 정확히 계산하는 방법은 두가지이다.
- 잔업 수당을 계산하는 코드를 모든 상수에 중복해서 넣는다.
- 계산 코드를 평일용과 주말용으로 나눠 각 도우미 메서드로 작성한 다음, 상수가 자신에게 필요한 메서드를 호출하게 한다.
그러나 두 코드 장황해서 가독성이 떨어지고 오류 발생 가능성이 높아진다.
가장 깔끔한 방법은 잔업 수당 계산을 private 중첩 열거 타입으로 옮긴후, PayrollDay 열거 타입 생성자에서 이 중 적당한 것을 선택하면 된다. 코드로 확인해보자.
enum PayrollDay {
MONDAY(WEEKDAY), ..., SATURDAY(WEEKEND), SUNDAY(WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
int pay(int minWorked, int payRate) {
return payType.pay(minWorked, payRate);
}
//전략 열거 타입 패턴
enum PayType {
WEEKDAY {
int overtimePay(int minWorked, int payRate) {
return minWorked <= MINS_PER_SHIFT ? 0 :
(minWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minWorked, int payRate) {
return minWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minWorked, int payRate) {
int basePay = minWorked * payRate;
return basePay + overtimePay(minWorked, payRate);
}
}
}
이처럼 switch 문 보다는 상수별 메서드 구현이 더 좋은 방식이 될 수 있다.
하지만 기존 열거 타입에 상수별 동작을 혼합해 넣을 때에는 switch 문이 더 좋은 선택이 될 수도 있다.
💫 결론
열거 타입은 필요한 원소를 컴파일타임에 다 알수 있는 상수 집합이라면 반드시 열거 타입을 사용하자.
태양계 행성, 한 주의 요일, 체스 말처럼 정해져 있는 상수 집합 또한 말이다.
열거 타입에 정의된 상수 개수가 영원히 고정될 필요는 없다.
열거 타입은 확실히 정수 상수보다 뛰어나고 안전하고 읽기 쉽다.
대다수 열거 타입이 명시적 생성자나 메서드 없이 사용되지만, 각 상수를 특정 데이터와 연결 짓거나 상수마다 다르게 동작해야 할 때에는 필요하다. 그 때에는 switch 문 대신 상수별 메서드 구현을 사용하고, 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자
'개인 공부 > 스터디' 카테고리의 다른 글
[Real MySQL] 인덱스 (0) | 2023.08.03 |
---|---|
[이펙티브 자바] 열거타입과 인터페이스 (0) | 2023.07.11 |
[이펙티브 자바] 제네릭 메서드 작성법 (0) | 2023.06.26 |
[이펙티브 자바] 제네릭 (0) | 2023.06.21 |
[이펙티브 자바] 인터페이스의 용도 (0) | 2023.06.20 |