[Java] 자바의 열거형 Enum

오늘은 회사에서 코딩을 하던중 if else가 남발하는 코드를 보게 되었다. if else가 남발하다 보면 가독성도 떨어지고 코드가 굉장히 지저분해진다.

이 부분을 어떻게 해결해야 할지 고민하며 찾아보다가 해결방법중 하나인 자바의 열거타입 enum에 대해서 알게되었다.

공부한 것을 한번 정리해보려고 한다.



자바의 열거형(Enum)


열거 타입이란?

열거 타입은 서로 연관된 상수의 집합을 저장하는 자료형이다.

데이터 중에는 몇 가지로 한정된 값만을 갖는 경우가 있다. 월(JAN, FEB, MAR…)라던지 요일(월화수…)같은 데이터는 12개, 7개의 값만을 갖는다.

이와 같이 한정된 값만을 갖는 데이터 타입이 열거 타입(enumeration type)이다.



enum 사용

열거 타입은 아래와 같이 정의할 수 있다.

public enum Week{
    
    //열거 상수
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
}

위 열거타입을 사용하기 위해선 변수를 선언하고 사용해야 한다.

열거타입도 하나의 데이터 타입이기 때문에 변수를 선언하고 사용하는게 가능하다.



열거타입 변수;

Week today;

Week today = Week.SUNDAY; // 열거 타입 변수 선언 시 열거 상수 저장 가능

Week birthday = null; // 열거 타입 변수도 참조타입이기에 null 값 저장 가능

열거 상수도 객체이기에 위에서 정의한 열거 타입 Week의 경우 MONDAY~SUNDAY 7개의 객체로 생성된다.

메소드 영역에 생성된 열거 상수가 해당 Week 객체를 각각 참조한다.



다음 코드에서 today는 스택 영역에 생성된다. today에 저장되는 값은 Week.SUNDAY 열거 상수가 참조하는 객체의 번지이다.

따라서 Week.SUNDAY, today는 같은 Week 객체를 참조한다.

열거타입 변수;

Week today = Week.SUNDAY;

Week today = Week.SUNDAY; // true
Enum의 생성자

enum은 사실 클래스다. 때문에 생성자를 가질 수 있다. 아래와 같이 Fruit 타입을 초기화하여 생성자를 호출해보자.

enum Fruit{
    APPLE, PEACH, BANANA;
    
    Fruit(){
        System.out.println("Call Constructor " +this);
    }
}

class Main {
    public static void main(String[] args) {
        Fruit type = Fruit.APPLE;
    }
}

결과는 다음과 같다.

Call Constructor APPLE
Call Constructor PEACH
Call Constructor BANANA

생성자가 세 번 호출되는 것을 볼 수 있다.

즉, 열거타입에 정의된 열거 상수의 개수 만큼 생성자가 호출된다.

추가적으로 열거타입의 생성자는 private만 허용하기 때문에 생성자 앞에 public을 붙이면 컴파일 오류가 발생한다.

enum도 필드의 인스턴스 변수를 가질 수 있다. 때문에 생성자의 매개변수를 통해서 이 값을 초기화할 수도 있다.

enum Fruit{
    APPLE("red"), PEACH("pink"), BANANA("yellow");
    
    String color;
    
    Fruit(String color){
        this.color = color;
    }
}



Enum의 메소드

열거형은 메소드를 가질 수도 있다. 메소드를 통해 위에서 정의한 Fruit 열거형의 color 인스턴스 변수를 출력해보았다.

enum Fruit{
    APPLE("red"), PEACH("pink"), BANANA("yellow");
    
    String color;
    
    Fruit(String color){
        this.color = color;
    }
    
    void getColor(Fruit fruit){
        System.out.println(fruit.name()+":"+fruit.color);
    }
}

class Main {
    public static void main(String[] args) 	{
        for(Fruit type: Fruit.values()){
            type.getColor(type);
        }
    }
}

결과는 아래와 같이 나왔다.

