Programming/JAVA

Enum이란?

잇나우 2021. 3. 2. 00:03
반응형

Enum이란?

enum이란 enumerated type의 줄임말로 열거형이라고 부르기도 하는데 컴퓨터 프로그래밍에서 열거형(enumerated type, enumeration)은 요소, 멤버라 불리는 명명된 값의 집합을 이루는 자료형이다. 열거자 이름들은 일반적으로 해당 언어의 상수 역할을 하는 식별자이다.
일부 열거자 자료형은 언어에 기본적으로 포함되어 있을 수 있다. 예를 들면 boolean 자료형은 false와 true 값이 미리 정의된 열거형으로 볼 수 있다. 많은 언어에서 사용자들이 새로운 열거형을 정의할 수 있게 하고 있다.

Enum 장점

  • IDE의 지원을 받을 수 있다. 자동완성, 오타검증, 텍스트 리팩토리 등
  • 허용 가능한 값들을 제한할 수 있다.
  • 리팩토링 시 변경 범위가 최소화 된다. 내용을 추가해도 Enum 코드만 수정하면 된다.
  • 확실한 부분과 불확실한 부분을 분리할 수 있다.
  • 문맥(Context)을 담을 수 있다.

Enum 정의하는 방법

{}안에 상수의 이름을 나열하면 된다.

enum 열거형이름 {
    상수명1, 상수명2, 상수명n
}

enum에 정의된 상수를 사용하는 방법은 열거형이름.상수명 이다. 클래스의 static 변수를 참조하는 것과 같다.

열거형 상수는 필드를 가질 수 있다. 필드를 추가하는 방법은 추가하고 싶은 필드를 정의하고 생성자의 인자로 추가 한 뒤 각각의 상수에 값을 입력하면 된다. ()안에는 primitive 타입 데이터는 물론 레퍼런스 타입 객체도 들어올 수 있다. 단 열거형 상수에 값을 부여할 경우 이에 해당하는 생성자도 함께 정의해주어야 한다. 상수 선언시 ()에 값을 부여하는 것은 ()에 해당하는 매개변수를 가진 생성자를 호출하는 것이며 경우에 따라서 매개변수로 들어온 값을 저장하는 변수도 선언해줄 수 있다.

enum Example {
    APPLE(1), BANANA(5), KIWI(4);

