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

EnumSet/EnumMap

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

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
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 사용.
열거형이 런타임에 생성되거나 교체되는 경우(동적 로딩/리플렉션), 이러한 구조는 올바르게 동작하지 않습니다.

1
과제
JAVA 25 SELF, 레벨 28, 레슨 3
잠금
완벽한 주말 계획하기 🏖️
완벽한 주말 계획하기 🏖️
1
과제
JAVA 25 SELF, 레벨 28, 레슨 3
잠금
아트 갤러리 "영감"의 동적 가격 책정 🎨
아트 갤러리 "영감"의 동적 가격 책정 🎨
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION