[Java] Enum(열거형)

enum의 메소드( values(), valueOf(), ordinal() ), EnumSet

Posted by Wonyong Jang on January 29, 2021 · 22 mins read

목표

자바의 열거형에 대해 학습하세요.

학습할 것

  • enum 이란
  • enum의 특징
  • enum이 제공하는 메소드( values()와 valueOf() )
  • java.lang.Enum
  • EnumSet

1. Enum(열거형) 이란

데이터 중에는 몇 가지로 한정된 값만을 갖는 경우가 흔히 있다. 예를 들어 계절은 봄, 여름, 가일, 겨울 그리고 요일은 월, 화, 수, 목, 금, 토, 일 이라는 한정된 값을 가진다. 이렇게 한정된 값만을 가지는 데이터 타입을 열거형(enumerated type)이라고 부르며 서로 연관된 상수들의 집합니다.

enum이란 enumerated type의 줄임말로 열거형에 사용될 수 있는 특정한 값들을 정의해서 해당 값들만 사용할 수 있게 한다.

기존에 상수를 사용하면서 발생했던 문제(type safe)를 개선하고자 자바 5부터 추가된 기능이다.

Enum은 왜 만들어졌는가?

Enum을 잘 사용하면 코드의 가독성을 높이고 논리적인 오류를 줄일 수 있다.
Enum을 잘 사용하기 위해 우선 Enum이 왜 탄생했는지 먼저 알아보자.

예시로 과일의 이름을 입력받으면 해당 과일의 칼로리를 출력하는 프로그램이 있고, 과일의 이름을 다음과 같이 상수로 관리한다고 생각해보자.

public class Test{

    public static final int APPLE = 1;
    public static final int PEACH = 2;
    public static final int BANANA = 3;
    public static void main(String[] args) {

        int type = APPLE;

        switch (type) {
            case APPLE:
                System.out.println("30 kcal");
                break;
            case PEACH:
                System.out.println("50 kcal");
                break;
            case BANANA:
                System.out.println("10 kcal");
                break;
        }

    }
}

우선 위 코드에서 마음에 들지 않는 점은 각각의 상수에 부여된 1,2,3이라는 리터럴은 단순히 상수들을 구분하고 이용하기 위해 부여된 것이지 논리적으로 아무런 의미가 없다. 다시 말해 APPLE은 정수 1과 아무런 관련도 없고 굳이 1이어야 할 이유도 업다.

두번째 문제는 이름의 충돌이 발생할 수 있다는 것이다. 만약 이 프로그램이 커져서, IT 회사의 정보가 추가되었고 회사 이름을 상수로 관리하려 한다 해보자.

public class EnumDemo {
    public static final int APPLE = 1;
    public static final int PEACH = 2;
    public static final int BANANA = 3;

    // 생략 

    public static final int APPLE = 1;
    public static final int GOOGLE = 2;
    public static final int FACEBOOK = 3;

}

과일 ‘사과’와 회사 ‘애플’은 이름은 같지만 서로 다른 의미를 가진다. 하지만 위의 예시처럼 사용하려면 이름이 중복되기 때문에 컴파일 에러가 발생한다.

이름의 중복은 아래처럼 이름을 다르게 해주거나

public class Test {
    public static final int FRUIT_APPLE = 1; // 과일 
    public static final int FRUIT_PEACH = 2;
    public static final int FRUIT_BANANA = 3;

    // 생략 

    public static final int COMPANY_APPLE = 1; // 회사 
    public static final int COMPANY_GOOGLE = 2;
    public static final int COMPANY_FACEBOOK = 3;

}

인터페이스로 만들면 구분이 가능해진다.
하지만, 이런식으로 상수를 인터페이스로 관리하는 것은 안티패턴이다. 인터페이스는 규약을 정하기 위해 만든 것이지, 이런 식으로 상수로 관리하라고 만든 개념이 아니기 때문이다.

interface Fruit {
    int APPLE = 1, PEACH = 2, BANANA = 3;
}

interface Company {
    int APPLE = 1, GOOGLE = 2, FACEBOOK = 3;
}

하지만 여전히 문제가 남아있다. fruit와 company 모두 int 타입의 자료형이기 때문에 아래와 같은 코드가 가능하다.

