Java의 enum은 단순히 상수 집합을 표현하는 것을 넘어, 필드·메서드·추상 메서드를 가질 수 있는 완전한 클래스입니다. 상수 대신 Enum을 사용해야 하는 이유부터 EnumSet, EnumMap, 싱글톤 패턴까지 완전히 정리합니다.


1. Enum이란? 왜 상수 대신 Enum을 쓰는가

정수 상수 패턴의 문제점

// 안티패턴: 정수 상수
public class Season {
    public static final int SPRING = 0;
    public static final int SUMMER = 1;
    public static final int FALL   = 2;
    public static final int WINTER = 3;
}

// 문제점
void doSomething(int season) { ... }
doSomething(Season.SPRING);  // OK
doSomething(999);            // 컴파일 에러 없음! 런타임 버그
doSomething(0);              // 어떤 계절인지 의미 불명확

Enum으로 해결

// Enum: 타입 안전 상수
public enum Season {
    SPRING, SUMMER, FALL, WINTER
}

void doSomething(Season season) { ... }
doSomething(Season.SPRING);  // OK
doSomething(999);            // 컴파일 에러! — 타입 안전

상수 패턴 vs Enum 비교

항목 정수 상수 Enum
타입 안전 X O
의미 있는 이름 X (숫자) O (SPRING)
switch 사용 O O
메서드 추가 X O
네임스페이스 X (충돌 위험) O (Season.SPRING)
디버깅 숫자만 표시 이름 표시
확장성 어려움 쉬움

2. Enum 내부 구현 (컴파일 시 클래스 변환)

컴파일러가 생성하는 코드

// 작성한 코드
public enum Season {
    SPRING, SUMMER, FALL, WINTER
}

// 컴파일러가 생성하는 코드 (대략적)
public final class Season extends Enum<Season> {
    public static final Season SPRING = new Season("SPRING", 0);
    public static final Season SUMMER = new Season("SUMMER", 1);
    public static final Season FALL   = new Season("FALL",   2);
    public static final Season WINTER = new Season("WINTER", 3);

    private static final Season[] $VALUES = { SPRING, SUMMER, FALL, WINTER };

    private Season(String name, int ordinal) {
        super(name, ordinal);
    }

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

    public static Season valueOf(String name) {
        return Enum.valueOf(Season.class, name);
    }
}

Enum의 특성

// 1. final 클래스 — 상속 불가
// class MySeason extends Season { }  // 컴파일 에러!

// 2. Enum끼리 상속 불가 (java.lang.Enum만 상속)
// enum Child extends Season { }  // 문법 자체 없음

// 3. 인터페이스 구현 가능
enum Season implements Printable {
    SPRING, SUMMER, FALL, WINTER;

    @Override
    public void print() {
        System.out.println(name());
    }
}

// 4. 인스턴스는 JVM 전역에서 단 하나
Season s1 = Season.SPRING;
Season s2 = Season.SPRING;
System.out.println(s1 == s2);  // true (항상)

3. Enum 기본 메서드

java.lang.Enum이 제공하는 메서드

Season s = Season.SUMMER;

// name() — 선언된 이름 반환
s.name()         // "SUMMER"

// ordinal() — 선언 순서 (0부터)
s.ordinal()      // 1

// toString() — 기본적으로 name()과 동일, 오버라이딩 가능
s.toString()     // "SUMMER"

// compareTo() — ordinal 기준 비교
Season.SPRING.compareTo(Season.WINTER)  // 음수 (SPRING < WINTER)

// values() — 모든 상수 배열 반환
Season[] seasons = Season.values();

// valueOf() — 이름으로 상수 반환
Season spring = Season.valueOf("SPRING");
// 없는 이름이면 IllegalArgumentException

실용적인 활용

// 모든 상수 순회
for (Season s : Season.values()) {
    System.out.println(s.ordinal() + ": " + s.name());
}
// 0: SPRING
// 1: SUMMER
// 2: FALL
// 3: WINTER

// switch 문
Season season = Season.SUMMER;
switch (season) {
    case SPRING -> System.out.println("봄");
    case SUMMER -> System.out.println("여름");
    case FALL   -> System.out.println("가을");
    case WINTER -> System.out.println("겨울");
}

// Java 14+ switch 표현식
String korean = switch (season) {
    case SPRING -> "봄";
    case SUMMER -> "여름";
    case FALL   -> "가을";
    case WINTER -> "겨울";
};

ordinal() 사용 주의

// ordinal()에 의존하는 코드는 위험
// 상수 순서가 바뀌면 전체 로직 붕괴
int idx = season.ordinal();  // 피하는 것이 좋음

// 대신 명시적 필드 사용
enum Season {
    SPRING(1), SUMMER(2), FALL(3), WINTER(4);
    private final int number;
    Season(int number) { this.number = number; }
    public int getNumber() { return number; }
}

4. Enum에 필드 / 메서드 / 생성자 추가

필드와 생성자

public enum Planet {
    MERCURY(3.303e+23, 2.4397e6),
    VENUS  (4.869e+24, 6.0518e6),
    EARTH  (5.976e+24, 6.37814e6),
    MARS   (6.421e+23, 3.3972e6);

    private final double mass;    // kg
    private final double radius;  // m

    // Enum 생성자는 항상 private (외부 생성 불가)
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
    }

    static final double G = 6.67300E-11;

    // 메서드 추가 가능
    double surfaceGravity() {
        return G * mass / (radius * radius);
    }

    double surfaceWeight(double otherMass) {
        return otherMass * surfaceGravity();
    }
}

// 사용
double earthWeight = 75.0;
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for (Planet p : Planet.values()) {
    System.out.printf("체중 on %s: %6.2f%n", p, p.surfaceWeight(mass));
}

인터페이스 구현

interface Discountable {
    double discount();
}

public enum MemberGrade implements Discountable {
    BRONZE {
        @Override
        public double discount() { return 0.05; }
    },
    SILVER {
        @Override
        public double discount() { return 0.10; }
    },
    GOLD {
        @Override
        public double discount() { return 0.20; }
    };
}

5. Enum + 추상 메서드 (전략 패턴)

각 상수마다 다른 동작 구현

public enum Operation {
    PLUS("+") {
        @Override
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        @Override
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        @Override
        public double apply(double x, double y) { return x * y; }
    },
    DIVIDE("/") {
        @Override
        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);

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

// 사용
double x = 10, y = 3;
for (Operation op : Operation.values()) {
    System.out.printf("%.1f %s %.1f = %.1f%n", x, op, y, op.apply(x, y));
}
// 10.0 + 3.0 = 13.0
// 10.0 - 3.0 = 7.0
// 10.0 * 3.0 = 30.0
// 10.0 / 3.0 = 3.3

람다를 필드로 사용하는 패턴 (Java 8+)

import java.util.function.DoubleBinaryOperator;

public enum Operation {
    PLUS  ("+", (x, y) -> x + y),
    MINUS ("-", (x, y) -> x - y),
    TIMES ("*", (x, y) -> x * y),
    DIVIDE("/", (x, y) -> x / y);

    private final String symbol;
    private final DoubleBinaryOperator op;

    Operation(String symbol, DoubleBinaryOperator op) {
        this.symbol = symbol;
        this.op = op;
    }

    public double apply(double x, double y) {
        return op.applyAsDouble(x, y);
    }

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

전략 Enum 패턴

// 요일별 급여 계산
public enum PayrollDay {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
    SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);

    private final PayType payType;

    PayrollDay() { this.payType = PayType.WEEKDAY; }
    PayrollDay(PayType payType) { this.payType = payType; }

    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }

    // 전략 열거 타입
    enum PayType {
        WEEKDAY {
            @Override
            int overtimePay(int mins, int payRate) {
                return mins <= MINS_PER_SHIFT ? 0 : (mins - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            @Override
            int overtimePay(int mins, int payRate) {
                return mins * 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);
        }
    }
}

6. EnumSet, EnumMap 활용

EnumSet

import java.util.EnumSet;

// 비트 벡터 기반 — 극도로 빠른 집합 연산
EnumSet<Season> springAndSummer = EnumSet.of(Season.SPRING, Season.SUMMER);
EnumSet<Season> all = EnumSet.allOf(Season.class);
EnumSet<Season> none = EnumSet.noneOf(Season.class);
EnumSet<Season> complement = EnumSet.complementOf(springAndSummer);

// 집합 연산
springAndSummer.contains(Season.SPRING);  // true
springAndSummer.add(Season.FALL);
springAndSummer.remove(Season.SPRING);
EnumSet 내부 구조 (4개 이하: RegularEnumSet):
SPRING=0, SUMMER=1, FALL=2, WINTER=3

비트 표현:
SPRING | SUMMER = 0001 | 0010 = 0011 (long 비트 필드)
contains(FALL)  = 0011 & 0100 = 0000 → false
// 권한(Permission) 표현 — 실무 활용
public enum Permission {
    READ, WRITE, EXECUTE, DELETE
}

// 사용자 권한 설정
EnumSet<Permission> adminPermissions = EnumSet.allOf(Permission.class);
EnumSet<Permission> readOnly = EnumSet.of(Permission.READ);
EnumSet<Permission> userPermissions = EnumSet.of(Permission.READ, Permission.WRITE);

// 권한 체크
if (userPermissions.contains(Permission.WRITE)) {
    // 쓰기 허용
}

EnumMap

import java.util.EnumMap;

// 배열 기반 — 일반 HashMap보다 빠름
EnumMap<Season, String> activities = new EnumMap<>(Season.class);
activities.put(Season.SPRING, "꽃구경");
activities.put(Season.SUMMER, "수영");
activities.put(Season.FALL, "단풍놀이");
activities.put(Season.WINTER, "스키");

System.out.println(activities.get(Season.SUMMER));  // 수영

// 순서 보장 (선언 순서)
activities.forEach((season, activity) ->
    System.out.println(season + ": " + activity));
// SPRING: 꽃구경
// SUMMER: 수영
// FALL: 단풍놀이
// WINTER: 스키
EnumMap 내부 구조:
ordinal을 인덱스로 사용하는 배열

[0] → "꽃구경"  (SPRING.ordinal() = 0)
[1] → "수영"    (SUMMER.ordinal() = 1)
[2] → "단풍놀이"(FALL.ordinal() = 2)
[3] → "스키"    (WINTER.ordinal() = 3)

HashMap의 해싱 비용 없음 → O(1) 배열 접근

EnumSet / EnumMap vs 일반 컬렉션

// 일반 HashSet/HashMap 사용 금지 (Enum일 때)
Set<Season> set = new HashSet<>();        // 느림
Map<Season, String> map = new HashMap<>(); // 느림

// EnumSet / EnumMap 사용 (성능 + 명확성)
Set<Season> set = EnumSet.noneOf(Season.class);  // 빠름
Map<Season, String> map = new EnumMap<>(Season.class); // 빠름

7. Enum 싱글톤 패턴

Enum 싱글톤이란?

Joshua Bloch(Effective Java)이 권장하는 싱글톤 구현 방식입니다.

public enum DatabaseConnection {
    INSTANCE;

    private final Connection connection;

    DatabaseConnection() {
        // 싱글톤 초기화 — JVM이 한 번만 실행 보장
        try {
            this.connection = DriverManager.getConnection(
                "jdbc:mysql://localhost:3306/mydb", "user", "password"
            );
        } catch (SQLException e) {
            throw new RuntimeException("DB 연결 실패", e);
        }
    }

    public Connection getConnection() {
        return connection;
    }

