← all posts
DEV 2026.05.02 · 14 min read Intermediate

Java Enum은 단순한 상수 묶음이 아니다

타입 안전성의 출발점부터 전략 패턴·상태 머신·싱글톤까지, Enum이 하나의 설계 언어로 기능하는 방식을 추적한다.


Java Enum을 처음 배울 때 대부분은 “타입 안전한 상수”라는 설명에서 멈춘다. 틀린 말은 아니지만, 그 설명만으로는 Enum이 왜 인터페이스를 구현하고, 추상 메서드를 선언하고, 싱글톤 패턴의 가장 안전한 구현체가 되는지 설명하지 못한다. Enum의 진짜 힘은 어디서 오는가?

타입 안전성: 컴파일러를 경비원으로

Enum 이전에는 public static final int MONDAY = 1 같은 정수 상수를 썼다. 이 방식의 핵심 문제는 컴파일러가 processDay(999)를 막아주지 못한다는 것이다. 유효하지 않은 값이 런타임까지 살아남고, 버그는 테스트나 프로덕션에서야 발견된다. 게다가 MONDAY + TUESDAY처럼 의미 없는 산술 연산도 컴파일러는 조용히 허용한다.

Enum은 이 문제를 타입 시스템으로 해결한다.

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

static void processDay(Day day) {
    // 검증 불필요 — 항상 유효한 값
    System.out.println("요일: " + day);
}

// processDay(999);                      // 컴파일 에러
// processDay(Day.MONDAY + Day.TUESDAY); // 컴파일 에러

컴파일러가 경비원이 된다. 유효하지 않은 값은 코드에 진입조차 못한다. 이것이 첫 번째 설계 원칙이다 — 잘못된 사용을 런타임이 아닌 컴파일 타임에 차단한다. 정수 상수와 달리 Enum 상수는 네임스페이스도 제공한다. Day.MONDAYMonth.MONDAY는 같은 이름을 가져도 전혀 다른 타입이다.

상수에 동작을 붙이기

Enum이 단순 상수와 근본적으로 다른 지점은 필드와 메서드를 가질 수 있다는 것이다. Planet Enum이 질량과 반지름을 필드로 가지고 표면 중력을 계산하는 메서드를 제공하듯, 각 상수는 데이터와 행동을 함께 캡슐화한다. 관련 로직이 Enum 바깥으로 흘러나가지 않는다.

더 나아가 Constant Specific Method 패턴은 각 상수가 추상 메서드를 개별적으로 구현하게 한다.

enum Operation {
    PLUS("+") {
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        public double apply(double x, double y) { return x - y; }
    },
    DIVIDE("/") {
        public double apply(double x, double y) {
            if (y == 0) throw new ArithmeticException("Division by zero");
            return x / y;
        }
    };

    private final String symbol;

    Operation(String symbol) { this.symbol = symbol; }

    public String getSymbol() { return symbol; }

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

switch 문이 사라진다. 새 연산을 추가할 때 기존 코드를 건드릴 필요가 없다. 각 상수가 자신의 로직을 책임진다. 누군가 새 상수를 추가하면서 추상 메서드 구현을 빠트리면 컴파일 에러가 발생한다 — 실수를 구조적으로 막는다.

switch 대신 추상 메서드

switch 문은 새 상수를 추가할 때 실수로 case를 빠트려도 컴파일러가 잡아주지 못한다. default가 있으면 더욱 조용히 넘어간다. 추상 메서드는 모든 상수가 반드시 구현을 제공하도록 강제한다.

인터페이스 구현과 전략 패턴

Enum은 인터페이스를 구현할 수 있다. 이 능력이 Enum을 진짜 설계 도구로 만든다. 전략 패턴을 구현할 때 보통은 인터페이스 하나와 구현 클래스 여러 개가 필요하다. Enum은 이 구조를 하나의 파일 안에 응집시킨다.

interface DiscountStrategy {
    double applyDiscount(double price);
    String getDescription();
}

enum CustomerType implements DiscountStrategy {
    REGULAR("일반 고객", 0.0) {
        public double applyDiscount(double price) { return price; }
    },
    VIP("VIP", 0.20) {
        public double applyDiscount(double price) {
            double discounted = price * (1 - discountRate);
            if (price >= 100_000) discounted *= 0.95; // 추가 할인
            return discounted;
        }
    },
    PREMIUM("프리미엄", 0.30) {
        public double applyDiscount(double price) {
            return Math.max(0, price * (1 - discountRate) - 5_000);
        }
    };

    private final String displayName;
    protected final double discountRate;

    CustomerType(String displayName, double discountRate) {
        this.displayName = displayName;
        this.discountRate = discountRate;
    }

