본문 바로가기

JAVA 스터디

[JAVA] Enum(열거형)

열거 타입(Enum Type)의 등장

자바의 열거 타입(Enum Type)은 자바 1.5부터 등장하기 시작했습니다.

Enum이 등장하기 이전에는 상수값 관리를 정수 열거 패턴(int enum pattern)으로 했습니다.

정수 열거 패턴은 static final로 불변의 상수값을 만들어 사용하는 것입니다.

네이밍 규칙은 대문자로 하며, 변수명을 의미 있고 다른 상수들과 구분 지을 수 있도록 지어야 합니다.

 

Enum은 이미 선언된 Enum 상수 외의 객체는 사용할 수 없으며,

toString 메소드를 호출하면 인쇄 가능 문자열로 쉽게 변환할 수 있습니다.

 

int enum pattern

아래는 정수 열거 패턴의 예시입니다.

 

public class Frutis {
    private static final int APPLE = 1;
    private static final int GRAPE = 2;
    private static final int ORANGE = 3;
}

 

정수 열거 패턴의 단점은 타입 안전을 보장할 수 없으며, 표현력도 좋지 않습니다.

 

자바가 정수 열거 패턴을 위한 별도 이름공간(namespace)을 지원하지 않기 때문에

언더바(_)를 사용하여 접두어로 이름 충돌을 방지했습니다.

 

가장 큰 단점 중 하나는 정수 열거 패턴을 사용한 상수값(constant)은 컴파일을 하면

그 값이 클라이언트 파일에 그대로 새겨지므로, 상수값이 바뀌면 다시 컴파일해야 합니다.

 

상수값이 추가되도 문제입니다.

위 예제에서 다양한 과일을 계속 추가하게 되면 상수가 너무 많아져 가독성이 떨어지게 됩니다.

상수들이 어떤 것에 관련된 것인지 한눈에 보기 어려워지기도 합니다.

 

문자열 하드코딩을 줄이기 위해서 문자열을 상수처럼 사용하는

문자열 열거 패턴(string enum pattern)을 사용하기도 합니다.

이 방식은 상수의 의미를 출력하기에는 좋지만, 문자열 비교에 따른 성능 저하가 발생합니다.

위와 같은 문제를 극복하고자 열거 타입(Enum Type)이 생겼습니다.

 

Enum은 서로 관련있는 상수들의 집합을 의미합니다.

어떤 클래스가 상수만으로 작성되어 있으면 반드시 class로 선언할 필요는 없습니다.

이럴 때 class로 선언된 부분에 enum이라고 선언하면 "이 객체는 상수의 집합이다."라고 명시하는 것입니다.

enum은 enumeration이라는 셈, 계산, 열거, 목록이라는 영어단어의 앞부분만 따서 만든 예약어입니다.

 

Enum은 int로 선언한 상수와 성능면에서 비슷하나, 자료형을 메모리에 올리고

초기화하는 공간적/시간적 비용 때문에 일부 손해를 봅니다.

하지만 코드의 가독성이 높아지므로 제 3자가 코드를 리뷰하기 좋습니다.

 

enum 정의하는 방법

생성방법은 class 키워드 자리에 enum만 작성해주면 됩니다.

다음과 같은 방법으로도 정의할 수 있습니다.

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

enum Month {
    JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY,
    AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
}

public class Example {
    public static void main(String[] args) {
    
        // (열거체이름.상수이름)으로 열거체 사용
        Day day = Day.MONDAY;
        
        switch(day) {
        case MONDAY:
            System.out.println("월요입니다.");
            break;
        case TUESDAY:
            System.out.println("화요입니다.");
            break;
        case WEDNESDAY:
            System.out.println("수요입니다.");
            break;
        ...
        }
    }
}

 

위처럼 enum이라는 키워드를 사용하고 의미 있는 이름으로 상수의 집합을 지정해줍니다.

그리고 상수들을 차례대로 나열하면 각 상수들은 0부터 시작해서 1씩 증가하는 데이터를 가지게 됩니다.

 

만약 특정 값을 지정하고 싶다면 다음과 같이 별도로 지정하면 됩니다.

 

enum Day {
    MONDAY(3), TUESDAY(10), WEDNESDAY(-1), THURSDAY(9), FRIDAY(5), SATURDAY(20), SUNDAY(0);
}

 

 

switch문의 레이블에서 enum의 데이터 타입을 생략하고 상수만 입력해도 됩니다.

switch문의 레이블은 조건으로 넘어온 데이터 타입을 미리 알고 있기 때문입니다.

 

class Day {
    public final static Day MONDAY = new Day();
    public final static Day TUESDAY = new Day();
    public final static Day WEDNESDAY = new Day();
}

enum Day {
    MONDAY, TUESDAY, WEDNESDAY;
}

 

위의 코드에서 첫 번째는 class를 이용한 실수 패턴이고, 두 번째는 enum을 사용한 상수 정의입니다.

둘 다 똑같은 기능을 수행하지만 두 번째 방법이 더 간결하고 가독성이 좋습니다.

 

첫 번째 방법은 자바에서 많이 쓰이는 패턴입니다.

이런 패턴을 문법적으로 지원하는 것이 바로 enum 입니다.

enum을 이용하면 아래와 같은 장점을 얻을 수 있습니다.

 

  • 코드가 단순해지며 가독성이 좋아진다.
  • 인스턴스 생성과 상속을 방지합니다.
  • 키워드 enum을 사용하기 때문에 구현의 의도가 열거임을 분명하게 나타냅니다.

enum 클래스의 원소에 추가 속성 부여

enum의 각 열거형 상수에 추가적으로 속성을 부여할 수 있습니다.

위에서 소개한 정수 열거 패턴을 Enum으로 바꾸면 다음과 같습니다.

 

public enum Fruits {
    APPLE(1);
    GRAPE(2);
    ORANGE(3);
    
    private final int fruits;
    
    priavte Fruits(int fruits) {
        this.fruits = fruits;
    }
    
    public int getFruit() {
        return fruits;
    }
}

 

열거 타입 안에 int 자료형의 값이 저장되어 있습니다.

이 의미는 생성자를 호출하여 특정 데이터 값을 Enum 필드에 저장한다는 의미입니다.

필드는 public으로 선언해도 되지만, private으로 선언하고

getFruit() 메소드와 같이 별도의 public 접근자 메서드를 두는 게 좋습니다.

 

enum생성자가 private인 이유

자바에서 enum 타입은 열거형을 의미하는 특별한 클래스입니다.

따라서 일반 클래스와 마찬가지로 생성자(constructor)가 필요합니다.

물론 개발자가 명시하지 않아도 컴파일러에 의해 디폴트 생성자(default constructor)가 정의될 겁니다.

그러나 enum의 경우에는 일반 클래스와는 달리 private으로 정의해야 합니다.

이를 어기고 public, protected로 정의한다면, 다음과 같은 에러가 발생합니다.

 

enum 생성자 에러

 

enum타입은 고정된 상수들의 집합으로, 런타임(run-time)이 아닌 컴파일 타임(compile-time)에 모든 값을 알아야 합니다.

즉 다른 패키지나 클래스에서 enum타입에 대해 접근해서 값을 변경하는 것은 불가능합니다.

따라서 컴파일 시 타입 안정성이 보장됩니다.

(해당 enum클래스 내에서 까지도 new키워드로 인스턴스 생성이 불가능하며 newInstance(), clone()등의 메소드도 불가)

 

위와 같은 이유로 enum타입의 생성자를 private으로 지정하는 것입니다.

enum타입의 생성자는 private 접근제어자가 디폴트이므로 개발자가 따로 지정하지 않아도 됩니다.

 

enum이 제공하는 메소드 ( values() 메소드와 valueOf() 메소드 )

values() 메소드

