CodeGym /행동 /JAVA 25 SELF /EnumSet/EnumMap

EnumSet/EnumMap

JAVA 25 SELF
레벨 28 , 레슨 3
사용 가능

1. 소개

Java에는 열거형(enum)을 다루기 위한 전용 컬렉션인 EnumSetEnumMap이 있습니다. 이들은 표준 라이브러리(java.util)에 포함되어 있으며 enum 타입을 최대한 효율적으로 처리하도록 설계되었습니다.

EnumSet과 EnumMap의 내부 동작 방식은?

EnumSet은 비트 마스크처럼 동작합니다. 각 열거형 요소가 정확히 한 비트를 차지하는 플래그 모음을 떠올리면 됩니다. 비트가 켜져 있으면 요소가 집합에 포함되고, 꺼져 있으면 포함되지 않습니다. 모든 것은 숫자 배열(long[])에 저장되며, enum의 값이 64 미만이라면 전체 집합이 단 하나의 숫자에 들어갑니다!

EnumMap은 더 단순합니다. 값의 배열이며, 인덱스로 enum 요소의 순서 번호(ordinal)를 사용합니다. 익숙한 HashMap<Enum, V> 대신 매우 컴팩트하고 빠른 구조를 얻습니다.

무엇이 좋아질까요? 추가, 삭제, 포함 여부 확인이 모두 O(1)에 수행됩니다. 메모리 사용도 최소입니다(특히 HashSetHashMap과 비교했을 때). 요소 반복 순서는 항상 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 변경과 직렬화

EnumSetEnumMap은 사용 중인 enum의 구성과 순서에 의존합니다. 새 요소를 추가하거나 기존 요소를 삭제하거나 위치를 바꾸면, 저장되었거나 직렬화된 컬렉션이 올바르게 동작하지 않을 수 있습니다.

직렬화

  • EnumSetEnumMap은 직렬화가 가능하지만, 직렬화와 역직렬화 사이에 enum이 변경되면 오류나 데이터 손실이 발생할 수 있습니다.
  • 직렬화 이후 enum 값이 삭제되면, 읽는 과정에서 예외가 발생할 가능성이 매우 높습니다.

모범 사례:

  • EnumSet/EnumMap을 직렬화해야 한다면, 해당 enum이 안정적임을 확신할 때에만 하세요.
  • 장기 보관 용도라면 값의 문자열 표현 목록 등으로 저장하는 방법을 사용하세요.

4. 유용한 팁

EnumSet/EnumMap과 일반 컬렉션 비교

컬렉션 키/원소 내부 구조 성능 메모리 Null
EnumSet
enum
비트 마스크 O(1) 매우 적음 불가
HashSet<Enum>
enum
해시 테이블 O(1) 더 많음 가능
EnumMap
enum
ordinal 기반 배열 O(1) 매우 적음 불가
HashMap<Enum, V>
enum
해시 테이블 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 사용.
열거형이 런타임에 생성되거나 교체되는 경우(동적 로딩/리플렉션), 이러한 구조는 올바르게 동작하지 않습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION