Post

[Java] Enum(열거형)

[Java] Enum(열거형)

개발을 하다 보면, 코드 속에서 반복되는 값들을 깔끔하게 관리하고 싶을 때가 많습니다. 이럴 때 유용하게 사용할 수 있는 도구가 바로 enum입니다. 이번 글에서는 enum이 무엇인지, 그리고 어떻게 활용할 수 있는지 알아보겠습니다.


Enum이란?


Enum은 서로 관련된 상수를 편리하게 선언하기 위한 것으로 여러 상수를 정의할 때 사용하면 유용합니다. 원래 자바는 C언어와 달리 열거형이라는 것이 존재하지 않았으나, JDK1.5부터 새로 추가되었습니다. 자바의 Enum은 C언어의 열거형보다 더 향상된 것으로 열거형이 가지는 값뿐만 아니라 타입도 관리하기 때문에 보다 논리적인 오류를 줄일 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Card {
    static final int CLOVER = 0;
    static final int HEART = 1;
    static final int DIAMOND = 2;
    static final int SPADE = 3;

    static final int TWO = 0;
    static final int THREE = 1;
    static final int FOUR = 2;

    final int kind;
    final int num;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Card {
    // 열거형 Kind를 정의
    enum Kind {
        CLOVER, HEART, DIAMOND, SPADE
    }

    // 열거형 Value를 정의
    enum Value {
        TWO, THREE, FOUR
    }

    final Kind kind; // 타입이 int가 아닌 Kind임에 유의.
    final Value value;
}

기존의 많은 언어들, 예를 들어 C언어에서는 타입이 달라도 값이 같으면 조건식결과가 참(true)이였으나, 자바의 열거형은 ‘타입에 안전한 열거형’ 이라서 실제 값이 같아도 타입이 다르면 컴파일 에러가 발생합니다. 이처럼 값뿐만 아니라 타입까지 체크하기 때문에 타입에 안전하다고 하는 것입니다.

1
2
if(Card.CLOVER == Card.TWO) // true지만 false이어야 의미상 맞음.
if(Card.kind.CLOVER == Card.Value.TWO) // 컴파일 에러. 값은 같지만 타입이 다름

그리고 더 중요한 것은 상수의 값이 바뀌면, 해당 상수를 참조하는 모든 소스를 다시 컴파일 해야한다는 것입니다. 하지만 열거형 상수를 사용하면, 기존의 소스를 다시 컴파일하지 않아도 됩니다.


정의와 사용


Enum을 정의하는 방법은 간단합니다. 아래와 같이 괄호{}안에 상수의 이름을 나열하기만 하면 됩니다.

1
2
3
4
5
enum 열거형이름 {
    상수명1,
    상수명2,
    ...
}

예를 들어 동서남북 4방향을 상수로 정의하는 열거형 Direction은 다음과 같습니다.

1
2
3
4
5
6
enum Direction {
    EAST,
    SOUTH,
    WEST,
    NORTH
}

이 열거형에 정의된 상수를 사용하는 방법은 ‘열거형이름.상수명’ 입니다. 클래스의 static변수를 참조하는 것과 동일합니다.

1
2
3
4
5
6
7
8
class Unit {
    int x, y;       // 유닛의 위치
    Direction dir;  // 열거형을 인스턴스 변수로 선언

    void init() {
        dir = Direction.EAST; // 유닛의 방향을 EAST로 초기화
    }
}

열거형 상수간의 비교에는 ‘==’를 사용할 수 있습니다. equals()가 아닌 ‘==’로 비교가 가능하다는 것은 그만큼 빠른 성능을 제공한다는 것입니다. 그러나 ‘<’, ‘>’ 와 같은 비교연산자는 사용할 수 없고 compareTo()는 사용가능합니다.

1
2
3
4
5
6
7
if(dir == Direction.EAST) {
    x++;
} else if(dir > Direction.WEST) { // 에러. 열거형 상수에 비교연산자 사용불가
    ...
} else if(dir.compareTo(Direction.WEST) > 0) { // compareTo()는 가능
    ...
}

다음과 같이 switch문의 조건식에도 열거형을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
void move() {
    switch(dir) {
        case EAST: x++; // Direction.EAST라고 쓰면 안된다.
            break;
        case WEST: x--;
            break;
        case SOUTH: y++;
            break;
        case NORTH: y--;
            break;
    }
}

모든 열거형의 조상 - java.lang.Enum

열거형 Direction에 정의된 모든 상수를 출력하려면, 아래와 같습니다.

1
2
3
4
Direction[] dArr = Direction.values();

for(Direction d : dArr)  // for(Direction d : Direction.values())
    System.out.printf("%s=%d%n", d.name(), d.ordinal());

values()는 열거형의 모든 상수를 배열에 담아 반환합니다. 이 메서드는 모든 열거형이 가지고 있는 것으로 컴파일러가 자동으로 추가해 줍니다. 그리고 ordinal()은 모든 열거형의 조상인 java.lang.Enum클래스에 정의된 것으로, 열거형 상수가 정의된 순서(0부터 시작)를 정수로 반환합니다.

Enum클래스에는 그 밖에도 아래와 같은 메서드가 정의되어 있습니다.

메서드설명
Class<E> getDeclaringClass() 열거형의 Class객체를 반환한다.
String name() 열거형 상수의 이름을 문자열로 반환한다.
int ordinal() 열거형 상수가 정의된 순서를 반환한다.(0부터 시작)
T valueOf(Class<T> enumType, String name) 지정된 열거형에서 name과 일치하는 열거형 상수를 반환한다.

▲ Enum 클래스 메서드

이외에도 values()처럼 컴파일러가 자동적으로 추가해주는 메서드가 하나 더 있습니다.

1
2
static E values()
static E valueOf(String name)

이 메서드는 열거형 상수의 이름으로 문자열 상수에 대한 참조를 얻을 수 있게 해줍니다.

1
2
3
4
Direction d = Direction.valueOf("WEST");

System.out.println(d);  // WEST
System.out.println(Direction.WEST == Direction.valueOf("WEST"));    // true


멤버 추가


Enum클래스에 정의된 ordinal()이 열거형 상수가 정의된 순서를 반환하지만, 이 값을 열거형 상수의 값으로 사용하지 않는 것이 좋습니다. 이 값은 내부적인 용도로만 사용되기 위한것이기 때문입니다.

열거형 상수의 값이 불연속적인 경우에는 이때는 다음과 같이 열거형 상수의 이름 옆에 원하는 값을 괄호()와 함께 적어주면 됩니다.

1
2
3
4
5
6
enum Direction {
    EAST(1),
    SOUTH(5),
    WEST(-1),
    NORTH(10)
}

그리고 지정된 값을 저장할 수 있는 인스턴스 변수와 생성자를 새로 추가해 주어야 합니다. 이 때 주의할 점은, 먼저 열거형 상수를 모두 정의한 다음에 다른 멤버들을 추가해야한다는 것입니다. 그리고 열거형 상수의 마지막에 ‘;’도 잊지 말아야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Direction {
    EAST(1), SOUTH(5), WEST(-1), NORTH(10);     // 끝에 ';'를 추가해야 한다.

    private final int value;    // 정수를 저장할 필드(인스턴스 변수)를 추가

    // 생성자 추가
    Direction(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}

열거형의 인스턴스 변수는 반드시 final이어야 한다는 제약은 없지만, value는 열거형 상수의 값을 저장하기 위한 것이므로 final을 붙였습니다. 그리고 외부에서 이 값을 얻을 수 있게 getValue()도 추가하였습니다.

1
Direction d = new Direction(1); // 에러. 열거형의 생성자는 외부에서 호출불가

열거형 Direction에 새로운 생성자가 추가되었지만, 위와 같이 열거형의 객체를 생성할 수 없습니다. 열거형의 생성자는 제어자가 묵시적으로 private이기 때문입니다.

1
2
3
4
5
6
enum Direction {
    ...
    Direction(int value) {  // private Direction(int value)와 동일
        ...
    }
}

필요하다면, 다음과 같이 하나의 열거형 상수에 여러 값을 지정할 수도 있습니다. 다만 그에 맞게 인스턴스 변수와 생성자 등을 새로 추가해주어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Direction {
    EAST(1, ">"), SOUTH(2, "V"), WEST(3, "<"), NORTH(4, "^");

    private final int value;
    private final String symbol;

    // 접근 제어자 private이 생략됨
    Direction(int value, String symbol) {
        this.value = value;
        this.symbol = symbol;
    }

    public int getValue() {
        return value;
    }

    public String getSymbol() {
        return symbol;
    }
}

추상 메서드 추가

열거형 Transportation은 운송 수단의 종류 별로 상수를 정의하고 있으며, 각 운송 수단에는 기본요금이 책정되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Transportation {
    BUS(100), TRAIN(150), SHIP(100), AIRPLANE(300);

    private final int BASIC_FARE;

    private TransPortation(int basicFare) {
        BASIC_FARE = basicFare;
    }

    int fare() {    // 운송 요금을 반환
        return BASIC_FARE;
    }
}

하지만 이것만으로는 부족합니다. 거리에 따라 요금을 계산하는 방식이 각 운송 수단마다 다를 수 있기 때문입니다. 이럴 때, 열거형에 추상 메서드 ‘fare(int distance)’를 선언하면 각 열거형 상수가 이 추상 메서드를 반드시 구현해야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
enum Transportation {
    BUS(100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    },
    TRAIN(150) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    },
    SHIP(100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    },
    AIRPLANE(300) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };

    abstract int fare(int distance);    // 거리에 따른 요금을 계산하는 추상 메서드

    protected final int BASIC_FARE;     // protected로 해야 각 상수에서 접근가능

    Transportation(int basicFare) {
        BASIC_FARE = basicFare;
    }

    public int getBasicFare() {
        return BASIC_FARE;
    }
}

