devseop08 님의 블로그

[OOP] 열거형 본문

Language/Java

[OOP] 열거형

devseop08 2025. 9. 18. 22:36

열거형 선언 방법

  • static final 상수들을 나열: 양이 증가할 수록 유지와 관리 어려움
class Card {
    static final int CLOVER = 0; 
    static final int HEART = 1; 
    static final int DIAMOND = 2; 
    static final int SPADE = 3;

    static final int TWO = 2; 
    static final int THREE = 3; 
    static final int FOUR = 4; 

    final int kind;
    final int num;
}
  • 열거형을 선언: enum 열거형 이름 { 상수명1, 상수명2, 상수명3} 형식으로 선언
enum Kind {
    CLOVER, 
    HEART, 
    DIAMOND, 
    SPADE
}

enum Value {
    TWO, 
    THREE, 
    FOUR
}

class Card {
    final Kind kind;
    final Value num;
}

열거 객체의 정적 초기화와 싱글턴 보장 방식

  1. 컴파일 시점의 구조: enum은 컴파일되면 사실상 java.lang.Enum을 상속하는 final 클래스가 된다.
enum Status{ READY, RUNNING } 
  • 컴파일 후 javap -c 명령어로 해당 enum의 클래스 파일을 디컴파일 시: 정적 초기화가 이루어지는 싱글턴 패턴 구조로 컴파일된 것을 확인 가능
    • 모든 상수는 정적 초기화 블록에서 한 번만 인스턴스화
    • 생성자는 private이고, 컴파일러가 강제로 private로 제한
    • 따라서 클래스 외부에서는 절대 new로 인스턴스를 새로 생성할 수 없다.
public final class Status extends java.lang.Enum<Status> {
    public static final Status READY; 
    public static final Status RUNNING;

    private static final Status[] $VALUES;

    static { // 정적 초기화
        READY = new Status("READY", 0);
        RUNNING = new Status("RUNNING", 1);
        $VALUES = new Status[] { READY, RUNNING };
    }

    private Status(String name, int ordinal) { // 싱글턴 패턴을 위한 private 생성자
        super(name, ordinal); 
    }

    public static Status[] values() {
        return $VALUES.clone();
    }

    public static Status valueOf(String name) { ... }
}
  1. JVM 클래스 로더 관점
    • enum 클래스는 JVM의 클래스 로더 단위로 관리된다.
    • 클래스 로딩 시 힙 영역에 1회 초기화/생성 -> Runtime Data Area의 메서드 영역에서 참조가 공유된다
    • 따라서 enum 상수, 즉 열거형 객체는 클래스 언로드 전까지 GC에 회수되지 않고 유지된다.
┌───────────────────────────────┐
│         메소드 영역            │  (JDK 8 이후 Metaspace)
│  - 클래스 메타데이터            │
│    (클래스 이름, 필드, 메서드) │
│  - enum 정의 정보              │
│    ex) enum Status {           │
│           READY, RUNNING, ...  │
│        }                       │
└───────────────┬───────────────┘
                │
      클래스 로딩 시 enum 메타데이터 저장
                │
┌───────────────▼───────────────┐
│              힙                │
│  - enum 상수 객체              │
│    Status.READY                │
│    Status.RUNNING              │
│    Status.DONE                 │
│  → JVM이 클래스 초기화 시 단 1회 생성 │
│  → 모든 참조는 이 객체를 공유       │
└───────────────┬───────────────┘
                │
      참조를 통해 enum 상수 사용
                │
┌───────────────▼───────────────┐
│             스택               │
│  - 지역 변수 슬롯              │
│    ex) Status s = Status.READY │
│       └── 스택에 "힙 객체 참조" 저장 │
└───────────────────────────────┘
  1. 리플렉션 차단: 보통 싱글턴 패턴 클래스는 리플렉션으로 생성자를 열어두면 두 번째 인스턴스가 생길 수 있으나 enum은 예외
    • Constuctor.newInstance() 호출 시 IllegalArgumentException 발생
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
  1. 직렬화 보장
  • 일반 싱글턴 클래스는 readResolve()를 구현하지 않으면 역직렬화 시 새 인스턴스가 생길 수 있으나 enum은 언어 차원에서 보장
  • 역직렬화 시 항상 기존 상수 인스턴스를 반환
  • 즉, GC나 직렬화/역직렬화 과정을 거쳐도 새로운 객체는 생기지 않는다.

열거형의 조상: 추상 클래스 java.lang.Enum