    private int value;
    Example (int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

enum에서 생성자를 정의한다면 private 로 선언해야한다. public, default, protected로 선언시에 컴파일 에러가 발생한다.
enum 타입은 고정된 상수들의 집합으로 enum에 대한 모든 정보들은 런타임이 아닌 컴파일 타임에 고정 되어야 한다. 그렇기 때문에 컴파일 시 안정성을 보장하기 위해 private 생성자만 사용이 가능하고 다른 패키지나 클래스에서 enum 타입에 접근해서 동적으로 값을 지정할 수 없다.
열거형의 생성자는 접근제어가 묵시적으로 private이기 때문에 외부에서는 열거형의 생성자를 호출할 수 없다.

열거형 상수간의 비교는 ==을 사용할 수 있다. equals()가 아닌 ==로 비교가 가능하다는 것은 그만큼 빠른 성능을 제공한다는 이야기이다. 하지만 <, > 와 같은 비교 연산자는 사용할 수 없지만 compareTo()는 사용이 가능하다.

switch문에서도 열거형을 사용할 수 있다

void eat() {
    switch(example) {
        case APPLE : x++;
            break;
        case BANANA : x--;
            break;
        case KIWI : x *= 2;
            break;
    }
}

switch 문에 열거형을 사용할 땐 case문에 열거형 타입의 이름은 적지 않고 상수의 이름만 적어야 한다는 제약이 있다.

Java 5 이전의 열거형 (int Enum 패턴)

Java 5 이전 버전까지는 열거형을 나타내는 표준 방식이 int Enum 패턴이었다. 패턴은 아래 예시와 같이 사용한다.

public class IntEnum {
    public static final int SEASON_WINTER = 0;
    public static final int SEASON_SPRING = 1;
    public static final int SEASON_SUMMER = 2;
    public static final int SEASON_FALL = 3;
}

int Enum 패턴은 아래와 같은 단점을 가지고 있다.

  • Not type_safe
  • No name_space
    다른 int Enum 타입과의 충돌을 피하려면 int 열거형의 상수 앞에 문자열을 붙여야 한다.
  • Brittleness (취성 / 변경에 약함)
    int Enum은 컴파일 타임 상수(static) 이므로 상수의 값이 바뀌면, 해당 상수를 참조하는 모든 소스를 다시 컴파일 해야한다.
  • printed values are uniformative (출력으로 어떤 정보도 알 수 없다)
    int Enum은 단지 정수이기 때문에 출력해보면 숫자만 출력된다. 우리는 숫자를 보고 무엇을 나타내는지, 심지어 어떤 타입인지에 대해서 알 수 없다.

상수 특정 메서드 (constant-specific method)

enum type에 필드나 행동(메서드)를 추가할 수 있다. 하나의 메서드에 대해 각각의 열거형 상수는 다른 행동을 정의할 수 있다. 아래 예시 코드는 열거형 상수가 4가지 기본 산술 연산을 나타내고 eval 메서드가 연산을 수행하는 enum type의 예제이다.

public enum Operation {
    PLUS, MINUS, MULTIPLY, DIVIDE;

    int eval(int x, int y) {
        switch (this) {
            case PLUS: return x + y;
            case MINUS: return x - y;
            case MULTIPLY return x * y;
            case DIVIDE return x / y;
        }
        throw new AssertionError("잘못된 산술" + this);
    }
}

class Exam {
    public static void main(String[] args) {
        int result = Operation.PLUS.eval(1, 2);
        System.out.println(result);
    }
}

위의 예제에서 switch문 바깥에 return이 없기 때문에 throw 문이 없으면 컴파일 에러가 난다.

Operation에 새로운 열거형 상수를 추가할 때마다 switch문에 새로운 case를 추가해야 한다는 단점이 잇다. 만약 추가하는 것을 잊어버리고 새로운 열거형 상수로 연산을 진행하면 에러를 throw하게 될것이다.
이러한 문제점을 해결하는 방법으로 각 열거형 상수에 다른 행동을 정의하는 방법이 있다. enum type에서 추상 메서드를 선언하고 각 상수에 구체적인 메서드로 재정의 할 수 있다. 이러한 방법을 상수 특정 메서드 (constant-specific method)라고 한다.

public enum Operation {
    PLUS {
        int eval(int x, int y) {
            return x + y;
        }
    },
    MINUS {
        int eval(int x, int y) {
            return x - y;
        }
    },
    MULTIPLY {
        int eval(int x, int y) {
            return x * y;
        }
    },
    DIVIDE {
        int eval(int x, int y) {
            return x / y;
        }        
    };

    // 연산을 위한 추상 메서드
    abstract int eval(int x, int y);
}

class Exam {
    public static void main(String[] args) {
        int x = 4;
        int y = 2;
        for (Operation op : Operation.values()) {
            // ...
        }
    }
}
enum Direction { EAST, SOUTH, WEST, NORTH }

사실 위의 열거형 상수 하나하나(EAST, SOUTH, WEST, NORTH)는 Direction의 객체이다. Direction 클래스의 static 상수 EAST, SOUTH, WEST, NORTH의 값은 객체의 주소고, 이 값은 바뀌지 않는 정적(static) 값이므로 ==으로 비교가 가능한 것이다.

enum과 switch문을 같이 사용할 경우 발생하는 문제점이 많다.

  1. 새로운 열거형 상수가 추가된다면 switch문을 수정해야 한다.
  2. 모든 case에 공통된 로직이 추가되면 갯수의 배수만큼 코드 길이가 길어진다.
  3. 결국 유지보수성이 낮으며 가독성은 높은 코드가 된다.
interface Operator {
    double apply(double x, double y);
}
enum Operation {
    PLUS ((x, y) -> x + y),
    MINUS((x, y) -> x - y),
    MULTIPLE((x, y) -> x * y),
    DIVIDE((x, y) -> x / y);

    private final Operator operator;

    Operation(Operator operator) {
        this.operator = operator;
    }

    public double apply(double x, double y) {
        return operator.apply(x, y);
    }
}

public class Exam {
    public static void main(String[] args) {
        System.out.println(Operation.PLUS.apply(1, 2));
    }
}

람다를 사용하여 위 코드와 같이 작성할 수 있다.

  1. 추상 메서드를 함수형 인터페이스로 분리한다.
  2. 생성자로 분리한 함수형 인터페이스 자료형을 주입받도록한다.
  3. apply() 메서드가 실행되면 주입된 객체의 apply()를 실행한다.

새로운 열거형을 추가해야한다면 단순히 열거형 상수와 로직으로 처리할 람다식만 기술해주면 된다.

Enum이 제공하는 메서드

Java의 enum은 기본적으로 제공되는 메서드가 몇가지 있다.

메서드 이름 설명
toString() 해당 상수의 이름을 문자열로 반환한다.
name() 해당 상수의 이름을 문자열로 반환한다.
compareTo() 정렬의 기준을 위한 메서드이다.
비교 대상보다 순서가 빠르면 -1, 같으면 0, 느리면 1을 반환한다
정렬 순서는 상수가 선언된 순서가 디폴트로 지정되어 있다.
ordinal() 상수의 선언 순서에 따른 인덱스 (Zero based) 값을 반환한다.
Enum 안에는 private final int ordinal; 이 정의되어 있고 이를 사용한다.
valueOf() 인자로 받은 이름과 같은 Enum 값으로 반환한다.
values() 선언된 모든 Enum 값을 순서대로 배열에 담아서 반환한다.

toString()과 name() 차이점

public final String name() {
    return name;
}
public String toString() {
    return name;
}
  • toString()과 name()은 같은 값을 반환한다.
  • name()은 final로 선언된 메서드로 오버라이딩이 불가능하다.
  • toString()은 일반적인 Object 클래스의 메서드로 오버라이딩 할 수 있다.

values()와 valueOf()

  • values()
    열거형의 모든 상수를 배열에 담아 반환한다.

      Direction[] arr = Direction.values();
  • valueOf(String name)
    열거형 상수의 이름으로 문자열 상수에 대한 참조를 얻을 수 있게 해준다.

      Direction dirction = Direction.valueOf("WEST");
      Direction.WEST == Direction.valueOf("WEST"); // true 반환

java.lang.Enum

Enum 클래스는 모든 열거형이 공통으로 상속받는 추상 클래스이다.

  • abstract 클래스로 인스턴스를 생성할 수 없다.
  • 인스턴스의 생성을 막고자 추상클래스로 선언했기 때문에 abstract 메서드가 존재하지 않는다.
  • 우리가 선언하는 enum 타입은 이 Enum 클래스를 자동으로 상속하여 Enum에서 제공하는 ordinal과 같은 변수 및 메서드를 사용할 수 있게 된다.

EnumSet

JDK 5에서 등장한 java.util 패키지 클래스로 Set 인터페이스를 기반으로 열거형 타입으로 지정해놓은 요소들을 가장 쉽고 빠르게 배열처럼 요소들을 다룰 수 있는 기능을 제공한다.
EnumSet은 비트연산을 이용해 메모리 공간도 적게 차지하고 속도도 빠르다. Set 기반이지만 enum과 static 타입의 메서드들로 구성되어 있어 안정성을 최대한 추구하면서 편리한 사용이 가능하다. 또한 HashSet과 달리 올바른 버킷을 찾기 위해 해시 코드를 꼐산할 필요가 없다.

Set 인터페이스를 구현하고 AbstractSet 인터페이스를 상속하고 있어 열거형을 이용해 Collection 인터페이스 및 set의 기능을 사용할 수 있다.

EnumSet을 사용하려면 몇가지 중요한 사항을 고려해야 한다.

  • 생성자 오버라이딩이 불가능하고 동일한 타입의 열거 상수만 선언이 가능하다
  • null 값을 넣거나 NullPointerException을 던질 수 없다
  • 쓰레드로부터 안전하지 않으므로 필요한 경우 외부에서 동기화해야한다.
  • 상수는 열거형에 선언된 순서에 따라 저장된다
  • 복사본에서는 Fail-Safe 방식의 Iterator를 사용한다. 동작 중 컬렉션이 수정되어도 작업을 중단하지 않고 진행한다. ConcurrentModificationException이 발생하지 않는다.
메서드 이름 설명
copyOf(EnumSet s) 매개변수로 들어온 EnumSet을 복사한다.
nonOf(Class elementType) 빈 EnumSet을 반환한다.
of(E e1, E e2) 열거형 상수 2개를 입력받아 새로운 EnumSet에 넣어 반환한다.
complementOf(EnumSet s) 매개변수에 들어온 EnumSet의 열거형 상수들을 제외한 열거형 상수들을 새로운 EnumSet에 넣어 반환한다.
range(E from, E to) 인자로 받은 열거형 상수 사이의 범위를 인덱스의 순서대로 새로운 EnumSet에 넣어 반환한다. 단 앞선 매개변수의 인덱스가 빠르면 런타임에 에러가 발생한다.
동기식으로 사용할 필요가 있으면 Collections.synchronizedSet을 사용한다.

EnumMap

EnumMapHashMap과 비슷하게 Enum을 기준으로 KeyValue를 가진 자료구조이다. HashMapKey의 고유성을 위해 해싱 처리를 해줬던것과는 달리 Enum의 상수는 이미 그 자체로 고유한 싱글턴 객체이므로 해싱처리를 해줄 필요가 없다. 그렇기 떄문에 HashMap보다 빠르다고 알려진 Map의 형태 중 하나이다.
또 다른 EnumMap의 장점으로는 순서를 기억한다는 특징이 있다. Enum 자체도 ordinal을 통해 순서가 나뉘어져 있어 HashMap에서는 TreeMap처럼 정렬된다 하더라도 입력시 선응이 우수하다는 특징이 있다.

EnumMap<K, V> em = new EnumMap<K, V>(Class keyType);
메서드 이름 설명
put(K key, V value) key값과 value 값을 받아 내부 배열에 저장한다.
putAll(Map<? extends K, ? extends V> m 이미 생성된적있는 Map 객체를 내부 배열에 저장한다.
size() EnumMap의 Key와 Value 쌍의 갯수를 반환한다.
get(Object key) key를 통해서 value의 값을 반환한다.
containsKey(Object key) EnumMap에 특정 key가 존재하는지 확인 후 boolean 타입으로 반환한다.
containsValue(Object value) EnumMap에 특정 value 값이 존재하는지 확인 후 boolean 타입으로 반환한다.
replace(K key, V value) 기존 key에 있던 value 값을 바꾼다.
replace(K key, V oldValue, V newValue) 안정성을 보장해주는 방법으로 key의 이전 value 값이 맞으면 현재값으로 변경해준다.

동기식으로 사용할 필요가 있다면 Collections.synchronizedMap을 사용한다.

Map<EnumKey, V> m = Collections.synchronizedMap(new EnumMap<EnumKey, V>(...));

Enum 싱글톤

enum의 문법적 특성을 이용한 싱글톤 객체 생성

  • 싱글톤의 특징(단 한 번의 인스턴스 호출, Thread간 동기화)을 가지며 비교적 간편하게 사용할 수 있는 방법이다.
  • 단 한번의 인스턴스 생성을 보장하며 사용이 간편하고 직렬화가 자동으로 처리되고 직렬화가 아무리 복잡하게 이루어져도 여러 개게가 생길 일이 없다.
  • 리플렉션을 통해 싱글톤을 깨뜨릴 수 없다.

참고
https://parkadd.tistory.com/50
https://velog.io/@kwj1270/Enum

반응형

'Programming > JAVA' 카테고리의 다른 글

애너테이션이란?  (0) 2021.03.06
멀티 쓰레드 프로그래밍이란?  (0) 2021.03.01
예외란?  (0) 2021.02.13
인터페이스란?  (0) 2021.02.06
패키지란?  (0) 2021.01.21