values() 메소드는 해당 열거체의 모든 상수를 저장한 배열을 생성하여 반환합니다.

이 메소드는 자바의 모든 열거체에 컴파일러가 자동으로 추가해 주는 메소드입니다.

 

enum Rainbow { 
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET 
}

public class Example {
    public static void main(String[] args) {
        Rainbow[] arr = Rainbow.values();

        for (Rainbow rb : arr) {
            System.out.println(rb);
        }
    }
}

// 실행결과
RED
ORANGE
YELLOW
GREEN
BLUE
INDIGO
VIOLET

 

valueOf() 메소드

valueOf() 메소드는 전달된 문자열과 일치하는 해당 열거체의 상수를 반환합니다.

 

enum Rainbow {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

public class Example {
    public static void main(String[] args) {
        Rainbow rb = Rainbow.valueOf("GREEN");
        System.out.println(rb);
    }
}

// 실행결과
GREEN

 

java.lang.Enum

Enum 클래스는 모든 자바 enum타입의 공통된 조상 클래스입니다.

Enum 클래스에는 enum타입을 조작하기 위한 다양한 메소드가 포함되어 있습니다.

 

기본 제공 메서드들

  • final String name() : 상수의 이름을 반환합니다.
  • final int ordinal() : 상수의 열거형에서의 순서를 반환합니다.
  • String toString() : 열거형 상수의 문자열 표현을 반환합니다. 재정의 가능합니다.
  • final boolean equals(Object obj) : 매개변수로 전달된 객체가 열거형 상수와 같다면 true를 반환하고, 그렇지 않다면 false를 반환합니다.
  • final int hashCode() : 이 열거형 상수에 대한 해시 코드를 반환합니다. 실제 구현은 super.hashCode()를 호출합니다.
  • final int compareTo(E obj) : 열거형의 순서를 비교합니다. 순서가 낮은 경우 음수, 같은 경우 0, 높은 경우 양수를 반환합니다.
  • final Class <E> getDeclaringClass() : 열거형 상수의 타입에 해당하는 Class 객체를 반환합니다.
  • final Object clone() : 열거형이 복제되지 않도록 보장하며, 단일 상태임을 보장해줍니다. 사용 시CloneNotSupportedException이 발생합니다. 열거형 상수를 만들기 위해 컴파일러 내부적으로 사용된다고 합니다.
  • final void finalize() : enum 클래스가 finalize 메서드를 가질 수 없음을 보장해줍니다.

 

EnumSet

EnumSet은 enum 클래스의 상수로 구현된 특별한 Set 컬렉션입니다.

이때 Set 컬렉션이란 데이터를 중복해서 저장할 수 없고, 순서가 보장되지 않는 자료구조를 말합니다.

EnumSet은 Set 인터페이스를 구현하고, AbstractSet을 상속합니다.

그러나 EnumSet은 대부분의 메소드를 재정의해서 사용합니다.

 

EnumSet

EnumSet을 사용할 때는 몇 가지 주의사항이 있습니다.

 

  1. 열거형 값만 저장할 수 있으며, 모든 값은 같은 열거형에 속해야 합니다.
  2. null을 추가할 수 없습니다. 만약 추가하게 된다면 NullPointerException이 발생합니다.
  3. EnumSet은 동기화되지 않습니다. 따라서 여러 쓰레드가 enum set에 동시에 엑세스하고 쓰레드 중 하나 이상이 set을 수정하는 경우 외부에서 동기화해야 합니다.
  4. 복사본에 fail-safe iterator(장애 발생 시 작업을 중단하지 않음)를 사용하여 컬렉션을 순회할 때, 컬렉션이 수정되어도 ConcurrentModificationException이 발생하지 않습니다.

EnumSet 내부는 비트 벡터로 구현되어있으며, 원소가 총 64개 이하라면

EnumSet 전체를 long 변수 하나로 표현하여 비트 필드에 비견되는 성능을 보여줍니다.