Technology/Java

Java의 Enum에 대해 알아보자!

ikjo 2022. 10. 2. 01:53

Enum이란?

Enum은 '열거'라는 뜻을 가지는 enumeration의 약어인데, 자바에서는 JDK1.5부터 Enum Type(이하 '열거형')이라는 특수한 자료형(data type)을 제공한다. 열거형은 특정 변수가 사전에 정의된 상수들의 집합에 속하도록 해주므로, 해당 변수의 값은 사전에 열거형을 통해 정의된 값들 중 하나여야만 한다. 예를 들면, 요일의 경우 '월, 화, 수, 목, 금, 토, 일'로서 단 7개만 존재하므로 열거형 사용을 고려해볼 수  있다. 

 

enum 정의하는 방법

enum 키워드를 이용함으로써 다음과 같이 열거형을 정의해볼 수 있다. 단순히 중괄호 { } 안에 상수의 이름을 나열하기만 했다.

 

public enum Day {
    SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY 
}

 

이때 열거형의 각각의 필드들은 모두 상수이기 때문에 대문자로 표기했다. 참고로 enum을 선언한다는 것은 열거형(enum type)이라고 불리는 하나의 클래스를 정의하는 것(enum은 클래스의 일종이다.)과 같아서 enum에는 필드뿐만 아니라 메서드도 정의될 수 있다. (이에 대해선 아래에서 좀 더 자세히 다룰 예정이다.)

 

이렇게 선언된 열거형에 접근하기 위해서는 '열거형이름.상수명' 방식으로 접근하면 된다. (에를 들어, Day.MONDAY 등)

 

이처럼 열거형에 요일과 관련된 상수를 관리함으로써 얻을 수 있는 이점은 무엇이 있을까?

 

enum의 진짜 용도

주의할 점으로는 열거형은 단순히 서로 관련된 상수를 한 곳에 관리하기 위한 용도로만 사용되는 것이 아니라는 점이다. 단순히 서로 관련된 상수를 한 곳에 관리하기 위함이라면 다음과 같이 하나의 클래스에 여러 정적 상수들을 선언하고 관리해도 무방하다.

 

class Day {

    public static final int SUNDAY = 1;
    public static final int MONDAY = 2;
    public static final int TUESDAY = 3;
    public static final int WEDNESDAY = 4;
    public static final int THURSDAY = 5;
    public static final int FRIDAY = 6;
    public static final int SATURDAY = 7;

}

 

하지만 위와 같이 관리할 경우 생길 수 있는 문제점은 사용자로부터 전달된 데이터의 값이 현재 관리되고 있는 상수들 처럼 '요일'을 나타내기 위한 목적을 나타내고 있는지, 아니면 '요일과는 무관한 다른 목적'을 나타내는 데이터인지 구분하기 어렵다는 점이다. 즉, 실제 값이 같은지만 검증할 수 있을뿐 어떤 타입(type)인지는 검증할 수 없는 것이다.

 

아래 예시는 사용자의 입력 데이터를 단순히 정적 상수로 비교한 경우이다. 이 경우 사용자의 입력 데이터 값(1)이 정말 요일을 나타내는지 보장하기 어렵다.

 

class Day {
    
    public static final int MONDAY = 1;
    
    // ...
}

        // ...

        int userInput = 1;
        
        // 조건식의 결과는 true but, userInput이 1인데, 이게 요일을 의미하는건가?
        if (userInput == Day.MONDAY) {
            // ...
            System.out.println("오늘은 월요일!");
        }
        
        // ...

 

하지만 자바의 열거형은 '타입에 안전(typesafe)'하기 때문에 실제 값이 같아도 타입이 다르면 (실행 전이라면) 컴파일 에러가 발생한다. 물론 사용자의 입력 데이터는 실행 단계에서 결정되기 때문에 컴파일러가 체크할순 없지만 사용자 입력 데이터가 어찌저찌해서 enum에 정의된 상수 외의 데이터가 할당됐더라도 에러가 발생함으로써 논리적 에러가 발생할 수 있는 처리 작업을 방지할 수 있다.

 

enum Day {
    SUNDAY, MONDAY, ...
}

        Day userInput = Day.MONDAY;
        
        // ...
        
        if (userInput == Day.MONDAY) { // 타입 일치!
            // ...
            System.out.println("오늘은 월요일!");
        }
        
        // ...
        
        /*
        int userInput = 1;
        if (userInput == Day.MONDAY) { // 타입 불일치(컴파일 에러 발생)
            // ...
        }
        */

 

참고로 열거형 상수간의 비교에선 equals() 메서드가 아닌 비교 연산자 '=='를 사용할 수 있다. (equals 보다 빠른 성능)

 

이처럼 열거형은 고정된 상수들의 집합을 표현하는데 매우 유용하다. 지금까지 7개로 고정된 '요일'을 나타내기 위한 예시를 들었는데, 이외에도 고정적인 상수들의 집합의 형태를 띄는 태양계 행성들(수성, 금성, 지구, 화성 등)이나 스레드의 상태 이름들(대기, 실행, 종료 등)도 예시로 생각해볼 수 있겠다.

 

 

enum이 제공하는 메서드 values와 valueOf

enum이 만들어질 때 컴파일러는 자동으로 일부 특수한 메서드를 추가해준다. 이 중에는 values()라는 정적 메서드가 있는데, 이 메서드는 열거형에 선언된 모든 상수들을 선언된 순서대로 배열에 저장하여 반환한다.

 

    enum Day {
        SUNDAY, MONDAY, ...
    }
    
        // ...

        Day[] days = Day.values(); // enum에 values라는 메서드는 정의되지 않았다.

 

이외에도 valueOf(String name)라는 정적 메서드도 존재한다. 이 메서드는 열거형 상수의 이름으로 문자열 상수에 대한 참조를 얻을 수 있게 해준다. 이러한 점을 미루어 보았을 때 열거형 상수 하나 하나가 Day 객체라는 것을 생각해볼 수 있다. 이때 각각의 값은 모두 객체(유일한)의 주소를 지니는데, 이 값은 바뀌지 않는 값이므로 앞서 == 연산으로 열거형 타입끼리 비교할 수 있었던 것이다.

 

    enum Day {
        SUNDAY, MONDAY
    }

        // ... 

        Day day = Day.valueOf("MONDAY"); // valueOf 메서드 역시 enum에 선언 X
        
        if (day == Day.MONDAY) { // true
            System.out.println("오늘은 월요일!!");
        }

 

참고로 이외에도 name(), ordinal() 등의 메서드도 사용이 가능한데, 이는 모든 enum들은 java.lang 패키지의 Enum 클래스를 상속받기 때문이다. 이때 자바의 경우 다중 상속이 불가능하므로 모든 enum들은 어떤 다른 클래스를 상속받을 수 없다.

 

 

열거형에 멤버 추가하기

지금까지 열거형에 상수 이름만 선언해주었는데, 이외에도 멤버를 추가할 수도 있다. (앞서 열거형 상수 하나 하나가 객체라고 했었던 것을 상기해보자!) 다음 예시 코드를 살펴보자.

 

    enum Day {
        SUNDAY(1), MONDAY(2), TUESDAY(3), WEDNESDAY(4),
        THURSDAY(5), FRIDAY(6), SATURDAY(7);
        // 필드와 메서드가 있는 경우 상수 끝에 세미콜론(;)을 붙여야 한다!

        private final int value;

        Day(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    }
    
        // ...
        Day day = Day.MONDAY;
        System.out.println(day.getValue()); // 2

 

위 코드를 살펴보면 이전과 달리 상수 이름 옆에 괄호로 값을 할당해준 것을 볼 수 있다. 이와 함께 Day 열거형에 value라는 정수형 필드와 생성자 그리고 getter 메서드가 추가된 것을 확인할 수 있다. 즉, 각각의 상수별로 생성자 초기화를 통해 자기 자신만의 상태 값을 가질 수 있게 된 것이다. 아울러 getter 메서드를 통해 해당 상태 값을 얻을 수 있게 되었다. 이는 각각의 상수가 객체라는 사실을 더욱 와닿게 한다.

 

이때 필드의 경우 외부에서 접근하지 못하도록 접근 제어자를 private으로 지정한 반면, 생성자에는 별다른 접근 제어자가 붙지 않았다. 이는 열거형의 생성자는 사용자가 호출하지 못하도록 private 접근 제어자로 설정되어 있기 때문이다. (이 생성자는 enum body 시작 부분에 정의된 상수들을 자동으로 생성함) 이는 상수가 단 한 개만 존재하도록 보장한다. (상수는 변하지 않는 값이닌까 여러개 있을 필요가 없다!)

 

참고로 특정 Day 객체가 특정 요일을 나타낼 경우 어떤 동작을 처리하도록 하기위해선 다음과 같이 switch문(또는 if문)을 이용한 별도 로직 처리 작업이 필요한데, 이를 호출하는 측에서 처리해주지 않고 enum에 아래와 같은 메서드를 선언해주면 Day 객체 내에서 처리가 가능하다. (디미터의 법칙)

 

    enum Day {
        SUNDAY(1), MONDAY(2), TUESDAY(3), WEDNESDAY(4),
        THURSDAY(5), FRIDAY(6), SATURDAY(7);

        // ...
        
        public void check(Day day) {
            switch (day) {
                case SUNDAY : {
                    System.out.println("오늘은 일요일!");
                    break;
                } case MONDAY : {
                    System.out.println("오늘은 월요일!");
                    break;
                } case TUESDAY : {
                    System.out.println("오늘은 화요일!");
                    break;
                } case WEDNESDAY : {
                    System.out.println("오늘은 수요일!");
                    break;
                } case THURSDAY : {
                    System.out.println("오늘은 목요일!");
                    break;
                } case FRIDAY : {
                    System.out.println("오늘은 금요일!");
                    break;
                } default : {
                    System.out.println("오늘은 토요일!");
                }
            }
        }
    }

 

이처럼 상수와 관련된 작업을 처리할 수 있는 메서드를 한 곳에 모아 관리할 수 있는 것도 enum을 사용함으로써 얻을 수 있는 이점이다.

 

EnumSet 자료구조

이러한 열거형 데이터를 처리하는데 특화된 Set Collection의 자료구조가 있는데, 그것은 바로 EnumSet이다.

 

참고로 EnumSet은 (AbstractSet 추상 클래스를 상속받은) 추상 클래스이며, 정적 팩터리 메서드 등 다수의 정적 메서드를 가진다. JDK에서는 RegularEnumSet와 JumboEnumSet라는 2개의 EnumSet 구현체를 제공하는데, EnumSet에 모든 메서드들은 산술 비트 연산을 사용함으로써 구현된다. 이로서 빠른 연산과 함께 일정 시간 안에 실행되는 장점이 있다.

 

EnumSet의 중요한 특징

이러한 EnumSet 자료구조를 사용함에 있어 중요한 몇가지 사항들이 있다.

 

1. 우선 열거형 데이터만을 그것도 같은 타입의 열거형 데이터를 가져야한다는 점이다.

 

2. 또한 null 값을 허용하지 않는다. 만일 지켜지지 않을 경우 NullPoniterException 예외가 발생한다.

 

3. 동기화 처리가 되어 있지 않아(not thread-safe) 필요 시 별도의 동기화 처리가 필요하다.

 

4. 열거형에 정의된 순으로 요소들이 저장된다.

 

5. fail-safe 방식의 iterator를 사용하기 때문에 순회할 때 데이터가 수정되어도 ConcurrentModificationException 예외가 발생하지 않는다.

 

※ 참고 : fail-fast 방식을 사용하는 경우 순회 중 데이터가 수정되면 ConcurrentModificationException 예외가 발생한다.

 

EnumSet 생성 메서드

가장 단순한 EnumSet 생성 메서드로는 allOf 메서드와 noneOf 메서드가 있다.

 

우선 allOf 메서드는 다음과 같이 특정 열거형에 정의된 모든 요소들을 포함한 EnumSet을 생성한다. 앞서 정의한 Day 열거형을 활용해보자.

 

    EnumSet<Day> days = EnumSet.allOf(Day.class);

 

 

반면에 noneOf 메서드는 특정 열거형에 대한 비어있는 EnumSet을 생성한다.

 

    EnumSet<Day> days = EnumSet.noneOf(Day.class);

 

 

이외에도 해당 열거형의 부분적인 요소만 담은 EnumSet을 생성하고 싶을 경우에는 여러개의 메서드로 오버로딩 된 of 메서드나 range 메서드를 사용할 수 있으며, 이외에도 특정 요소를 제외하여 생성하는 complementOf 메서드나 특정 EnumSet 등을 복사하는 copyOf 메서드도 있다.

 

사실 EnumSet은 생성 메서드가 일반적인 Set 자료구조랑 차이가 있을 뿐 사실 그 외에 메서드들의 경우 Set 자료구조가 지원하는 모든 연산을 처리할 수 있다.

 

        EnumSet<Day> days = EnumSet.noneOf(Day.class);
        days.add(Day.MONDAY); // 요소 추가
        days.add(Day.TUESDAY); // 요소 추가
        days.contains(Day.MONDAY); // 요소 포함 여부 검증
        days.forEach(System.out::println); // 요소 순회 출력
        days.remove(Day.MONDAY); // 요소 삭제
        
        // ...

 

 

참고자료

  • 도우출판 "자바의 정석"
  • https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html
  • https://www.baeldung.com/java-enumset
  • https://docs.oracle.com/javase/8/docs/api/java/util/EnumSet.html