    public void executeQuery(String sql) {
        // ...
    }
}

// 사용
Connection conn = DatabaseConnection.INSTANCE.getConnection();

Enum 싱글톤의 장점

일반 싱글톤의 문제점:
1. 리플렉션 공격 취약
   Constructor c = Singleton.class.getDeclaredConstructor();
   c.setAccessible(true);
   Singleton s2 = c.newInstance();  // 두 번째 인스턴스 생성!

2. 역직렬화 시 새 인스턴스 생성
   readResolve() 메서드 추가 필요

Enum 싱글톤:
1. 리플렉션으로 생성 불가
   → JVM이 Enum 생성자 리플렉션 차단
2. 직렬화/역직렬화 안전
   → JVM이 직접 처리, 항상 같은 인스턴스
3. 스레드 안전
   → 클래스 로딩 시 한 번만 초기화
// 리플렉션 공격 불가
try {
    Constructor<DatabaseConnection> c =
        DatabaseConnection.class.getDeclaredConstructor(String.class, int.class);
    c.setAccessible(true);
    c.newInstance("INSTANCE", 0);
} catch (Exception e) {
    // java.lang.IllegalArgumentException: Cannot reflectively create enum objects
}

실무에서의 Enum 싱글톤

// 설정 관리
public enum AppConfig {
    INSTANCE;

    private final Properties props = new Properties();

    AppConfig() {
        try (InputStream is = getClass().getResourceAsStream("/app.properties")) {
            props.load(is);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public String get(String key) {
        return props.getProperty(key);
    }

    public String get(String key, String defaultValue) {
        return props.getProperty(key, defaultValue);
    }
}

// 사용
String host = AppConfig.INSTANCE.get("db.host", "localhost");

8. 실무 활용 패턴

상태 머신 (State Machine)

public enum OrderStatus {
    PENDING {
        @Override
        public OrderStatus next() { return CONFIRMED; }
        @Override
        public boolean canCancel() { return true; }
    },
    CONFIRMED {
        @Override
        public OrderStatus next() { return SHIPPED; }
        @Override
        public boolean canCancel() { return true; }
    },
    SHIPPED {
        @Override
        public OrderStatus next() { return DELIVERED; }
        @Override
        public boolean canCancel() { return false; }
    },
    DELIVERED {
        @Override
        public OrderStatus next() { throw new IllegalStateException("최종 상태"); }
        @Override
        public boolean canCancel() { return false; }
    };

    public abstract OrderStatus next();
    public abstract boolean canCancel();
}

// 사용
OrderStatus status = OrderStatus.PENDING;
if (status.canCancel()) {
    System.out.println("취소 가능");
}
status = status.next();  // CONFIRMED

valueOf 안전 파싱

// valueOf는 없는 이름이면 예외
Season s = Season.valueOf("INVALID");  // IllegalArgumentException!

// 안전한 파싱 유틸
public static <T extends Enum<T>> Optional<T> safeValueOf(Class<T> enumClass, String name) {
    try {
        return Optional.of(Enum.valueOf(enumClass, name));
    } catch (IllegalArgumentException e) {
        return Optional.empty();
    }
}

// 또는 Map으로 구현
public enum Season {
    SPRING, SUMMER, FALL, WINTER;

    private static final Map<String, Season> BY_NAME = Arrays.stream(values())
        .collect(Collectors.toMap(Season::name, s -> s));

    public static Optional<Season> fromName(String name) {
        return Optional.ofNullable(BY_NAME.get(name));
    }
}

9. 전체 요약

Enum 핵심 정리:
┌─────────────────────────────────────────────────────────┐
│  기본                                                   │
│  - 타입 안전한 상수 집합                                │
│  - 컴파일러가 final class로 변환                        │
│  - 인스턴스는 전역에서 단 하나 (== 비교 가능)           │
│                                                         │
│  확장                                                   │
│  - 필드/메서드/생성자 추가 가능                         │
│  - 추상 메서드로 전략 패턴 구현                         │
│  - 인터페이스 구현 가능                                 │
│                                                         │
│  컬렉션                                                 │
│  - EnumSet: 비트 벡터 기반, Set<Enum>보다 빠름          │
│  - EnumMap: 배열 기반, Map<Enum,V>보다 빠름             │
│                                                         │
│  싱글톤                                                 │
│  - 리플렉션 / 직렬화 공격에 안전한 유일한 방법          │
│                                                         │
│  주의                                                   │
│  - ordinal()에 의존하는 코드 금지                       │
│  - valueOf()는 예외 가능 → 안전 파싱 래퍼 사용          │
└─────────────────────────────────────────────────────────┘

카테고리:

업데이트: