자바에서 열거 타입이 생기기 전까지는 정수 상수를 열거하는 방법을 사용하였다.
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;
🚫 위와 같은 방식의 단점
- 타입 안전 보장 X
- 컴파일러가 이해하는 값은 정수이므로 ORANGE_NAVEL 을 건네야 하는 곳에 APPLE_FUJI를 보내고 동등연산자로 비교하더라도 컴파일러는 틀린것을 모름.
- 상수의 값이 바뀌면 클라이언트도 다시 컴파일 해야함.
- 문자열로 출력하기가 까다로움
하지만 열거타입을 사용하면 위의 단점을 해결할 수 있다❗
우선 열거타입이 무엇인지 알아보자.
열거타입이란??
서로 연관된 상수의 집합을 저장하는 자료형이다.
요일을 나타내는 월, 화, 수, 목, 금, 토, 일과 계절을 나타내는 봄, 여름, 가을, 겨울도 예로들 수 있다.
그럼 아까 위에서 나왔던 예제를 열거타입으로 바꿔보자.
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }
훨씬 간결해 진것을 알 수 있다.
열거타입의 특징을 알아보자.
- 열거타입 자체는 클래스 이다.
- 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
- 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final 이다.
- 컴파일타임 타입 안정성을 제공한다. ( Apple 열거타입을 매개변수로 받는 메서드를 선언했다면, Apple의 세가지 값 중 하나임이 확실하다. )
여기서 끝이 아니라 열거 타입에는 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수도 있다.
열거 타입에 메서드나 필드를 추가하는건 뭘까?
아래는 열거 타입에 메서드와 필드를 추가한 예이다.
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARlli ( 5 • 975e+24, 6 • 378e6 ) ,
MARS (6.419e+23, 3,393e6),
JUPITER( l. 899e+27, 7.149e7 ) ,
SATIJIIII (5,685e+26, 6,027e7),
URANUS (8.683e+25, 2,556e7),
NEPT1.NE ( l. 024e+26, 2 • 477e7 ) ;
private final double mass; II 질량(단위: 킬로그램)
private final double radius; II 반지롤(단위: 미터)
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 I ( radius * radius ) ;
}
publie double mass ( ) { retu rn mass ; }
public double radius( ) { return radius; }
public double surfaceGravity( ) { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; II F = ma
}
}
위의 Planet 열거 클래스에서 어떤 객체의 지구에서의 무게를 입력받아 여덟 행성에서의 무게를 출력하는 것을 출력해보자.
public class WeightTable {
public static void main(String 11 args) {
double earthWeight = Double. parseDouble(args [0] ) ;
double mass = earthWeight / Planet. EARTH. surfaceGravity() ;
for (Planet p : Planet.values())
System.out.printf("%s에서의 무게는 합이다.%11",p, p.surfaceWeight(mass));
}
}
✅ 출력결과
MERCURY'에서의 무게는 69.912739이다.
VENUS에서의 무게는 167.434436이다.
EAR뼈人1의 무게는 185.00000001다.
MARS에서의 무게는 70.226739<시다.
JUPITER에서의 무게는 467.990696이다.
SAlURI떠서의 무게는 197.120111이다.
URPNUS에서의 무게는 167.398264이다.
NEPlUNE에서의 무게는 210.208751이다.
위 예제에서 알 수 있듯이 열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드인 values 를 제공한다. 값들은 선언된 순서로 저장되며 각 열거 타입 값의 toString 메서드는 상수 이름을 문자열로 반환하므로 println과 printf 로 출력하기에 딱 좋다.
근데 만약 위의 열거타입에서 상수를 하나 제거하면??
➡️ 제거한 상수를 참조하지 않는 클라이언트에는 아무 영향이 없다.
Planet 예에서 보여준 특성만으로 열거 타입을 사용하는 상황 대다수를 훌륭히 설명할 수 있다. 하지만 만약 상수마다 동작이 달라져야 하는 상황에는 어떻게 해야할까?
우선 switch 문을 이용해 상수의 값에 따라 분기하는 방법을 시도해보자.
열거 타입의 상수 별 메서드 구현
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 문도 추가해야 하며, 혹시라도 추가를 깜빡하면 컴파일은 되지만 새로 추가한 연산을 수행하려 할 때 “알 수 없는 연산” 이라는 런타임 오류를 내며 프로그램이 종료된다.
다행히 열거 타입은 상수별로 다르게 동작하는 코드를 구현하는 더 나은 수단을 제공한다.
상수별 메서드 구현
열거타입에 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;}},
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 메서드가 상수 선언 바로 옆에 붙어 있으니 새로운 상수를 추가 할 때 apply 도 재정의해야 한다는 사실을 잊기 어렵다.
✅ 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; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE ( III" ) {
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);
}
위의 예제가 얼마냐 편하냐면
public static void main(String [] args) {
double x = Double.parseDouble (args[0]); // 2
double y = Double.parseDouble(args[1]); // 4
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n", x, op, y, op. apply(x, y));
}
아래를 출력하기위해 위의 간단한 코드이면 된다.
✅ 실행결과
2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000
마지막으로 전략 열거 타입 패턴을 알아보자. 전략 열거 타입 패턴은 아래와 같은 경우를 해결하기 위해 등장했다.
enum PayrollDay {
MONDAY, TUESDAY, WECNESDAY, THURSDAY, FRIDAY, SATURDAY, SLNDAY;
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minutesWorked, int payRate) {
int basePay = minutesWorked * payRate;
int overtimePay;
switch(this) {
case SATlltDAY : case SUNDAY : // 주말
overtimePay = basePay / 2;
break;
default : // 주중
overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
}
return basePay + overtimePay;
}
}
➡️ 분명 간결하고 정상으로 보지이지만 위험한 코드다. 휴가와 같은 새로운 값을 열거 타입에 추가하려면 그 값을 처리하는 case 문을 쌓으로 넣어줘야 한다. 안그러면 휴가 기간에 열심히 일해도 평일과 똑같은 임금으로 처리되는 불상사가 나타난다!!
이를 해결하기위한 전략 열거 타입 패턴을 알아보자.
전략 열거 타입 패턴
enum PayrollDay {
MONDAY, TUESDAY, WE마JESDAY, THURSDAY, FRIDAY,
SATURDAY(PayType.WEEKEND), SUNDAY(PayType.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;
},
}
WEEKBIID {
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(minsWorkecl, payRate) ;
}
}
}
➡️ 잔업수당 계산을 PayType 으로 옮기고 PayrollDay 열거 타입의 생성자에서 이 중 적당한 것을 선택한다. 그러면 PayrollDay 열거 타입은 잔업수당 계산을 그 전략 열거 타입에 위임하여, switch 문이나 상수별 메서드 구현이 필요 없게 된다. 복잡하지만 switch 문보다 안전하고 유연하다.
핵심정리❗❗
열거 타입은 확실히 정수 상수보다 뛰어나다. 더 읽기 쉽고 안전하고 강력하다.
대다수 열거 타입이 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작하게 할 때는 필요하다.
드물게는 하나의 메서드가 상수별로 다르게 동작해야 할 때도 있다. 이런 열거 타입에서는 switch 문 대신 상수별 메서드 구현을 사용하자. 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.
참고
도서 - Effective Java
'Java' 카테고리의 다른 글
Item40. @Override 애너테이션을 일관되게 사용하라. (0) | 2022.06.13 |
---|---|
Item31. 한정적 와일드카드를 사용해 API 유연성을 높이라. (0) | 2022.06.04 |
Item25. 톱레벨 클래스는 한 파일에 하나만 담으라. (0) | 2022.05.24 |
SOLID 좋은 객체지향 설계의 5가지 원칙 (0) | 2022.05.19 |
다형성, 오버라이딩, 오버로딩 (0) | 2022.05.19 |
댓글