[JAVA] Enum 클래스에 대한 이해

kindof

·

2021. 6. 20. 17:32

0. 문제 제기

자바 1.5 버전 이전에는 상수를 선언할 때는 해당 클래스 내부에 final static Strirng, final static int와 같은 방식을 사용했습니다.

 

하지만 이런 방식의 상수 정의는 상수의 개수가 많아질수록 가독성이 떨어지고, 어떤 것에 관한 상수인지 파악하기도 어려워지는 단점이 있었습니다.

 

따라서 이런 상수들을 용도에 맞게 모아서 사용하기 위한 클래스나 인터페이스를 정의하기도 했는데 이러한 방식 역시 몇 가지 문제점을 안겨주고 있었습니다.

 

예시를 한 번 보겠습니다.

interface UNIVERSITY{
    int SEOUL = 1;
    int YONSEI = 2;
    int KOREA = 3;
}

interface MAJOR{
    int KOREAN = 1;
    int MATH = 2;
    int ENGLISH = 3;
    int SCIENCE = 4;
}


public class EnumExample {
    public static void main(String[] args) {

        if(UNIVERSITY.SEOUL == MAJOR.KOREAN){
            System.out.println("두 상수는 같습니다");
        }

        int major = MAJOR.MATH;

        switch (major){
            case MAJOR.KOREAN:
                System.out.println("국어 전공");
                break;
            case MAJOR.MATH:
                System.out.println("수학 전공");
                break;
            case MAJOR.ENGLISH:
                System.out.println("영어 전공");
                break;
            case MAJOR.SCIENCE:
                System.out.println("과학 전공");
                break;
        }
    }
}

위처럼 인터페이스 내에 상수를 표현하면 각 인터페이스에서 상수가 무엇을 의미하는지 알기 쉬워집니다.

 

하지만 이렇게 서로 다른 인터페이스에 상수를 선언해두면 본문의 switch문에서 상수를 비교할 수가 없죠.

 

그럼에도 불구하고 코드로만 보면 UNIVERSITY.SEOUL, MAJOR.KOREAN 모두 1이란 값을 가지기 때문에 두 값이 같아보인다는 겁니다. 서울대학교를 의미하는 값 1과 국어 전공을 의미하는 값 1이 같다는 게 좀 이상하지 않나요?

 

이러한 해석의 문제는 컴파일러가 컴파일 시점에 잡아낼 수 있는 문제가 아니기 때문에 런타임에 개발자가 원하는대로 코드가 안 돌아가는 상황을 초래합니다. 

 

어떻게 하면 이 문제를 해결할 수 있을까요?

class UNIVERSITY{
    public static final UNIVERSITY SEOUL = new UNIVERSITY();
    public static final UNIVERSITY YONSEI = new UNIVERSITY();
    public static final UNIVERSITY KOREA = new UNIVERSITY();
}

class MAJOR{
    public static final MAJOR KOREAN = new MAJOR();
    public static final MAJOR MATH = new MAJOR();
    public static final MAJOR ENGLISH = new MAJOR();
    public static final MAJOR SCIENCE = new MAJOR();
}


public class EnumExample {
    public static void main(String[] args) {

        if(UNIVERSITY.SEOUL == MAJOR.KOREAN){
            System.out.println("두 상수는 같습니다");
        }

        MAJOR major = MAJOR.MATH;

        switch (major){
            case MAJOR.KOREAN:
                System.out.println("국어 전공");
                break;
            case MAJOR.MATH:
                System.out.println("수학 전공");
                break;
            case MAJOR.ENGLISH:
                System.out.println("영어 전공");
                break;
            case MAJOR.SCIENCE:
                System.out.println("과학 전공");
                break;
        }
    }
}

서로 다른 집합에 속하는 상수를 비교하는 것은 런타임에 의도하지 않은 결과를 발생시키기 때문에 인터페이스 대신에 클래스를 선언하고, 자기 자신을 객체로 만들어 그 값을 할당하도록 했습니다.

 

하지만 이렇게 리팩토링을 한다해도, 서로 다른 타입 간 비교를 하는 것이 불가능한 문제도 생기고 객체는 애초에 switch문의 인자로 받아들여지지 못하는 문제가 생기게 되죠. 이를 또 해결하기 위해 switch문을 일일이 쪼개서 나누면 이제 배보다 배꼽이 더 커지는 상황이 됩니다.

 

따라서 자바는 버전 1.5 이후부터 Enum이라는 개념을 도입했습니다. 이제 Enum에 대해 알아보겠습니다.


1. Enum의 기본 개념

