상수를 이용해서 열거 패턴을 사용하면 구현은 쉽지만 문제점이 있다. 바로 실수할 가능성이 높아지고, 컴파일러를 통해서 어떤 오류나 경고도 볼 수 없다. 코드를 살펴 보자
public class FruitFood {
public static final int APPLE_JUICE = 0;
public static final int APPLE_MANGO_JUICE = 1;
public static final int APPLE_CIDAR = 2;
public static final int ORANGE_JUICE = 0;
public static final int ORANGE_MANGO_JUICE = 1;
public static final int ORANGE_CIDAR = 2;
}
일부로 실수를 유도하도록 코드를 짯는데 만약에 APPLE_JUICE == ORANGE_JUICE 를 비교한다고 가정해보자 결과는 true이다. 우리는 의미상 구분을 했지만 정수형 상수이므로 컴파일러는 2개의 상수 간의 차이점을 인지하지 못한다. 그리고 정수형이기 때문에 실수로 다음과 같은 코드를 짤 수도 있다.
int mix_juice = APPLE_JUICE * ORANGE_JUICE;
진짜 이런 실수를 할 지는 모르겠지만, 실제로 이렇게 코드를 짜도 컴파일러는 어떤 오류도, 경고도 출력하지 않는다. 나중에 테스트 때 코드가 터지고 디버깅해서야 발견할 수 있는 코드이다. 심지어 단순 정수형이기 때문에 디버깅도 쉽지 않다.
위의 코드만으로는 모든 상수를 조회할 수 없다. 만약 조회하는 메서드를 만든다고 해도 문제이다. 정수형 값이 저장되어 있기 때문에 순회를 한다면 단순히 [0, 1, 2, 0, 1, 2]를 줄 것이다. 문제는 이렇게 주면 우리는 해당 숫자가 어떤의미를 가지고 있는지 파악하기 힘들다.
위의 2개의 문제는 enum으로 말끔히 해결할 수 있다. enum은 기본적으로 싱글턴 구조이며 열거형으로 선언한 상수를 인스턴스 클래스 형태로 제공한다. 기본이 class이기 때문에 왠만한 Object에서 제공하는 것 모두 제공하며, Comparable과 Serializable이 구현되어 있어 직렬화도 전혀 문제가 없다. 심지어 toString까지 지원해서 enum 인스턴스를 출력하면 인스턴스 이름을 자동으로 출력해준다.
enum FruitFood {
APPLE_JUICE, APPLE_MANGO_JUICE, APPLE_CIDER, ORANGE_JUICE, ORANGE_MANGO_JUICE, ORANGE_CIDER
}
위의 형태로 열거형으로 짜면 상수 조회 또한 매우 쉽다.
@Test
void test2() {
System.out.println(FruitFood.APPLE_CIDER.equals(FruitFood.ORANGE_CIDER));
System.out.println(Arrays.toString(FruitFood.values()));
}
위의 코드를 실행하면
다음과 같이 열거형 상수 인스턴스의 이름이 알아서 toString()이 구현되어 있어 따로 구현하지 않아도 쉽게 볼 수 있고 열거형 상수들의 목록을 조회해볼 수 있다.
java의 enum은 다른 언어와 달리 단순 열거형 상수만이 아닌 클래스를 제공한다. 그래서 abstract를 활용한 추상화 메서드나 인스턴스 메서드, 정적 메서드 모두 제공하고 필요하면 인스턴스 변수 또한 활용할 수 있다.
public enum Planet {
MERCURY(..., ....),
VENUS(.., ....),
EARTH(..., ....),
MARS(..., ....),
JUPITER(..., ....),
SATURN(..., ....),
URANUS(..., ....),
NEPTUNE(..., ....);
private final double mass;
private final double radius;
private final 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 mass() {return mass;}
public double radius() {return radius;}
public double surfaceGravity() {return surfaceGravity;}
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
클래스처럼 사용가능한 enum이다. enum을 활용하면 굉장히 쉽게 메서드를 제공해줄 수 있고, 다양하게 활용할 수 있다. 생성자가 private 또는 private-package인 이유는 오직 하나의 인스턴스만 생성되는 것이 보장되는 객체가 바로 enum이기 때문이다.
예를 들면 연산과 같은 기능을 상수화하고 싶을 수 있다. 각 객체별 메서드를 구현하고 순회할 때 특히 유연하다
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);
}
위와 같은 코드를 짜면 Operation.values()를 활용해 for문을 활용할 수 있다. 그 이외에도 메서드 자체를 객체화했기 때문에 다양하게 활용가능하다.
목록이 정해져 있고, if문을 줄이고 싶다면 전략 패턴을 활용할 수 있다.
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);
}
}
}
전략 패턴을 활용하면 if 문 없이 전략패턴으로 구현된 열거형 객체를 각 요일마다 주입해서 효율적으로 코드를 짤 수 있다.