위의 코드는 열거형에 정의된 추상 메서드를 각 상수가 어떻게 구현하는지 보여준다. 마치 익명 클래스를 작성한 것처럼 보일 정도로 유사합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
enum Transportation {
    BUS(100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    },
    TRAIN(150) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    },
    SHIP(100) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    },
    AIRPLANE(300) {
        int fare(int distance) {
            return distance * BASIC_FARE;
        }
    };

    protected final int BASIC_FARE; // protected로 해야 각 상수에서 접근가능

    Transportation(int basicFare) { // private Transportation(int basicFare)
        BASIC_FARE = basicFare;
    }

    public int getBasicFare() {
        return BASIC_FARE;
    }

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

class EnumEx3 {
    public static void main(String[] args) {
        System.out.println("bus fare=" + Transportation.BUS.fare(100));
        System.out.println("train fare=" + Transportation.TRAIN.fare(100));
        System.out.println("ship fare=" + Transportation.SHIP.fare(100));
        System.out.println("airplane fare=" + Transportation.AIRPLANE.fare(100));
    }
}

/**
 * 결과
 */

bus fare=10000
train fare=15000
ship fare=10000
airplane fare=30000

예제에서는 각 열거형 상수가 추상 메서드 fare()를 똑같은 내용으로 구현했지만, 다르게 구현될 수도 있게 하려고 추상 메서드로 선언한 것입니다.


Enum의 이해


지금까지 열거형에 대해서 모두 살펴봤는데, 열거형의 이해를 돕기 위해 마지막으로 열거형이 내부적으로 어떻게 구현되었는지에 대해 설명하고자 합니다.

만일 열거형 Direction이 다음과 같이 정의되어 있을 때,

1
2
3
enum Direction {
    EAST, SOUTH, WEST, NORTH
}

사실은 열거형 상수 하나하나가 Direction객체입니다. 위의 문장을 클래스로 정의한다면 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
class Direction {
    static final Direction EAST = new Direction("EAST");
    static final Direction SOUTH = new Direction("SOUTH");
    static final Direction WEST = new Direction("WEST");
    static final Direction NORTH = new Direction("NORTH");

    private String name;

    private Direction(String name) {
        this.name = name;
    }
}

Direction클래스의 static상수 EAST, SOUTH, WEST, NORTH의 값은 객체의 주소이고, 이 값은 바뀌지 않는 값이므로 ‘==’로 비교가 가능한 것입니다.

모든 열거형은 추상클래스 Enum의 자손이므로, Enum을 흉내 내어 MyEnum을 작성하면 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
abstract class MyEnum<T extends MyEnum<T>> implements Comparable<T> {
    static int id = 0;  // 객체에 붙일 일련번호(0부터 시작)

    int ordinal;
    String name = "";

    public int ordinal() {
        return ordinal;
    }

    MyEnum(String name) {
        this.name = name;
        ordinal = id++; // 객체를 생성할 때마다 id의 값을 증가시킨다.
    }

    public int compareTo(T t) {
        return ordianl - t.ordinal();
    }
}

앞서 6장에서 배운 것과 같이 객체가 생성될 때마다 번호를 붙여서 인스턴스변수 ordinal에 저장합니다. 그리고 Comparable인터페이스를 구현해서 열거형 상수간의 비교가 가능하도록 되어 있습니다. 구현 내용은 간단하다. 두 열거형 상수의 ordinal값을 서로 빼주기만 하면 됩니다. 만일 클래스를 MyEnum와 같이 선언하였다면, compareTo()를 위와 같이 간단히 작성할 수 없었을 것입니다. 타입 T에 ordinal()이 정의되어 있는지 확인할 수 없기 때문입니다.

1
2
3
4
5
6
abstract class MyEnum<T> implements Comparable<T> {
    ...
    public int compareTo(T t) {
        return ordinal - t.ordinal();   // 에러.
    }
}

위 코드는 타입 T가 MyEnum의 자손이어야 한다는 의미를 나타냅니다. 타입 T가 MyEnum의 자손이므로 ordinal()이 정의되어 있는 것은 분명하므로 형변환 없이도 에러가 나지 않습니다.

그리고 추상 메서드를 새로 추가하면, 클래스 앞에도 ‘abstract’를 붙여줘야 하고, 각 static상수들도 추상 메서드를 구현해주어야 합니다. 아래의 코드에서는 익명 클래스의 형태로 추상 메서드를 구현하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
abstract class Direction extends MyEnum {
    static final Direction EAST = new Direction("EAST") {   // 익명 클래스
        Point move(Point p) {
            ...
        }
    };

    static final Direction SOUTH = new Direction("SOUTH") {
        Point move(Point p) {
            ...
        }
    };

    static final Direction WEST = new Direction("SOUTH") {
        Point move(Point p) {
            ...
        }
    };

    static final Direction NORTH = new Direction("NORTH") {
        Point move(Point p) {
            ...
        }
    };

    private String name;

    private Direction(String name) {
        this.name = name;
    }

    abstract Point move(Point p);
}

읽어주셔서 감사합니다. 😊

Reference
자바의 정석 - 남궁성
ChatGPT - OpenAI

This post is licensed under CC BY 4.0 by the author.