Java/Effective Java

[Effective Java] 6장. 열거 타입과 애너테이션

SeungbeomKim 2025. 5. 20. 23:31

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

  • enum type 나오기 전에는 정수 열거 패턴(int enum pattern)을 사용해 왔습니다.
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;

public static final int ORANGE_NEVEL = 0;
public static final int ORANGE_TEMPLE = 1;

 

  • 위와 같은 정수 열거 패턴은 타입 안정성을 보장하기 어렵습니다.
  • 위의 대안으로 나온 것이 열거 타입입니다.

 

열거 타입

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

 

장점

  • 완전완 형태의 클래스입니다.
  • 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final로 볼 수 있습니다.
  • 인스턴스가 하나만 존재합니다.
  • 열거 타입은 컴파일 시점에서 타입 안정성을 제공합니다.
  • 열거 타입의 toString 메서드는 출력하기에 적합한 문자열을 제공합니다.
  • 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현할 수 있습니다.

 

Example

enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS(4.869e+24, 6.052e6),
    EARTH(5.975e+24, 6.378e6);
    // ...

    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 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 이어야 합니다.
  • 열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드 values를 제공합니다.

 

[item-35] ordinal 메서드 대신 인스턴스 필드를 사용하라

  • ordinal 메서드: 해당 상수가 열거 타입에서 몇 번째인지 반환하는 메서드
  • 가장 첫 번째 상수는 0을 반환합니다.

ex) ordinal 메서드 예시

  • 상수의 선언을 바꾸는 순간 오동작을 할 수 있으며, 이미 사용 중인 정수와 값이 같은 상수는 추가할 수도 없습니다.
public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;

    public int numberOfMusicians() { return ordinal() + 1; }   
}

 

해결책

  • 열거 타입 상수에 연결된 값은 ordinal 메서드로 얻지 말고, 인스턴스 필드에 저장
public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), NONET(9), DECTET(10),
    DOUBLE_QUARTET(8), TRIPLE_QUARTET(12);

    private final int int numberOfMusicians;
    Ensemble(int size) { this.numberOfMusicians = size; }
    public int numberOfMusicians() { return numberOfMusicians; }
}

 

[item-36] 비트 필드 대신 EnumSet을 사용하라

 

예전에는 정수 열거 패턴에 비트 필드 표시

public class Text {
    public static final int STYLE_BOLD          = 1 << 0;  // 1
    public static final int STYLE_ITALIC        = 1 << 1;  // 2
    public static final int STYLE_UNDERLINE     = 1 << 2;  // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8

    // 매개변수 styles는 0개 이상의 STYLE_ 상수를 비트별 OR한 값이다.
    public void applyStyles(int styles) { ... }
}

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
  • 비트 필드: 비트별 OR연산을 이용하여 여러 상수를 하나의 집합으로 모으기 위한 필드

 

단점

  • 정수 열거 상수의 단점을 그대로 가져옵니다.
  • 비트 필드에 포함된 모든 의미상의 원소를 순회하기도 어렵고 최대 몇 비트가 필요한지 미리 예측한 후 타입을 선택해야 합니다.

 

해결책: EnumSet 활용

  • Google의 Guava Library인 Collections.unmodifiableSet을 사용하면 불변 상태로 만들 수 있습니다.
public class Text {
    public enum Style { BOLD, ITALIC, INDERLINE, STRIKETHROUGH }

    // 깔끔하고 안전하다. 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
    // 보통 인터페이스를 전달 받는 것이 좋은 습관이다.
    public void applyStyles(Set<Style> styles) { ... }
}

// Guava 라이브러리 사용
Set immutableEnumSet = Collections.unmodifiableSet(EnumSet.of(Text.Style.BOLD, Text.Style.ITALIC));
immutableEnumSet.add(Text.Style.INDERLINE); // java.lang.UnsupportedOperationException

 

[item-37] ordinal 인덱싱 대신 EnumMap을 사용하라

  • ordinal 메서드를 배열 인덱스로 사용하면 위험합니다.