Enum은 Enumeration의 줄임말로 열거형이라고도 하며, 서로 연관된 상수들의 집합을 의미하는데요. final static String, int와 같이 문자열이나 숫자들을 나타내는 기본자료형의 값을 Enum으로 대체해서 사용할 수 있게 되었습니다.


위에서 인터페이스나 클래스로 서로 연관된 상수들을 모아뒀는데, 이제 그렇게 할 필요가 없고 단순히 enum 타입으로 선언해주면 됩니다. 또한 위 예시에 나온 것처럼 인터페이스나 클래스로 상수를 정의하는 것을 보완해서 타입 안정성을 갖출 수 있게 되었죠.

 


2. Enum 정의하는 방법

1) 기본적인 enum 선언

enum으로 지정된 상수들은 모두 대문자로 선언하며, 선언된 순서에 따라 0부터 Index 값을 갖게 되고, 순차적으로 증가합니다.

 

또한 열거형 변수들을 모두 선언한 후, 세미콜론(;)을 찍으면 안 됩니다. 단, 멤버 변수 등을 추가할 때는 찍어야 합니다. 세미콜론을 찍으면 IDE가 빨간줄을 그을 겁니다.

 

예제를 볼까요?

// 1) 별도의 .java 선언하기
// DevType.java
public enum DevType{
    MOBILE, WEB, SERVER
}

// Developer.java
public Class Developer{
    public String name;
    public int career;
    public DevType type;
}
-------------------------------
// 2) Class 내부에서 선언
public class Developer{
    public String name;
    public int career;
    public enum DevType{
        MOBILE, WEB, SERVER
    }
}
------------------------------
// 3) Class 외부에서 선언
public class Developer{
    public String name;
    public int career;
    public DevType type;
}

enum DevType{
    MOBILE, WEB, SERVER
}

 

2) enum에 멤버 추가해서 선언

Enum에 멤버 변수를 추가하는 것은 enum 상수와 연관된 데이터를 상수 자체에 포함시켜 관리를 용이하게 하기 위함입니다.

 

인스턴스 필드를 추가하려면 enum 상수의 이름 옆에 원하는 값을 괄호와 함께 적어주면 되는데요.

 

말로 설명하는 것보다 코드로 확인해보겠습니다.

package oop;


enum portalSite{
    NAVER("https://www.naver.com"),
    DAUM("https://www.daum.net"),
    GOOGLE("https://www.google.com");

    private final String url;       // 인스턴스 필드 추가

    // 생성자 추가
    portalSite(String url) {this.url = url;}

    // 인스턴스 필드 get 메서드 추가
    public String getUrl() {return url;}
}


public class EnumDemo {
    String portalUrl;

    public EnumDemo(portalSite p){
        this.portalUrl = p.getUrl();
    }

    public static void main(String[] args) {
        EnumDemo e = new EnumDemo(portalSite.NAVER);
        System.out.println("portalUrl = " + e.portalUrl);
    }
}

위에서 포탈 사이트의 URL을 상수로 저장하는 portalSite라는 Enum 객체를 선언했습니다.

 

그리고 EnumDemo라는 클래스는 portalUrl을 멤버 변수로 가지고 있는데, 생성자의 매개변수로 portalSite를 받아 해당 Url을 입력받죠.

 

따라서 이 상황에서 메인 함수의 결과값은 아래와 같습니다.


3. enum이 제공하는 메소드

- values() : enum type의 모든 열거객체들을 배열로 만들어 리턴해줍니다

enum DevType{
    MOBILE, WEB, SERVER
}

public class ~~{
    public PSVM(String[] args){
        for(DevType t : DevType.values()){
            System.out.println(t);          // MOBILE\n WEB\n SERVER\n
        }
    }
}

 

- valueOf() : 매개값으로 주어지는 문자열과 동일한 문자열을 가지는 열거 객체를 리턴해줍니다.

enum DevType{
    MOBILE, WEB, SERVER
}

public class ~~{
    public PSVM(String[] args){
        DevType t = DevType.valueOf("MOBILE");
        System.out.println(t);      // MOBILE
    }
}

 

- compareTo() : 매개값으로 주어진 열거 객체를 기준으로 전후에 몇번째 위치하는지 비교합니다.

enum DevType{
    MOBILE, WEB, SERVER
}

public class ~~{
    public PSVM(String[] args){
        DevType t1 = DevType.MOBILE;
        DevType t2 = DevType.SERVER;

        int result = t1.compareTo(t2);
        System.out.println(result);      // -2
    }
}

 