if (Fruit.APPLE == Company.APPLE) { // Type Safety 하지 않다!  
   // ......
}

하지만 ‘과일’과 ‘회사’는 서로 비교조차 되어서는 안되는 개념이다. 따라서 위와 같은 코드는 애초에 작성할 수 없게 컴파일 과정에서 막아줘야 한다.

둘이 애초에 비교를 하지 못하도록 하려면 서로 다른 객체로 만들어주면 된다.

class Fruit {
    public static final Fruit APPLE = new Fruit();
    public static final Fruit PEACH = new Fruit();
    public static final Fruit BANANA = new Fruit();
}

class Company {
    public static final Company APPLE = new Company();
    public static final Company GOOGLE = new Company();
    public static final Company FACEBOOK = new Company();
}

이렇게 하면 위에서 언급했던 문제들이 모두 해결된다.

  1. 상수와 리터럴이 논리적인 연관이 없다.
  2. 서로 다른 개념끼리 이름이 충돌할 수 있다.
  3. 서로 다른 개념임에도 비교하는 코드가 가능하다.

위의 문제가 모두 해결되지만 다른 문제가 발생한다.

사용자 정의 타입은 switch문의 조건에 들어갈 수 없다.

switch문의 조건으로 들어갈 수 있는 데이터 타입은 byte, short, char, int, enum, String, Byte, Short, Character, Integer 이다.

public class EnumDemo {

    public static void main(String[] args) {
        Fruit type = Fruit.APPLE;
        switch (type) {   // 컴파일 에러
            case Fruit.APPLE:
                System.out.println("32 kcal");
                break;
            case Fruit.PEACH:
                System.out.println("52 kcal");
                break;
            case Fruit.BANANA:
                System.out.println("16 kcal");
                break;
        }

    }
}

Enum은 위처럼 상수를 클래스로 정의해서 관리할 때 얻을 수 있는 이점들을 모두 취하면서 상수들을 더욱 간단히 선언할 수 있도록 하기 위해 만들어졌다.


자바 Enum의 특징

자바 enum의 여러가지 특징에 대해 알아보자.

1. enum에 정의된 상수들은 해당 enum type의 객체이다.

C 등의 다른 언어에도 열거형이 존재한다. 하지만 다른 언어들과 달리 자바의 enum은 단순한 정수 값이 아닌 해당 enum type의 객체이다.

앞서 살펴본 것처럼,

enum Fruit { APPLE, PEACH, BANANA }   

만일 이런 열거형이 정의되어 있을 때, 이를 클래스로 정의한다면 다음처럼 표현할 수 있다.

class Fruit {
    public static final Fruit APPLE = new Fruit("APPLE");
    public static final Fruit PEACH = new Fruit("PEACH");
    public static final Fruit BANANA = new Fruit("BANANA");
    
    private String name;
    
    private Fruit(String name) {
        this.name = name;
    }
}

물론 실제 enum의 구현과는 다르지만, 이런 형태라고 생각하면 enum을 학습하는데 있어서 훨씬 이해가 쉽다.

2. 생성자와 메서드를 추가할 수 있다.

자바에서 enum은 엄연한 클래스이다.
enum을 정의하는 법은 클래스를 정의하는 법과 거의 비슷한데 몇가지 차이가 있다.
우선 class 대신 enum이라고 적는다.
enum 안에는 열거할 상수의 이름을 선언한다.
이름은 대문자로 선언하는 것이 관례이며 각 상수는 콤마로 구분한다.
제일 마지막 상수의 끝에는 세미콜론을 붙여야 한다. 앞부분에서 살펴본 것처럼, 간단히 상수의 이름만 선언할 때는 세미콜론을 붙이지 않아도 된다.

enum Fruit {
    APPLE, PEACH, BANANA;    // 열거할 상수의 이름 선언, 마지막에 ; 을 꼭 붙여야한다.

    Fruit() {
        System.out.println("생성자 호출 " + this.name());
    }
}

public class Test {

