아이템 34. int 상수 대신 열거 타입을 사용하라

wisdom·2022년 8월 19일
0

Effetctive Java

목록 보기
34/80
post-thumbnail

✔️ 열거 타입 (Enum Type)

📌 열거 패턴 의 한계

자바에서 열거 타입을 지원하기 전에는 정수 열거 패턴(int enum pattern)을 사용하곤 했다.

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 GRAPE_NAVEL = 0;
public static final int GRAPE_TEMPLE = 1;
public static final int GRAPE_BLOOD = 2;

정수 열거 패턴에는 단점이 많다.

  1. 타입 안전을 보장할 수 없다.
  2. 표현력이 좋지 않다.
  3. 정수 열거 패턴을 위한 별도 이름공간(namespace)를 지원하지 않는다.
    즉, 이름 충돌을 방지하기 위해서는 접두어를 써서 구분해야 한다. (사과용 상수에는 APPLE, 오렌지용 상수는 ORANGE를 붙였다)
  4. 문자열로 출력하기가 까다롭다.
  5. 같은 정수 열거 그룹에 속한 모든 상수를 순회하는 방법이 마땅치 않다. (심지어 전체 상수 개수도 알 수 없다)

이러한 열거 패턴의 단점을 모두 해결해주는 것이 바로 열거 타입이다.

📌 열거 타입 이란?

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

자바의 열거 타입은 완전한 형태의 클래스라서 다른 언어의 열거 타입보다 훨씬 강력하다.

  • 열거 타입: 클래스. 외부에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final이다.
  • 상수: 상수 하나당 자신의 인스턴스를 하나씩 만들어, public static final 필드로 공개한다.

📌 열거 타입 의 장점

1. 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재한다.

즉, 열거 타입은 인스턴스 통제된다.

싱글턴은 원소가 하나뿐인 열거 타입이라고 할 수 있고, 열거 타입은 싱글턴을 일반화한 형태라고 볼 수 있다.

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

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

3. 열거 타입에는 각자의 이름 공간이 있다.

따라서 이름이 같은 상수가 있더라도 공존할 수 있다.

열거 패턴처럼 접두어를 써서 구분할 필요가 없는 것이다.

4. 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일하지 않아도 된다.

공개되는 것은 오직 필드의 이름뿐이라, 정수 열거 패턴과 달리 상수 값이 클라이언트로 컴파일되어 각인되지 않기 때문이다.

5. 열거 타입의 toString 메서드는 출력하기에 적합한 문자열을 반환한다.

각 열거 타입 값의 toString은 상수 이름을 문자열로 반환한다.

6. 열거 타입에 정의된 상수들의 값 배열을 반환하는 values 메서드를 제공한다.

열거 타입은 values 라는 정적 메서드를 통해 정의된 상수 값들을 순회할 수 있다.

이때 값들은 선언된 순서대로 저장된다.

7. 임의의 메서드나 필드를 추가할 수 있고, 임의의 인터페이스를 구현하게 할 수도 있다.

📌 열거 타입은 언제 써야 할까?

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

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

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


✔️ 데이터와 메서드를 갖는 열거 타입

각 상수와 연관된 데이터를 해당 상수 자체에 내재시키고 싶을 때는 어떻게 할까?

태양계의 8개 행성을 예로 보자.

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.477e7);

    private final double mass;           // 질량(단위: 킬로그램)
    private final double radius;         // 반지름(단위: 미터)
    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 / (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
    }
}
  • 각 열거 타입 상수 오른쪽 괄호 안 숫자는 생성자에 넘겨지는 매개변수다.

📌 열거 타입 상수 각각을 특정 데이터와 연결지으려면,

  • 특정 데이터의 필드 를 추가하고
  • 데이터를 받아 인스턴스 필드에 저장하는 생성자 를 추가하면 된다.
  • 생성자에 넘겨지는 데이터는 각 열거 타입 상수 오른쪽 괄호 안에 넣는다.

+) 열거 타입은 근본적으로 불변이라 모든 필드는 final 이어야 한다.(아이템 17)

+) 또한, 필드는 private 으로 두고 별도의 public 접근자 메서드 를 추가하는게 좋다.(아이템 16)


✔️ 상수별 메서드 구현 (Constant-specific method implementation)

상수마다 동작이 달라져야 하는 상황에서는 switch 문 보다는 상수별 메서드 구현(constant-specific method implementation)을 이용하자.

💡 상수별 메서드 구현 (constant-specific method implementation)

추상 메서드를 선언하고 각 상수별 클래스 몸체(constant-specific class body)를 상수에 맞게 재정의하는 방법이다.

다음은 상수별 메서드 구현을 활용한 계산기 예제다.

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;}};
	
	private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }

    @Override public String toString() { return symbol; }

	// 추상 메서드
	public abstract double apply(double x, double y);
}
  • apply 라는 추상 메서드를 선언하고, 각 상수에서 용도에 맞게 재정의하였다. (상수별 메서드 구현)
  • symbol 이라는 데이터를 추가(상수별 데이터)하고 toString 을 재정의하여, 계산식을 편하게 출력할 수 있도록 하였다.

위와 같이 toString 을 재정의하는 경우, toString 이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 fromString 메서드도 함께 제공하는 걸 고려해보자.

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

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

✔️ 전략 열거 타입 패턴

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

상수끼리 코드를 공유하고자 할 때는 전략 열거 타입 패턴 을 사용할 수 있다.

주중과 주말의 잔업수당을 구하는 예제를 살펴보자.

주중(weekday)에는 오버타임이 발생할 경우 잔업수당이 주어지고, 주말(weekend)에는 무조건 잔업수당이 주어진다.

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);
        }
    }
}
  • PayType 이라는 전략 열거 타입을 이용해, 주중과 주말의 잔업수당을 따로 계산하도록 하였다.

✔️ switch 문

상수별로 다르게 동작해야 할 때, 상수별 메서드 구현과 전략 열거 타입 패턴을 이용하였다.

switch 문의 경우, 코드가 예쁘지 않고, 깨지기 쉽다.

또한 새로운 상수를 추가하면 case 문도 추가해야 하고, 혹시라도 깜빡한다면 제대로 동작하지 않게 된다.

따라서, 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("Unknown op: " + op);
    }
}



📌 핵심 정리

  • 열거 타입은 확실히 정수 상수보다 뛰어나다. 더 읽기 쉽고 안전하고 강력하다.
  • 대다수 열거 타입이 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작하게 할 때는 필요하다.
  • 하나의 메서드가 상수별로 다르게 동작해야 할 때는, switch 문 대신 상수별 메서드 구현 을 사용하자.
  • 열거 타입 상수 일부가 같은 동작을 공유한다면, 전략 열거 타입 패턴 을 사용하자.
profile
백엔드 개발자

0개의 댓글