public abstract class Enum<E extends Enum<E>>  
        implements Constable, Comparable<E>, Serializable {

    private final String name;

    public final String name() {  
        return name;  
    }

    private final int ordinal;

    public final int ordinal() {  
        return ordinal;  
    }

    protected Enum(String name, int ordinal) {  
        this.name = name;  
        this.ordinal = ordinal;  
    }    

    public String toString() {  
        return name;  
    }    

    public final boolean equals(Object other) {  
        return this==other;  
    }    

    @Stable  
    private int hash;

    public final int hashCode() {
        int hc = hash;  
        if (hc == 0) {  
            hc = hash = System.identityHashCode(this);  
        }  
        return hc;        
    }

    protected final Object clone() throws CloneNotSupportedException {  
        throw new CloneNotSupportedException();  
    }    

    public final int compareTo(E o) {  
        Enum<?> other = o;  
        Enum<E> self = this;  
        if (self.getClass() != other.getClass() && // optimization  
            self.getDeclaringClass() != other.getDeclaringClass())  
            throw new ClassCastException();  
        return self.ordinal - other.ordinal;  
    }    

    public final Class<E> getDeclaringClass() {  
        Class<?> clazz = getClass();  
        Class<?> zuper = clazz.getSuperclass();  
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;  
    }

    public static <T extends Enum<T>> T valueOf(Class<T> enumClass,  
                                                    String name) {  
        T result = enumClass.enumConstantDirectory().get(name);  
        if (result != null)  
            return result;  
        if (name == null)  
            throw new NullPointerException("Name is null");  
        throw new IllegalArgumentException(  
            "No enum constant " + enumClass.getCanonicalName() + "." + name);  
    }
}
  • protected Enum(String name, int ordinal) 생성자 => Enumabstract 클래스이기 때문에 직접적으로 생성자를 호출하지 못하므로, enum의 생성자에서 super를 통해 호출할 수 있도록 하기 위해 protected 선언

  • private final String nameprivate final int ordinal => 불변성을 갖도록 하기 위해 private final 선언

  • ordinalenum 객체가 갖는 순서값이다.

  • enum 타입에 있는 values()valueOf(String) 정적 메서드는 컴파일러가 해당 타입에 자동 생성하는 유틸이며, Enum 클래스의 인스턴스 메서드가 아니다.

  • equals 메서드는 final로 선언되어 오버라이드 불가하고 return this==other;로 참조 동일성을 판단하도록 구현돼 있으므로 enum 객체 간의 equals 메서드를 통한 비교도 참조 동일성을 판단한다.

  • hashCode 메서드는 final로 선언되어 오버라이드 불가하고 아이덴티티 기반의 hash 값을 반환하며 enum 객체는 싱글턴을 보장하므로 enum 객체의 참조값을 HashMap, HashSet의 키로 사용이 가능하다.

  • comparteTo 메서드 또한 final로 선언되어 오버라이드 불가하다. 두 enum 객체가 서로 같은 enum 타입인지 검사하고 같지 않으면 ClassCastException을 발생시키기 때문에 같은 타입의 enum 객체에 대해서만 compartTo 메서드를 사용 가능하다.

    열거형 객체 비교

  • 열거형 상수, 즉 열거형 객체 간의 == 연산은 열거형 상수(열거형 객체)의 참조값끼리 비교하므로 열거형 상수 간의 참조 동일성을 판단한다.

  • equals 메서드도 == 연산과 완전히 동일하게 열거형 상수 간의 참조 동일성을 판단한다.

  • 열거형 상수, 즉 열거형 객체 간의 >, >=, <, <= 연산은 불가하다.

  • >, >=, <, <= 연산 대신 compareTo 메서드를 사용해야 하며 두 열거형 상수의 타입이 같은 타입이 아니면 ClassCastException을 발생시키므로 같은 타입의 열거형 상수에 대해서만 사용 가능하다.

  • compareTo 메서드는 enum 객체가 갖는 순서값을 이용하는 ordinal() 기반의 비교를 제공한다.

  • equals 메서드를 enum 객체 간의 내용 동등성이 아닌 참조 동일성을 판단하는 것으로 고정(final 선언)한 이유

    • enum 객체는 애초에 싱글턴 객체이므로 같은 타입의 두 enum 객체 간의 참조값 비교는 내용 동등성을 판단하는 것과 같다.
    • 그리고 서로 다른 타입의 두 enum 객체의 내용 동등성을 판단하는 것은 애초에 의미가 없으므로 equals 메서드와 == 연산 모두 참조 동일성을 판단하는 것으로 충분하다.
    • 결국엔 enum 객체가 싱글톤 객체임을 보장하기 위함에서 기인한 결과이다.
enum Status { READY, RUNNING }

Status a = Status.READY;
Status b = Status.READY;
Status c = Status.RUNNING;

assert a == b;              // true
assert a.equals(b);         // true (== 로 비교)
assert !a.equals(c);        // true
assert !Objects.equals(a, null); // true (null 안전)

열거형 멤버 추가하기

  • Enum 클래스로부터 상속받은 멤버들 외에 멤버를 더 추가할 시 주의사항
      1. 열거형의 불변성과 스레드 안정성을 보장하도록 멤버 변수 private final 선언: 불변 설계
      1. 추가된 인스턴스 변수 초기화를 위한 생성자 추가(private 선언이 생략된 생성자): 열거형의 생성자는 항상 private => 외부에서 해당 열거형 객체 생성 불가
      1. 열거형 상수 생성에 인자 전달
enum Direction {
    EAST(1, ">"), SOUTH(2, "V"), WEST(3, "<"), EAST(4, "^");

    private static final Direction[] DIR_ARR = Direction.values();
    private final int value;
    private final String symbol;

    Direction(int value, String symbol) {
        this.value = value;
        this.symbol = symbol;
    }

    public int getValue() {
        return value;
    }

    public String getSymbol() {
        return symbol;
    }

    public static Direction of(int direction) {
        if(direction < 1 || direction > 4) {
            throw IllegalArgumentException("Invalid value :" + direction);
        }

        return DIR_ARR[direction - 1];
    } 
}

열거형 사용 시 장점

  1. 관련 상수들의 유지와 관리가 용이하다.
  2. 심볼릭 상수로 가독성이 좋은 코드를 작성하게 해준다: 기존엔 1, boolean, a와 같은 상수 리터럴에는 식별자를 지정해줄 수 없었지만 열거형 상수가 심볼릭 상수로써, 식별자와 같은 역할을 해준다.
  3. 상수를 마치 변수처럼 다룰 수 있게 된다. => 열거형 객체의 참조값 저장, 메서드 호출
  4. 타입 안정성 보장: final로 선언되며 상속도 제한되기 때문에 대입 연산 시 정확한 타입의 값을 할당할 수 있다.
Week w1 = 1; // 타입 에러
Week w2 = Week.SUNDAY; // 정확한 타입만 대입 가능