1. 소개
Java에는 열거형(enum)을 다루기 위한 전용 컬렉션인 EnumSet과 EnumMap이 있습니다. 이들은 표준 라이브러리(java.util)에 포함되어 있으며 enum 타입을 최대한 효율적으로 처리하도록 설계되었습니다.
EnumSet과 EnumMap의 내부 동작 방식은?
EnumSet은 비트 마스크처럼 동작합니다. 각 열거형 요소가 정확히 한 비트를 차지하는 플래그 모음을 떠올리면 됩니다. 비트가 켜져 있으면 요소가 집합에 포함되고, 꺼져 있으면 포함되지 않습니다. 모든 것은 숫자 배열(long[])에 저장되며, enum의 값이 64 미만이라면 전체 집합이 단 하나의 숫자에 들어갑니다!
EnumMap은 더 단순합니다. 값의 배열이며, 인덱스로 enum 요소의 순서 번호(ordinal)를 사용합니다. 익숙한 HashMap<Enum, V> 대신 매우 컴팩트하고 빠른 구조를 얻습니다.
무엇이 좋아질까요? 추가, 삭제, 포함 여부 확인이 모두 O(1)에 수행됩니다. 메모리 사용도 최소입니다(특히 HashSet 및 HashMap과 비교했을 때). 요소 반복 순서는 항상 enum에 선언된 순서를 따르므로 결과가 예측 가능합니다.
예시: 동작 모습
enum Day { MON, TUE, WED, THU, FRI, SAT, SUN }
EnumSet<Day> weekend = EnumSet.of(Day.SAT, Day.SUN);
System.out.println(weekend); // [SAT, SUN]
겉으로는 일반적인 집합처럼 보입니다. 하지만 내부적으로는 두 개의 비트가 켜진 숫자입니다. 하나는 SAT, 다른 하나는 SUN을 나타냅니다. FRI를 추가하면 비트가 하나 더 켜집니다. 해시 테이블도 없고, 불필요한 객체도 없습니다.
EnumMap<Day, String> schedule = new EnumMap<>(Day.class);
schedule.put(Day.MON, "Gym");
schedule.put(Day.FRI, "Party");
System.out.println(schedule); // {MON=Gym, FRI=Party}
여기서 키(Day)는 내부적으로 배열 인덱스로 변환되므로, 접근 속도가 배열 요소에 접근하는 것만큼 빠릅니다.
2. 사용 사례: 플래그, 테이블, 유한 상태 기계
EnumSet: 플래그와 상태 집합에 최적
- 플래그: 제한된 개수의 옵션에 대해 on/off 상태를 저장합니다.
- enum 값의 집합: 요일, 사용자 권한, 작업 상태 등.
- 유한 상태 기계(FSM): 허용되는 전이를 EnumSet에 보관하기 편리합니다.
예시: 접근 권한 플래그
enum Permission { READ, WRITE, EXECUTE }
EnumSet<Permission> perms = EnumSet.of(Permission.READ, Permission.WRITE);
if (perms.contains(Permission.WRITE)) {
// 쓰기 허용됨
}
예시: 일부를 제외한 모든 값
EnumSet<Day> workdays = EnumSet.complementOf(EnumSet.of(Day.SAT, Day.SUN));
System.out.println(workdays); // [MON, TUE, WED, THU, FRI]
EnumMap: enum 키 기반 테이블에 최적
- 매핑 테이블: 키는 enum 값, 값은 임의의 객체입니다.
- 빠른 접근: HashMap<Enum, V>보다 더 빠르고 더 컴팩트합니다.
예시: 요일별 가격
EnumMap<Day, Integer> prices = new EnumMap<>(Day.class);
prices.put(Day.MON, 100);
prices.put(Day.SAT, 200);
System.out.println(prices.get(Day.SAT)); // 200
예시: 유한 상태 기계
enum State { START, RUNNING, STOPPED }
EnumMap<State, EnumSet<State>> transitions = new EnumMap<>(State.class);
transitions.put(State.START, EnumSet.of(State.RUNNING));
transitions.put(State.RUNNING, EnumSet.of(State.STOPPED));
transitions.put(State.STOPPED, EnumSet.noneOf(State.class));
3. 함정: enum 변경과 직렬화
EnumSet과 EnumMap은 사용 중인 enum의 구성과 순서에 의존합니다. 새 요소를 추가하거나 기존 요소를 삭제하거나 위치를 바꾸면, 저장되었거나 직렬화된 컬렉션이 올바르게 동작하지 않을 수 있습니다.
직렬화
- EnumSet과 EnumMap은 직렬화가 가능하지만, 직렬화와 역직렬화 사이에 enum이 변경되면 오류나 데이터 손실이 발생할 수 있습니다.
- 직렬화 이후 enum 값이 삭제되면, 읽는 과정에서 예외가 발생할 가능성이 매우 높습니다.
모범 사례:
- EnumSet/EnumMap을 직렬화해야 한다면, 해당 enum이 안정적임을 확신할 때에만 하세요.
- 장기 보관 용도라면 값의 문자열 표현 목록 등으로 저장하는 방법을 사용하세요.
4. 유용한 팁
EnumSet/EnumMap과 일반 컬렉션 비교
| 컬렉션 | 키/원소 | 내부 구조 | 성능 | 메모리 | Null |
|---|---|---|---|---|---|
|
|
비트 마스크 | O(1) | 매우 적음 | 불가 |
|
|
해시 테이블 | O(1) | 더 많음 | 가능 |
|
|
ordinal 기반 배열 | O(1) | 매우 적음 | 불가 |
|
|
해시 테이블 | O(1) | 더 많음 | 가능 |
모범 사례
- EnumSet을 값의 집합에 사용하세요(of, noneOf, allOf, complementOf).
- 키가 enum인 연관 테이블에는 EnumMap을 사용하세요.
- ‘거대한’ 열거형은 피하세요 — 수백 개의 값은 컴팩트함을 떨어뜨립니다.
- enum이 변할 수 있다면 직렬화하지 마세요.
- null을 키나 값으로 사용하지 마세요.
5. 실전: 애플리케이션에서 EnumSet과 EnumMap 사용법
예시: 사용자 역할 저장
enum Role { USER, ADMIN, MODERATOR }
class User {
private EnumSet<Role> roles = EnumSet.noneOf(Role.class);
public void addRole(Role role) {
roles.add(role);
}
public boolean isAdmin() {
return roles.contains(Role.ADMIN);
}
}
예시: 상태 전이 테이블
enum State { NEW, IN_PROGRESS, DONE }
EnumMap<State, EnumSet<State>> transitions = new EnumMap<>(State.class);
transitions.put(State.NEW, EnumSet.of(State.IN_PROGRESS));
transitions.put(State.IN_PROGRESS, EnumSet.of(State.DONE));
transitions.put(State.DONE, EnumSet.noneOf(State.class));
6. EnumSet/EnumMap 사용 시 흔한 실수
오류 1: enum이 아닌 타입과 함께 EnumSet/EnumMap을 사용.
이 컬렉션은 enum 타입에만 동작합니다.
// EnumSet<String> set = EnumSet.of("A", "B"); // 컴파일 오류!
오류 2: 매우 큰 enum에 EnumSet/EnumMap 사용.
수백 개의 값을 가진 열거형은 컴팩트함을 떨어뜨립니다. 그럼에도 불구하고 동일한 데이터라면 HashSet/HashMap보다 메모리 측면에서 유리한 경우가 많습니다.
오류 3: 직렬화 후 enum을 변경.
직렬화 후 값 추가/삭제/재정렬은 읽기 시 오류 또는 데이터 손실로 이어질 수 있습니다.
오류 4: null과 함께 EnumSet/EnumMap 사용.
EnumSet의 원소도, EnumMap의 키/값도 null일 수 없습니다.
EnumSet<Day> days = EnumSet.of(null); // NullPointerException!
EnumMap<Day, String> map = new EnumMap<>(Day.class);
map.put(null, "test"); // NullPointerException!
오류 5: EnumSet이 ‘일반적인’ Set라고 기대함.
EnumSet은 자신의 enum 값만 저장합니다. 열거형 밖의 임의 객체는 추가할 수 없습니다.
오류 6: ‘변경 가능한’ enum에 EnumSet/EnumMap 사용.
열거형이 런타임에 생성되거나 교체되는 경우(동적 로딩/리플렉션), 이러한 구조는 올바르게 동작하지 않습니다.
GO TO FULL VERSION