본문 바로가기

Dev Book Review/Effective Java

[Effective Java] item 34. int 상수 대신 열거 타입을 사용하라

열거타입 (enum) : 일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않는 타입

정수 열거 패턴 (int enum pattern) : 이전까지 사용하던 패턴

 

1. 정수 열거 패턴 (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 ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
  • 타입 안전을 보장할 방법이 없으며 표현력이 좋지 않다.
    Apple에서 Orange를 사용해도 컴파일러의 경고 메세지가 없다.
  • 접두어를 사용한 이름 충돌을 방지하는 방법을 사용한다.
  • 평범한 상수 나열이라, 컴파일 하면 그 값이 그대로 새겨지기 때문에 프로그램이 깨지기 쉽다.
  • 정수 열거 그룹에 속한 모든 상수를 한 바퀴 순회하는 방법도 마땅치 않으며 상수가 몇개인지도 알 수 없다.

 

2. 문자열 열거 패턴(string enum pattern)

정수 열거 패턴보다 더 나쁘다

private final String APPLE = "1";
private final String GRAPE = "2";
private final String ORANGE = "3";
  • 문자열에 오타가 있어도 컴파일러에서 확인할 길이 없어 런타임 버그
  • 문자열 비교에 따른 성능저하

 

3. 열거 타입 (enum)

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

완전한 형태의 클래스 (정수값 뿐인)라서 다른 언어의 열거 타입보다 훨씬 강력하다.

 

a. 열거 타입의 아이디어

  • 열거 타입은 클래스
  • 상수 하나당 자신의 인스턴스를 하나씩 만들어서 public static final 필드로 공개한다.
  • 열거 타입은 final이다 : 밖에서 접근가능한 생성자를 제공하지 않는다.
  • 열거 타입 선언으로 만들어진 인스턴스는 딱 1개만 존재한다.
 

b. 열거 타입과 싱글턴

  • 열거 타입은 인스턴스 통제클래스이다 : 언제 어느 인스턴스를 살아 있게 할지를 통제할 수 있음 (정적 팩터리 방식 클래스)
  • 싱글턴 = 원소가 하나뿐인 열거타입
  • 열거타입 = 싱글턴을 일반화한 형태
 

c. 열거타입의 장점

  • 컴파일 타입 안전성 제공 : Apple 열거타입 인수에 Orange를 넘길 수 없음
  • 이름 같은 상수 공존 : 각자의 이름공간이 있기 때문! 공개 되는 것이 필드의 이름이라 상수 값이 클라이언트에 컴파일 되어 각인되지 않기 때문이다.
  • 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수 있다.
  • 상수를 하나 제거했을 때 : 제거한 상수를 참조하지 않는 클라이언트에 아무 영향이 없다.
    참조를 한 클라이언트에서는 컴파일(런타임-다시 컴파일 X일 때) 오류가 발생할 것! (정수 열거 패턴에서는 기대할 수 없는 대응)

 

4. 데이터와 메서드를 갖는 열거 타입

각 상수와 연관된 데이터를 해당 상수 자체에 내재시킨다.

고차원의 추상 개념 하나를 표현 할 수 있다.

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이야 한다.
  • 필드를 private으로 두고 별도의 public 접근자 메서드를 두자
 

a. 열거타입의 배열 values()

자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드 값들은 선언된 순서로 저장된다.

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

b. 열거타입을 올바르게 사용하기

  • 일반 클래스와 마찬가지로 기능을 클라이언트에게 노출해야할 합당한 이유가 없다면 private으로, 혹은 (필요하다면) package-private으로 선언하라
  • 널리 쓰이는 열거타입 = 톱레벨 클래스로 구현
  • 특정 톱레벨 클래스에서만 사용 = 해당 클래스의 멤버 클래스로 구현

 

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

switch를 이용한 구현은 새로운 상수를 추가할 때마다 해당 case문도 추가해야해서 깨지기 쉽다.

상수별 메서드 구현

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

import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;

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

    public abstract double apply(double x, double y);    
}

열거 타입의 valueOf(string)

상수 이름을 입력받아 이 이름에 해당하는 상수를 반환해 주는 메서드

fromString 메서드 제공

열거타입의 toString을 재정의 할 때 함께 제공하는 걸 고려해보자.

toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 메서드

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

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

열거타입 정적 필드의 생성 시점

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

Operation 상수가 stringToEnum 맵에 추가되는 시점 : 열거타입 생성 후 정적 필드가 초기화 될 때

열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다 : 컴파일 오류

  • 열거 타입의 정적 필드 중 열거 타입 생성자에서 접근 할 수 잇는 것은 상수 변수 뿐이다.
  • 열거 타입 생성자 실행 시점에는 정적 필드 초기화 전이다.
  • 열거 타입 생성자에서 같은 열거 타입의 다른 상수에도 접근 할 수 없다.
    (열거 타입의 인스턴스를 public static final으로 선언함. 다른 형제 상수도 static이므로 열거 타입 생성자에서 정적 필드에 접근할 수 없다는 제약이 적용된다.)

 

6. 상수별 동작 혼합 : 전략 열거 타입 패턴

열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.

package effectivejava.chapter6.item34;

import static effectivejava.chapter6.item34.PayrollDay.PayType.*;

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

    public static void main(String[] args) {
        for (PayrollDay day : values())
            System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
    }
}

추가하려는 메서드가 의미상 열거타입에 속하는 경우 다음과 같이 전략 열거 타입 패턴을 사용한다.

그렇지 않은 경우에는 switch를 적용해서 간단하게 만든다.

 

7. 열거타입을 사용해야 할 때

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

    Ex) 태양계 행성, 한 주의 요일, 체스말

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

    Ex) 메뉴 아이템, 연산 코드, 명령줄 플래그

  • 열거타입의 성능은 상수와 별반 다르지 않다