- ordinal() : 해당 상수가 enum 클래스에서 몇 번째 인덱스에 있는지 리턴해주는데, 이 메서드는 되도록 쓰면 안 됩니다.

 

그 이유는 만약 어떤 코드 중에 ordinal() 메서드를 이용해서 작업을 하는 부분이 있는데, 우리가 enum 부분을 애초에 손보게 된다면?? ordinal()을 이용하고 있는 모든 코드들을 다시 뜯어고쳐야 하기 때문에 유지보수 관점에서 안 좋기 때문입니다.


4. java.lang.Enum

모든 enum들은 내부적으로 java.lang.enum 클래스를 상속합니다. 자바는 다중 상속을 지원하지 않기 때문에 enum은 다른 클래스를 상속받을 수 없죠.

 

참고로 toString() 메서드는 상수의 이름을 리턴하도록 구현되어 있습니다.

 

기존에 상속받고 있는 클래스가 존재하기 때문에 다중 상속은 지원하지 않지만 다양한 인터페이스들은 구현할 수 있습니다.

 


 

5. enum 예제

enum을 통해 관련되어 있지만 관련성을 표시하기 힘든 형태의 데이터를 한 곳에서 관리할 수 있습니다. 예를 들어 경기에서 승리한 사람과 패배한 사람을 리스트로 관리한다고 생각해보겠습니다.

 

이 경우 리스트의 변수명을 통해 관리하거나 Player라는 클래스를 상속받는 형태로 관리하는 등의 방법이 있다. 하지만 enum을 사용하면 보다 명확한 방법으로 이들의 관계를 표시할 수 있습니다.

 

public enum Winner{
    WINNER("승리", Arrays.asList("kyle", "pobi", "hello", "world")), LOSER("패배", Arrays.asList("hodol","dunbbog","ruga");

    private final String winner;
    private final List<String> list;

    Winner(String winner, List<String> list){
        this.winner = winner;
        this.list = list;
    }
}

단순히 관련있는 데이터를 모아서 관리할 뿐만 아니라 자바에서 enum은 완전한 클래스의 형태를 보이기 때문에 관련 로직을 같은 enum 클래스 내에서 관리할 수 있습니다.

 

아래 예를 보면 승리자가 누구인지, 승리자가 몇 명인지 등 승리와 관련된 로직을 한 곳에서 관리할 수 있습니다.

public boolean isWinner(String name){
    return WINNER.list.contains(name);
}

public int getWinnerSize(){
    return WINNER.list.size();
}

 

비슷한 예로 구매한 로또와 당첨번호가 같은 갯수, 그리고 각 갯수에 대응하는 상금 등과 같이 관련된 데이터도 하나의 Enum으로 관리 할 수 있습니다.

 

로또의 당첨 개수와 당첨 금액을 관리하는 하나의 클래스로 클래스를 분리하지 않고도 두 데이터(일치한 갯수와 당첨 금액)의 연관성을 명확하게 보여줍니다.

 

뿐만 아니라 일치한 수를 입력하면 그 수와 일치하는 인스턴스를 반환하는 메소드 등 상태와 연관있는 행위 또한 한 곳에서 관리 할 수 있는 장점이 있습니다.

 

public enum Statistic {
        THREE(3, 5000),
        FOUR(4, 50_000),
        FIVE(5, 1_500_000),
        BONUS(5, 3_000_000),
        SIX(6, 2_000_000_000);

        private final int matchingNumbers;
        private final int prize;

        Statistic(int matchingNumbers, int prize) {
            this.matchingNumbers = matchingNumbers;
            this.prize = prize;
        }

        public static Statistic getRank(int numberOfMatch) {
            return Arrays.stream(values())
                .filter(statistic -> statistic.matchingNumbers == numberOfMatch)
                .findFirst()
                .orElseThrow(new IllegalArgumentException("일치하는 번호가 3미만입니다."))
        }

6. 나가면서

지금까지 자바 enum에 대해 공부해봤습니다.

 

enum은 자바 IDE의 적극적인 지원을 받아서 자동 완성 기능도 되고, 오타 수정 및 텍스트 리팩토링 등이 용이합니다.

 

또한 리팩토링 시 상수를 enum에서 관리하기 때문에 내용의 추가가 필요하더라도 enum 코드 외에 수정을 할 필요가 없습니다. 이 부분에서 ordinal()을 쓰면 안 되는 이유가 있기도 하죠. 순서가 바뀌어버리니까요.

 

마지막으로 다른 언어와 달리 java에서는 enum이 완전한 기능을 가진 클래스라서, 상수와 관련된 작업들을 여기에서 구현해놓을 수 있습니다.

 

감사합니다.