// 배열은 제네릭과 호환되지 않으니 비검사 형변환도 필요
Set<Plant>[] plantByLifeCycle = 
    (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];

for (int i = 0; i < plantsByLifeCycle.length; i++) {
    plantsByLifeCycle[i] = new HashSet<>();
}

for (plant p : garden) {
    plantsByLifeCycle[p.lifeCycle.ordinal()].add(p); -> 문제 부분
}

// 결과 출력
for (int i = 0; i < plantsByLifeCycle.length; i++) {
    System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
  • 배열은 각 인덱스의 의미를 모르기 때문에, 위 코드에서의 %s %s\n과 같은 출력 결과를 포맷팅 해야 합니다.

 

해결책

 

1. EnumMap 사용

// EnumMap을 사용하여 데이터와 열거 타입을 매핑한다.
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
    new EnumMap<>(Plant.LifeCycle.class);

for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
    plantsByLifeCycle.put(lc, new HashSet<>());
}

for (Plant p : garden) {
    plantsByLifeCycle.get(p.lifeCycle).add(p);
}
System.out.println(plantsByLifeCycle);

 

 

2. 스트림(Stream) 사용

// Map을 이용해 데이터와 열거 타입 매핑
Arrays.stream(garden)
    .collect(groupingBy(p -> p.lifeCycle))

// EnumMap을 이용해 데이터와 열거 타입 매핑
Arrays.stream(garden)
    .collect(groupingBy(
        p -> p.lifeCycle, 
        () -> new EnumMap<>(LifeCycle.class),
        toSet())
    );

 

 

[item-38] 확장할 수 있는 열거타입이 필요하면 인터페이스를 사용하라

  • 열거 타입을 확장하는 것은 대부분 좋지 않습니다.
  • 하지만, 연산 코드(operation code)를 구현할 때는 어울릴 수 있습니다.
  • 이때는 열거 타입 enum이 인터페이스를 구현(implements)할 수 있다는 점을 이용하면 됩니다.
public interface Operation {
    double apply(double x, double y);
}

public enum BasicOperation implements 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;
    
    BasicOperation(String symbol) { 
	    this.symbol = symbol; 
    }
    
    @Override public String toString() { 
	    return symbol; 
    }
}
  • 타입 수준에서도 기본 열거 타입 대신에 확장한 열거 타입을 넘겨서 열거 타입의 모든 원소를 순회할 수 있게 합니다.

 

[item-39] 명명 패턴보다 애너테이션을 사용하라

  • Junit3 version까지는 테스트 메서드의 이름이 test로 시작해야 했습니다.
  • test라는 이름이 없거나, 오타가 났다면 테스트 코드는 실행조차 되지 않았습니다.
  • Junit4부터 애너테이션을 도입하여 이러한 문제들을 해결할 수 있었습니다.
  • 애너테이션이 할 수 있는 일들을 명명 패턴으로 처리할 필요는 없습니다.

 

[item-40] @Override 애너테이션을 일관되게 사용하라

  • @Override: 상위 타입의 메서드를 재정의 하기 위한 애너테이션
  • 이러한 메서드를 일관되게 사용하면 발생할 수 있는 실수나 버그들을 줄일 수 있습니다.
  • 추상 메서드를 재정의 할 때는 제외하고는 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 다는 습관을 가져야 합니다. (위의 이유 때문에)

 

[item-41] 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라

  • Marker Interface: 아무 메서드도 갖고 있지 않고 단지 자신을 구현하는 클래스가 특정 속성을 갖는 것을 표현해 주는 인터페이스
  • ex) Serializable, Cloneable, @Target(ElementType.TYPE)
  • 직렬화: 객체를 바이트 스트림으로 변환하여 파일이나 네트워크 전송 등을 가능하게 하는 기능입니다.

마커 인터페이스 대표적 예시: Clonable, Serializable

마커 인터페이스의 장점

  1. 클래스의 인스턴스를 구분하는 타입으로 사용할 수 있습니다.
  2. 적용 대상을 더 정밀하게 지정할 수 있습니다.
  3. 거대한 애너테이션 시스템의 자원을 받습니다.