    public static void main(String[] args) {
        Fruit apple = Fruit.APPLE;
    // Fruit grape = new Fruit();
    // 에러 발생. 열거형의 생성자의 접근제어자는 항상 private이다.
    }
}
// Fruit apple = Fruit.APPLE; 코드가 실행되면 아래처럼 
// 열거된 모든 상수의 생성자가 호출된다.
생성자 호출 : APPLE
생성자 호출 : PEACH
생성자 호출 : BANANA

생성자를 정의할 수 있는데, enum의 생성자의 접근제어자는 private이기 때문에 외부에서 상수를 추가할 수 없다.

왜 enum의 생성자를 private 으로 강제 했을까?

생각해보면 enum은 상수 목록이다. 상수라는건 변수와 달리 다른 값이 할당 될 수 없고 그런 시도도 하면 안된다. 그렇기 때문에 생성자를 통해서 상수에 다른 값을 할당하려는 생각조차 하지 못하도록(동적으로 할당할 수 없도록) 강제하는 것 같다.

열거형의 멤버 중 하나를 호출하면, 열거된 모든 상수의 객체가 생성된다. 위 예시를 보면 APPLE 하나를 호출했는데 열거된 모든 상수의 생성자가 호출되었음을 확인할 수 있다. 상수 하나당 각각의 인스턴스가 만들어지며 모두 public static final 이다.

또한, 생성자를 이용해서 상수에 데이터를 추가할 수 있다.

public enum Transport {
    BUS(1200),
    TAXI(3000),
    SUBWAY(1300);

    private int basicFare;

    public int value() {
        return basicFare;
    }

    Transport(int basicFare) {
        this.basicFare = basicFare;
    }
}

public class Test{

    public static void main(String[] args) {

        Transport bus = Transport.BUS;
        System.out.println(Transport.BUS.value()); // 출력 : 1200
    }
}

다음과 같이 switch문을 이용 해서 각 상수별로 다른 로직을 실행하는 메서드를 정의할 수도 있다.

enum Transport {
    BUS(1200), TAXI(3900), SUBWAY(1200);

    private final int BASIC_FARE;   // 기본요금

    Transport(int basicFare) {
        BASIC_FARE = basicFare;
    }

    public double fare() {     // 운송 수단별로 다르게 책정되는 요금
        switch (this) {
            case BUS -> {
                return BASIC_FARE * 1.5;
            }
            case TAXI -> {
                return BASIC_FARE * 2.0;
            }
            case SUBWAY -> {
                return BASIC_FARE * 0.5;
            }
            default -> throw new IllegalArgumentException(); // 실행될 일 없는 코드이지만 없으면 컴파일 에러
        }
    }

}

이렇게 추상 메서드를 선언해서 각 상수 별로 다르게 동작하는 코드를 구현할 수도 있다.

enum Transport {
    BUS(1200) {
        @Override
        double fare(int distance) {
            return distance * BASIC_FARE * 1.5;
        }
    },

    TAXI(3900) {
        @Override
        double fare(int distance) {
            return distance * BASIC_FARE * 2.0;
        }
    },

    SUBWAY(1200) {
        @Override
        double fare(int distance) {
            return distance * BASIC_FARE * 0.5;
        }
    };

    protected final int BASIC_FARE;  // 기본요금, protected로 선언해야 상수에서 접근 가능

    Transport(int basicFare) {
        BASIC_FARE = basicFare;
    }

    abstract double fare(int distance);   // 거리에 따른 요금 계산
}

3. 상수 간의 비교가 가능하다.

enum 상수 간의 비교에는 == 를 사용할 수 있다. 단 >, < 같은 비교 연산자는 사용할 수 없고 comparreTo()를 사용할 수 있다.


3. Enum이 제공하는 메소드

Enum이 제공하는 여러가지 메소드를 알아보자.

name()

열거 객체의 문자열을 리턴한다.

Season season = Season.AUTUMN;
System.out.println(season.name()); // 출력 : AUTUMN

ordinal()

열거 객체가 몇번째인지를 리턴한다.

public enum Season {
    SPRING,  // 0
    SUMMER,  // 1
    AUTUMN,  // 2
    WINTER   // 3
}


Season season = Season.AUTUMN;
System.out.println(season.ordinal()); // 출력 : 2

