Java Enum 완전 정리
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()는 예외 가능 → 안전 파싱 래퍼 사용 │
└─────────────────────────────────────────────────────────┘