아이템 34. int 상수 대신 열거 타입을 사용하라. - ksw6169/effective-java GitHub Wiki

열거 타입(enum) 이란?

  • 열거 타입은 일정 개수의 상수 값을 정의한 다음 그 외의 값은 허용하지 않는 타입이다.
  • 사계절, 태양계의 행성, 카드게임의 카드 종류 등이 좋은 예다.
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

열거 타입의 장점

1. 불변이다.

  • 열거 타입은 하나의 클래스로 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
  • 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함을 보장한다.

2. 컴파일타입 타입 안전성을 제공한다.

  • 다른 타입의 값을 넘기려 하면 컴파일 오류가 발생한다.

3. 각자의 이름 공간이 있으므로 이름이 같은 상수도 공존할 수 있다.

  • Apple, Orange와 같은 열거 타입 내에 각각 같은 이름의 상수가 있더라도 충돌하지 않는다.

4. 열거 타입의 toString 메소드는 출력에 적합한 문자열을 반환한다.

  • 각 열거 타입 값의 toString 메소드는 상수 이름을 문자열로 반환한다.

5. 열거 타입에는 메소드, 필드를 추가하거나 인터페이스를 구현하게 할 수도 있다.

  • 또한 열거 타입은 Object 메소드들과 Comparable, Serializable을 잘 구현해 놓았기 때문에 문제없이 사용 가능하다.

열거 타입에서 지원하는 메소드

valueOf()

  • 열거 타입에는 상수 이름을 입력 받아 그 이름에 해당하는 상수를 반환해주는 valueOf 메소드가 자동 생성된다.

values()

  • 열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메소드인 values 를 제공한다.
  • 값들은 선언된 순서대로 저장된다.

열거 타입을 사용할 때 참고 사항

  • 열거 타입 상수를 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS(4.869e+24, 6.052e6),
    ...
    
    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 static final로 선언한 것이므로 열거 타입 생성자에서 정적 필드에 접근할 수 없다는 제약이 있다.)

상수별 메소드 구현(Constant-specific method implementation)

열거 타입을 사용할 때 상수마다 동작이 달라져야 하는 상황이 있을 수 있다. 이 때 switch 문을 사용한다면 다음과 같이 사용할 수 있다. 하지만 열거 타입에 상수가 추가되었을 때 case문을 깜빡하고 추가해주지 않으면 런타임 에러가 발생할 수 있다.

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

열거 타입은 상수별로 다르게 동작하는 코드를 구현하는 더 나은 수단을 제공한다. 즉, 열거 타입에 추상 메소드를 선언하고 각 상수에서 자신에 맞게 재정의하는 방법이다. 이를 상수별 메소드 구현이라 한다.

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

중첩 열거 타입

  • 상수별 메소드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.
  • 이를 해결하기 위해 모든 상수에 메소드를 중복해서 넣어주거나, 별도의 도우미 메소드로 만들어 각 상수가 자신에게 필요한 메소드를 적절히 호출하는 방식을 생각할 수 있지만 두 방식 모두 코드가 장황해지고 가독성이 크게 떨어지고 오류 발생 가능성이 높아진다.
  • 따라서 가장 깔끔한 방법으로 중첩 열거 타입을 사용하는 방식을 생각해볼 수 있다.
public enum PayrollDay {
    MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(PayType.WEEKDAY), THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY),
    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 minutesWorked, int payRate) {
                return minutesWorked <= MINS_PER_SHIFT ?
                        0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },

        WEEKEND {
            int overtimePay(int minutesWorked, int payRate) {
                return minutesWorked * payRate / 2;
            }
        };

        abstract int overtimePay(int minutesWorked, int payRate);

        private static final int MINS_PER_SHIFT = 8 * 60;

        int pay(int minutesWorked, int payRate) {
            int basePay = minutesWorked * payRate;
            return basePay + overtimePay(minutesWorked, payRate);
        }
    }
}

toString을 제공할 때 fromString도 함께 제공하는 것을 고려하라.

열거 타입의 toString 메소드를 재정의하려거든 toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 fromString 메소드를 함께 제공하는 것을 고려하라.

public enum Operation {

    PLUS("+") {
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    },

    MINUS("-") {
        @Override
        public double apply(double x, double y) {
            return x - y;
        }
    },

    TIMES("*") {
        @Override
        public double apply(double x, double y) {
            return x * y;
        }
    },

    DIVIDE("/") {
        @Override
        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);

    // key: symbol, value: operation
    private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(Collectors.toMap(Object::toString, e -> e));

    // 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
    public static Optional<Operation> fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }
}

위 예제에서 fromString은 Optional을 반환하는데 이는 주어진 문자열이 가리키는 연산이 존재하지 않을 수 있음을 클라이언트에 알리고 그 상황을 클라이언트에서 대처하도록 한 것이다.


열거 타입에서 switch 문이 유용한 상황

기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 switch 문이 좋은 선택이 될 수 있다. 즉, 기존 열거 타입에 없는 기능을 수행하고자 할 때(다른 상수에 정의된 동작을 수행하게 한다거나 등등) switch 문을 사용할 수 있다.

public static Operation inverse(Operation op) {
    switch(op) {
        case PLUS:   return Operation.MINUS;
        case MINUS:  return Operation.PLUS;
        case TIMES:  return Operation.DIVIDE;
        case DIVIDE: return Operation.TIMES;
        default:     throw new AssertionError("알 수 없는 연산: " + op);
    }
}

열거 타입의 성능

대부분의 경우 열거 타입의 성능은 정수 상수와 별반 다르지 않다. 열거 타입을 메모리에 올리는 공간과 초기화하는 시간이 들긴 하지만 체감될 정도는 아니다.


열거 타입은 언제 사용해야 하는가?

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


참고 자료

  • Effective Java 3/E
⚠️ **GitHub.com Fallback** ⚠️