    @Override
    public String getDescription() {
        return displayName + " (" + (int)(discountRate * 100) + "% 할인)";
    }
}

CustomerTypeDiscountStrategy 타입으로 다룰 수 있다. 의존성 주입, 다형성, 전략 교체가 전부 가능하다. 별도의 클래스 계층 없이 Enum 파일 하나에서. 결제 수단, 압축 알고리즘, 필터 체인 — 전략이 유한하고 열거 가능한 경우라면 Enum 전략 패턴이 강력한 선택지가 된다.

상태 머신과 싱글톤

두 가지 고급 활용이 있다.

상태 머신: 각 상수가 next()confirm() 같은 전환 메서드를 구현하면, Enum 자체가 상태 전이 규칙을 인코딩한다. OrderState.PENDING에서 cancel()을 호출하면 CANCELLED로 전환되고, DELIVERED에서 ship()을 호출하면 IllegalStateException이 던져진다. 허용되지 않는 전환은 런타임 예외로 막힌다. 상태 전이 로직이 Enum 바깥의 if-else 체인으로 흩어지지 않는다.

싱글톤: Enum 싱글톤은 세 가지 문제를 자동으로 해결한다.

enum DatabaseConnection {
    INSTANCE;

    private boolean connected = false;

    public void connect() {
        if (!connected) {
            System.out.println("DB 연결 중...");
            connected = true;
        }
    }

    public void executeQuery(String query) {
        if (connected) System.out.println("쿼리: " + query);
        else throw new IllegalStateException("연결되지 않음");
    }
}

스레드 안전성은 JVM이 보장한다. 역직렬화 시 새 인스턴스 생성을 막는다. Reflection으로 새 인스턴스를 만들 수 없다. 일반 싱글톤 클래스가 synchronized, volatile, readResolve 등으로 복잡하게 막아야 하는 것들을, Enum은 구조적으로 불가능하게 만든다.

트레이드오프

Enum 싱글톤이 항상 최선은 아니다. Enum 상수는 클래스 로딩 시점에 모두 초기화된다 — 지연 초기화(lazy initialization)가 필요한 경우에는 적합하지 않다. 상속이 필요하거나 DI 프레임워크가 생명주기를 직접 관리해야 하는 경우에도 일반 클래스가 낫다. Enum 싱글톤은 “JVM이 관리하는 전역 상태”가 필요할 때 가장 빛난다.

EnumSet과 EnumMap, 그리고 피해야 할 패턴

Enum 전용 컬렉션은 일반 컬렉션보다 빠르다. EnumSet은 비트 벡터로 구현되어 집합 연산이 단일 비트 연산으로 처리된다. EnumMap은 배열 기반으로, HashMap의 해시 계산 오버헤드 없이 O(1) 접근을 제공하고 선언 순서를 보장한다. Enum 키를 쓰는 Map이나 Set이라면 항상 EnumMap/EnumSet을 선택한다.

// 역할별 권한 — EnumSet 활용
enum Permission { READ, WRITE, EXECUTE, DELETE, ADMIN }

enum Role {
    GUEST(EnumSet.of(Permission.READ)),
    USER(EnumSet.of(Permission.READ, Permission.WRITE)),
    ADMIN(EnumSet.allOf(Permission.class));

    private final EnumSet<Permission> permissions;

    Role(EnumSet<Permission> permissions) { this.permissions = permissions; }

    public boolean hasPermission(Permission p) { return permissions.contains(p); }
}

반대로, 피해야 할 패턴이 하나 있다. ordinal()을 배열 인덱스나 데이터베이스 저장 값으로 쓰는 것이다. 상수 순서가 바뀌거나 중간에 새 상수가 추가되는 순간 조용히 깨진다. 항상 명시적 필드를 사용한다.

정리

  • Enum은 컴파일 타임에 유효하지 않은 값을 차단하는 타입 안전 상수다. 정수 상수의 네임스페이스 오염과 의미 없는 연산 문제를 함께 해결한다.
  • Constant Specific Method와 인터페이스 구현을 결합하면, 전략 패턴을 switch 문 없이 Enum 하나로 표현할 수 있다.
  • 상태 머신에서 Enum은 허용된 전이만 메서드로 노출하고 나머지를 예외로 막아, 상태 로직을 한곳에 응집시킨다.
  • Enum 싱글톤은 스레드 안전성·직렬화 안전성·Reflection 방어를 구조적으로 보장한다. 단, 지연 초기화가 필요하면 적합하지 않다.
  • EnumSet/EnumMap은 Enum 전용 고성능 컬렉션이다. ordinal() 직접 의존은 금지다.

Enum은 “상수를 묶는 도구”가 아니라 타입 안전성, 행동 캡슐화, 상태 관리를 하나의 구조 안에서 표현하는 설계 언어다.