APPLE:red
PEACH:pink
BANANA:yellow



그렇다면 Enum이 제공하는 메서드에 대해서도 살펴보자.

  • valueOf()

매개값으로 주어지는 문자열과 동일한 문자열을 가지는 열거 객체를 리턴한다.

이 메소드는 외부로부터 문자열을 입력받아 열거 객체로 변환할 때 유용하게 사용할 수 있다.

Week weekDay = Week.valueOf("SATURDAY");
System.out.println(weekDay); // 코드 결과 SATURDAY

= values()

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

다음은 Week 열거 타입의 모든 열거 객체를 배열로 만들어 반복문으로 반복하는 코드다.

Week[] days = Week.values();

for(Week day : days){
    System.out.println(day); 
}

코드 결과는 아래와 같다.

MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY

이때 배열의 인덱스는 열거 객체의 순번과 같고 각 인덱스 값은 해당 순번의 열거 객체 번지이다.

-name()

열거 객체가 가지고 있는 문자열을 리턴하는 메소드다.

리턴되는 문자열은 열거 타입을 정의할 때 사용한 상수 이름과 동일하다. 아래 name 변수에는 열거 객체 내부의 문자열인 “SUNDAY”가 저장된다.

Week today = Week.SUNDAY;
String name= today.name();

-ordinal()

public enum Week{
    MONDAY,         // 0번
    TUESDAY,     // 1번
    WEDNESDAY, // 2번
    THURSDAY,     // 3번
    FRIDAY,   // 4번
    SATURDAY,   // 5번
    SUNDAY   // 6번
}

아래 코드는 ordinal 변수에 숫자 6을 저장한다.

Week today = Week.SUNDAY;
int ordinal = today.ordinal();
  • compareTo()

매개값으로 주어진 열거 객체를 기준으로 전후로 몇 번째 위치하는 지 비교한다. 만약 열거 객체가 매개값의 열거 객체보다 순번이 빠르다면 음수, 늦다면 양수가 리턴된다.

아래 예제는 result1에 -2, result2에 2가 저장된다.

Week day1 = Week.MONDAY;
Week day2 = Week.WEDNESDAY;
int result1 = day1.compareTo(day2);
int result2 = day2.compareTo(day1);

단, ordinal 메서드를 사용하는 행위는 안티 패턴이 될 가능성이 많다. 사용을 자제하자. (Effective Java)



EnumSet

EnumSet은 enum 클래스로 작동하기 위해 특화된 Set 컬렉션(데이터를 중복 저장할 수 없고, 순서가 보장되지 않는 자료구조)이라고 할 수 있다.

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

이러한 EnumSet의 내부는 비트 벡터로 구현되었다.

EnumSet을 사용할 때는 몇 가지 고려할 사항이 있다.

-EnumSet 고려사항

  1. 열거형 값만 포함 할 수 있으며, 모든 값은 동일한 열거형이어야 한다.

  2. null을 추가할 수 없다.

  3. 스레드에 안전하지 않으므로, 필요할 경우 외부에서 동기화한다.

  4. 복사본에 fail-safe iterator(장애 발생시 작업을 중단하지 않음) 를 사용하여 컬렉션을 순회할 때, 컬렉션이 수정되어도 ConcurrentModificationException이 발생하지 않는다.



-EnumSet 특징

  1. 열거형 타입으로 지정해놓은 요소들을 가장 쉽고 빠르게 다룰 수 있게 한다.

  2. 요소의 개수가 2^6을 넘지 않을 경우 long 데이터형의 비트필드를 사용한다

  3. 겉으로는 Set을 기반으로 하지만 내부적으로 비트필드를 사용하기 때문에, 메모리 공간도 적게 차지하고 속도도 빠르다.

  4. enum과 static 메서드로 구성되어 있어 안정성을 추구하면서 편리한 사용이 가능하다.

그렇다면 Enumset을 다루는 예제를 보도록 하자.

public class EnumSetTest {

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

    public static void main(String[] args) {
        // 1. allOf() : 열거형 Day의 모든 요소를 반환
        EnumSet es = EnumSet.allOf(Day.class);
        EnumSet es2 = EnumSet.copyOf(es); // es를 복사
        System.out.println("EnumSet Day: "+ es2);

        // 2. noneOf() : 열거형 Day를 비워라
        es2 = EnumSet.noneOf(Day.class);
        System.out.println("Day를 비워라 => "+ es2);

        // 3. of() : 지정한 요소를 반환
        es = EnumSet.of(Day.FRIDAY, Day.WEDNESDAY);
        System.out.println("금요일과 수요일만 => "+ es);

        // 4. comeplementOf() : 지정된 요소를 제외한 나머지를 반환
        es2 = EnumSet.complementOf(es);
        System.out.println("금요일과 수요일 제외한 나머지 => "+ es2);

        // 5. range() : 지정된 구간 내의 요소만 반환
        es2 = EnumSet.range(Day.TUESDAY, Day.FRIDAY);
        System.out.println("화요일부터 금요일까지 => "+ es2);
    }
}

결과는 아래와 같다

EnumSet Day: [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY] Day를 비워라 => [] 금요일과 수요일만 => [WEDNESDAY, FRIDAY] 금요일과 수요일 제외한 나머지 => [SUNDAY, MONDAY, TUESDAY, THURSDAY, SATURDAY] 화요일부터 금요일까지 => [TUESDAY, WEDNESDAY, THURSDAY, FRIDAY]



그렇다면 내가 if else문으로 이루어진 복잡한 코드를 어떤식으로 enum을 사용해서 해결했는지 보도록 하겠다.

먼저 기존 코드이다.

if (strMaker.toString().equals("HME") || strMaker.toString().equals("KME") || strMaker.toString().equals("GME") || strMaker.toString().equals("HCHN")
                    || strMaker.toString().equals("GMC") || strMaker.toString().equals("HBHMC") || strMaker.toString().equals("KDYK") || strMaker.toString().equals("HMU")
                    || strMaker.toString().equals("KMU") || strMaker.toString().equals("HMA") || strMaker.toString().equals("HAC") || strMaker.toString().equals("KCI")
                    || strMaker.toString().equals("GMA") || strMaker.toString().equals("GAC") || strMaker.toString().equals("KMA")
                    || strMaker.toString().equals("KMC") || strMaker.toString().equals("HMC")) {

                /// 작업코드 ...
            }

기존에 회사에 구현되어 있던 코드인데 딱봐도 코드가 깔끔해 보이지가 않는다.

나는 이부분을 해결하기 위해 먼저 Enum 클래스를 만들어주었다.

  public enum AutoDownloadMaker {
        HME, KME, GME, HCHN, GMC, HBHMC, KDYK, HMU, KMU, HMA, HAC, KCI, GMA, GAC, KMA
    }

그리고 EnumSet을 사용하여 기존 데이터와 Enum 클래스의 상수들을 비교할 수 있도록 코드를 개선하였다.

  EnumSet<AutoDownloadMaker> autoDownloadMakers = EnumSet.allOf(AutoDownloadMaker.class);
  boolean isContain = autoDownloadMakers.toString().contains(strMaker.toString());
  
  if(isContain) { // AutoDownload 대상 지역에 포함 시 AutoDownload 수행
                /// 작업코드 ...
            }

위와 같이 Enumset 설정을 한 후에 isContain값의 결과에 따라 코드를 실행할 수 있도록 하였다.

딱 봐도 기존 코드에 비해 엄청 개선이 된 것을 볼 수 있다.



오늘 Enum에 대해서 알아보았는데 아직 부족한 부분도 많고 Enum 활용법도 상상 이상으로 광대한 것 같다.

추가적으로 계속 공부해나갈 필요를 느꼈다.

그리고 앞으로도 if else문의 과도한 사용을 없애기 위해 어떠한 방법이 있는지 계속 알아보고 공부해야겠다.

Categories:

Updated: