Item34 - int 상수 대신 열거 타입을 사용하라(1)

2023. 12. 10. 14:38Book/이펙티브 자바

 

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

자바에서 열거 타입을 지원하기 전에는 다음 코드처럼 정수 상수를 한 묶음 선언해서 사용했다.

 

- 정수 열거 패턴

private static final int APPLE_FUJI     = 0;
private static final int APPLE_PIPPIN   = 1;
private static final int APPLE_GRANNY_SMITH = 2;
private static final int ORANGE_NAVEL  = 0;
private static final int ORANGE_TEMPLE = 1;
private static final int ORANGE_BLOOD  = 2;

 

단점.

 

 APPLE_XXXX이 들어올 곳에 ORANGE_XXXX가 들어와도 컴파일러는 모르고 APPLE_FUJI == ORANGE_NAVEL 은 true가 나온다. 타입 안전을 보장하지도 못하고, 표현력도 좋지 않다.

     

또한 같은 정수 열거 그룹에 속한 상수를 순회하는 것도 마땅치 않다.

 

-문자열 열거 패턴

private static final int APPLE_FUJI     = "apple fuji";
private static final int APPLE_PIPPIN   = "apple pippin";
private static final int APPLE_GRANNY_SMITH = "apple granny smith";

문자열 열거 패턴은 더욱 나쁘다. 하드코딩한 문자열에 오타가 있어도 컴파일러는 확인할 길이 없으며, 또한 문자열 비교에 따른 성능저하도 생긴다.

 

 

자바는 열거 패턴의 단점을 없애고 장점을 안겨주는 enum type이 있다.

public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}

 

이 열거 타입은 완전한 형태의 클래스다.

상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.

열거 타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final이다.

 

따라서 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나식만 존재한다.

 

장점.

컴파일타임 타입 안정성을 제공 + 표현력도 좋음

public enum Day {MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY}

public static void main(String[] args) {
    pringToday(Day.MONDAY);
    //pringToday(ORANGE.NAVEL); => 컴파일 타임 에러
}

public static void pringToday(Day day) {
    System.out.println(day.toString());
}

=>
MONDAY

 

 

또한 열거 타입에 임의의 메소드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수도 있다. Object의 공통 메서드들도 높은 품질로 구현해놨고 Comparable과 Serializable을 구현했으며, 직렬화 형태도 웬만큼 변형을 가해도 문제없이 동작하게끔 구현해놨다.

 

1. 열거 타입에 메서드나 필드 추가

태양계의 여덟 행성이 있고,

각 행성은 질량과 반지름, 표면중력 필드를 가지고 있다고 해보자.

표면 중력은 질량과 반지름을 이용해 게산할 수 있다.

 

// 코드 34-3 데이터와 메서드를 갖는 열거 타입 (211쪽)
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이어야 한다.

 

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

 

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

=>
MERCURY에서의 무게는 21.162775이다.
VENUS에서의 무게는 50.682856이다.
EARTH에서의 무게는 56.000000이다.
MARS에서의 무게는 21.257824이다.
JUPITER에서의 무게는 141.662049이다.
SATURN에서의 무게는 59.668790이다.
URANUS에서의 무게는 50.671907이다.
NEPTUNE에서의 무게는 63.630757이다.

 

 

 

tip*

- 열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은 private이나 package-private 메서드로 구현(아이템15)

- 널리 쓰이는 열거 타입은 톱레벨 클래스로 만들고 특정 톱레벨 클래스에서만 쓰인다면 멤버클래스로 만든다.(아이템24)

 

 

Planet 상수들은 서로 다른 데이터와 연결되는 데 그쳤지만, 한 걸음 더 나아가 상수마다 동작이 달라져야 하는 상황도 있다.

public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
    
    public double applu(double x, double y) {
        return switch (this) {
            case PLUS -> x + y;
            case MINUS -> x - y;
            case TIMES -> x * y;
            case DIVIDE -> x / y;
        };
    }
}

 

나쁘지 않아 보이지만 새로운 연산이 추가될 때 마다 case 문도 추가해야 한다. 혹시 깜빡한다면 런타임에 오류를 낼 수 있다. 더 좋은 대안은 열거 타입에 추상 메서드를 선언하고 각 상수에서 자신에 맞게 재정의하는 방법이다. 이를 상수별 메서드 구현이라고 한다.

 

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 메서드 구현을 깜빡한다면 컴파일 에러가 날 것이다. 또한 상수별 메서드 구현을 상수별 데이터와 결합할 수도 있다.

 

// 코드 34-6 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입 (215-216쪽)
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);

    // 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽)
    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));
    }

    public static void main(String[] args) {
        List<String> list = List.of("1.2", "3.4");
        double x = Double.parseDouble(list.get(0));
        double y = Double.parseDouble(list.get(1));
        for (Operation op : Operation.values())
            System.out.printf("%f %s %f = %f%n",
                    x, op, y, op.apply(x, y));

        Operation.PLUS.apply(1.2, 5.6);
    }
}

=>

1.200000 + 3.400000 = 4.600000
1.200000 - 3.400000 = -2.200000
1.200000 * 3.400000 = 4.080000
1.200000 / 3.400000 = 0.352941