자바 API문서에서는 enum의 ordinal 메서드에 대해 다음과 같이 말한다.

Most programmers will have no use for this method. It is designed for use by sophisticated enum-based data structures, such as EnumSet and EnumMap.

ordinal은 enum 내부에서 사용하기 위해 만든것이지, 프로그래머가 이 메서드에 의존하는 코드를 작성하는 것은 안티패턴이다.

위의 해결책은 간단하다. ordinal 메서드를 사용하지 말고, 인스턴스 필드에 저장하자.

public enum Season {
    SPRING(0),
    SUMMER(1),
    AUTUMN(2),
    WINTER(3);

    private final int numberOfSeason;

    Season(int number) {
        this.numberOfSeason = number;
    }

    public int getNumberOfSeason() {
        return numberOfSeason;
    }
}

compareTo()

매개값으로 주어진 열거 객체를 기준으로 전 후로 몇번째 위치하는지 비교

Season season1 = Season.AUTUMN; // 2
Season season2 = Season.SPRING; // 0
System.out.println(season1.compareTo(season2)); // 출력 : 2
System.out.println(season2.compareTo(season1)); // 출력 : -2

valueOf()

매개값으로 주어지는 문자열과 동일한 문자열을 가지는 열거 객체를 리턴한다.
외부로부터 문자열을 받아 열거 객체를 반환할 때 유용하다.

Season season = Season.valueOf("SPRING");

values()

열거 타입의 모든 열거 객체들을 배열로 만들어 리턴

Season[] seasons = Season.values();
    for(Season season : seasons) {
         System.out.println(season);
     }

4. java.lang.Enum

enum 클래스는 java.lang.Enum 클래스를 상속 받도록 되어 있다.
그렇기 때문에 다중 상속을 지원하지 않는 java에서 enum 클래스는 별도의 상속을 받을 수 없다.

뿐만 아니라 api문서를 보면 enum 클래스의 생성자에 대해 Sole constructor 라고 설명이 작성 되어 있다. 이게 무슨 말인지 잘 몰라서 찾아보니, It is for use by code emitted by the compiler in response to enum type declarations. 라는 뜻인것 같다.
즉, 컴파일러가 사용하는 코드이기 때문에 사용자가 호출해서 사용할 수 없다.


5. EnumSet

EnumSet 클래스는 java.util 패키지에 정의 되어 있는 클래스이다.
이름에서 유추할 수 있듯 Set 인터페이스를 구현하고 있으며, 일반적으로 알고 있는 Set 자료구조의 특징을 가지고 있다.

즉, EnumSet은 enum 타입에 사용하기 위한 특수한 Set 구현이다.
또한, EnumSet은 내부적으로 bit vector로 표현된다. 따라서 매우 효율적이다.

다음과 같이 사용하면 된다.

public enum Transport {
    BUS ,
    SUBWAY,
    TAXI
}

public class Test{

    public static void main(String[] args) {

        EnumSet<Transport> transports = EnumSet.of(Transport.BUS);
        System.out.println(transports);

        EnumSet<Transport> transports2 = EnumSet.of(Transport.BUS, Transport.TAXI);
        System.out.println(transports2);

        EnumSet<Transport> all = EnumSet.allOf(Transport.class);
        System.out.println(all);

        EnumSet<Transport> none = EnumSet.noneOf(Transport.class);
        System.out.println(none);

        EnumSet<Transport> range = EnumSet.range(Transport.BUS, Transport.SUBWAY);
        System.out.println(range);
    }
}

Output

[BUS]
[BUS, TAXI]
[BUS, SUBWAY, TAXI]
[]
[BUS, SUBWAY]

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

Set<Transport> s = Collections.synchronizedSet(EnumSet.noneOf(Transport.class));    

또한, HashMap 대신 EnumMap을 사용할 수도 있으니 참고하자.


마지막으로 enum을 실무에서 어떻게 활용될 수 있는지는 아래의 글을 참조하면 좋을 것 같다.
우아한형제들 기술 블로그(Java Enum 활용기)


Reference

https://blog.naver.com/hsm622/222218251749
https://wisdom-and-record.tistory.com/52
https://github.com/whiteship/live-study/issues/11