열거타입은 일정 개수의 상수 값을 정희한 다음, 그 외의 값은 허용하지 않는 타입이다.

 

//가장 단순한 열거 타입
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),
    EARTH(5.975e+24, 6.378e6),
    MARS(6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN(5.685e+26, 6.027e7),
    URANUS(8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.447e7);

    // 중력상수 (단위: m^3 / kg s^2)
    private static final double G = 6.67300E-11;

    // 생성자
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        this.surfaceGravity = G * mass / (radius * radius);
    }

    private final double mass;            // 질량(단위: 킬로그램)
    private final double radius;          // 반지름(단위: 미터)
    private final double surfaceGravity;  // 표면중력(단위: m / s^2)

    public double surfaceWeight(double mass) {
        return mass * surfaceGravity; // F = ma
    }
}
  • 열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
  • 필드를 public으로 선언해도 되지만, private으로 두고 별도의 public 접근자 메소드를 두는게낫다.

 

Planet 열거 타입은 단순하지만 놀랍도록 강력하다.

어떤 객체의 지구에서의 무게를 입력받아 여덟 행성에서의 무게를 출력하는 일을 다음처럼 짧은 코드로 작성할 수 있다.

public class WeightTable {
    public static void main(String[] args) {
        double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for (Planet p : Palanet.values()) 
            System.out.println("%s에서 무게는 %f이다. %n", p, p.surfaceWeight(mass));
    }
}

열거타입이 지원하는 메소드

  • value() : 자신 안에 정의된 상수들의 값을 배열에 담아 반환
  • toString() : 상수 이름을 문자열로 반환

Planet 예시로 열거타입에 대해 자세히 설명할 수 있지만 더 다양한 기능을 제공해줬으면 할 때도 있다.

 

Planet 상수들은 서로 다른 데이터와 연결되는 데 그쳤지만

한걸음 더 나아가 상수마다 동작이 달라져야 하는 상황도 있을 것이다.

 

사칙연산 계산기를 예시로 봐보자.

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);
    }

}

switch문을 이용해 상수의 값에 다라 분기하는 방법을 사용했는데 이 예시는 깨지기 쉬운 코드이다.

예컨대 새로운 상수를 추가하면 해당 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);
}

한편, 상수별 메소드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.

 

 

 

급여명세서에서 쓸 요일을 표현하는 열거타입을 예시로 보자.

enum PayrollDay {
    MONDAY, TUESDAY, WEDSDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    private static final int MINS_PER_SHIFT = 8 * 60;

    int pay(int minutesWorked, int payRate) {
        int basePay = minutesWorked * 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 + overtimePay;
    }
}

이 열거 타입은 직원의 기본 임금과 그날 일한 시간이 주어지면 일당을 계산해주는 메소드를 가지고 있다.

 

분명 switch를 이용한 간결한 방법이지만 관리관점에서는 위험한 코드이다.

휴가와 같은 새로운 값을 열거 타입에 추가하려면 그 값을 처리하는 case문을 이지 말고 쌍으로 넣어줘야하기 때문이다.

 

 

이를 개선하는 가장 깔끔한 방법은 새로운 상수를 추가 할 때 잔업수당 '전략'을 선택하도록 하는 것이다.

 

잔업수당 계산은 private 중첩 열거 타입으로 옮기고 payRollDay 열거 타입으로 옮기고

payrollDay 열거타입의 생성자에서 이 중 적당한 것을 선택한다.

enum PayrollDay {
    MONDAY, TUESDAY, WEDSDAY, THURSDAY, FRIDAY, 
    SATURDAY(PayTyoe.WEEKEND), SUNDAY(PayType.WEEKEND);

    private final PayType payType;

    PayrollDya(PayType payTyoe) {this.payType = payType;}

    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }

    /* 전략 열거 타입 */
    enum PayType {
        WEEKDAY {
            int overtimePay(int minusWorked, int payRate) {
                return minusWorked <= MINS_PER_SHIFT ? 0 :
                (minusWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            int overtimePay(int minusWorked, int payRate) {
                return minusWorked * 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);
        }
    }
}

필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 항상 열거타입을 사용하자

열거 타입에 정의된 상수 개수가 영훤히 고정 불변일 필요는 없다.

열거 타입은 나중에 상수가 추가돼도 바이너리 수준에서 호환되도록 설계되었다.

정리

  • 열거 타입은 정수 상수보다 읽기 쉽고 안전하고 강력하다.
  • 대부분 열거 타입을 명시적 생성자나 메소드 없이 쓰지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작할 때는 필요하다.
  • 드물에 상수별로 다르게 동작해야 할때는 switch를 사용하자
  • 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.

+ Recent posts