Search
🙉

이펙티브 자바:: 아이템 34 <int 상수 대신 열거 타입을 사용하라>

Intro::

이펙티브 자바 정리본입니다.

결론

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

왜 열거 타입을 사용해야 하는가?

// 정수 열거 패턴 public static final int APPLE_FUJI = 0; public static final int APPLE_PIPPIN = 0; ...
Java
복사
정수 열거 패턴
타입 안전을 보장할 방법이 없습니다.
컴파일러가 경고를 하지 않습니다.
문자열 열거 패턴
상수의 의미를 출력할 수 있다는 점은 좋지만, 하드코딩을 유발합니다.
런타임 버그를 유발합니다.

열거 타입

열거 타입 자체는 클래스입니다.
상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개합니다.
열거 타입은 생성자를 제공하지 않으므로 사실상 final 이며, 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없으니 인스턴스들은 딱 하나만 존재하도록 보장됩니다.
싱글턴은 원소가 하나뿐인 열거 타입이라 할 수 있고, 반대로 열거타입은 싱글턴을 일반화한 형태라고 할 수 있습니다.
컴파일타임 타입 안전성을 제공합니다.
열거 타입에는 각자의 이름공간이 있어서 이름이 같은 상수도 공존가능합니다.
임의의 메서드나 필드를 추가할 수 있고, 임의의 인터페이스를 구현하게 할 수 있습니다.
열거 타입의 제거된 상수를 참조하는 클라이언트는 제거된 상수를 참조하는 줄에서 디버깅에 유용한 컴파일 오류를 확인할 수 있습니다.
열거 타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없습니다.
열거 타입의 정적 필드 중 열거 타입의 생성자에서 접근할 수 있는 것은 상수 변수뿐입니다.
switch 문은 열거 타입의 상수별 동작을 구현하는데 적합하지 않지만, 기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 좋은 선택이 될 수 있습니다.
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 } }
Java
복사

상수가 더 다양한 기능을 제공해줬으면 할 때

// 코드 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)); } }
Java
복사

전략 열거 타입

// 코드 34-9 전략 열거 타입 패턴 (218-219쪽) 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)); } }
Java
복사

그래서 열거 타입 언제 쓸까?

필요한 원소를 컴파일타임에 다 알 수 있는 상수집합이라면 항상 열거 타입을 사용하면 됩니다.
열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없습니다.

질문

왜 열거 타입 생성자에서 다른 정적 필드를 접근할 수 없는가?

자바에서는 클래스가 로드될 때 정적 필드와 정적 블록이 초기화됩니다. 하지만 열거 타입의 경우, 열거 상수(enum constant)가 먼저 인스턴스화되고, 그 이후에 정적 필드가 초기화됩니다. 이로 인해, 열거 타입의 생성자에서 정적 필드에 접근하면 예상치 못한 문제가 발생할 수 있습니다.
정적 필드는 클래스 로드 시에 한 번만 초기화되며, 여러 인스턴스에 걸쳐 공유됩니다.
열거 상수는 각각의 인스턴스가 클래스 로딩 시점에 생성됩니다. 즉, 정적 필드 초기화가 완료되기 전에 열거 상수의 생성자 호출이 먼저 발생합니다.
이 때문에 열거 상수의 생성자에서 정적 필드를 사용할 경우, 정적 필드가 아직 초기화되지 않았을 수 있습니다. 이는 프로그램에서 NullPointerException 또는 예기치 못한 동작을 일으킬 수 있는 이유입니다.
public enum ExampleEnum { CONSTANT_A, CONSTANT_B; private static final int STATIC_VALUE = initializeStaticValue(); // 정적 필드 // 생성자 ExampleEnum() { System.out.println("Constructor called for: " + this); // 이 시점에서 STATIC_VALUE는 아직 초기화되지 않았을 수 있음 System.out.println("STATIC_VALUE = " + STATIC_VALUE); } // 정적 필드 초기화 메서드 private static int initializeStaticValue() { System.out.println("Initializing STATIC_VALUE..."); return 100; } public static void main(String[] args) { // 열거 상수에 접근하여 초기화 강제 ExampleEnum[] values = ExampleEnum.values(); } }
Java
복사
Constructor called for: CONSTANT_A Constructor called for: CONSTANT_B Initializing STATIC_VALUE...
Java
복사

References::

이펙티브 자바 / 조슈아 블로크 지음 (프로그래밍 인사이트)