원래 Enum에 관련된 글도 이 전 블로그에서 작성했던 적이 있었다. 한번 글들을 날려먹고 두번째 작성하는 글이므로,
이 전에 작성했던 글만큼의 정성을 들일 수 있을까…..?

일단 Java Enum에 대해서는 내가 지금부터 쓰는 글보다 잘 써 놓은 글이 있으니 먼저 읽어 보는것도 좋을 것 같다.

enum을 사용하는 이유는 여러 가지가 있으나, 가장 큰 이유는 DB의 코드값과 달리 IDE 또는 컴파일 단계에서 지원을 받을수 있다는게 가장 큰 이유일 것이다. 자동완성도 지원하며 매직넘버와 달리 오타를 냈다면 컴파일 단계에서 잡아준다. 그말이 그말이잖..

장점, 장점을 보자

컴파일 단계에서 지원을 받을 수 있다

IDE에서는 자동완성도 되고, 오타가 났을 경우에 런타임 에러가 아닌 컴파일 에러를 내므로 버그를 만들 확률이 줄어든다.(어디까지나 완벽히 없어지는건 아니다. 버그 장인은 도구를 탓하지 않는다 )

리팩토링 또는 수정이 쉽다

값이 추가 되더라도 enum값 하나만 추가 하면 된다. 혹시 enum이 내포하고 있는 값이 달라진다면 enum만 수정하면 된다.

그 외에도

기본적으로 enum은 선언시 불변객체로 생성되므로 여러번 할당하더라도 새로 object를 생성하는 비용 부담이 덜하고, String과는 달리 equals를 사용하여 비교할 필요가 없다. == 를 사용하여 비교해도 잘 작동한다.

그렇다면 단점은?

배포 문제에서 자유롭지 못하다

컴파일 단계에서 작동해서 IDE의 지원을 받을 수 있다는 건, 반대로 배포 문제에서 자유롭지 못하다는 뜻이기도 하다. 간단한 값 추가가 있다고 하더라도 DB에서 사용하는 코드와는 달리 배포를 해야 한다. 요즘은 CI/CD가 워낙 잘 돼있는데다가 무중단 배포 환경도 손쉽게 구축하여 사용하는 추세라 크게 문제가 안된다고 할 수 있지만, 그렇다고 하더라도 분명 DB에서 손쉽게 추가하는 것에 비하면 부담이 되는 부분은 사실이다.

만약 DB에서 일원화된 persistant 영역에서 관리하는 코드값이라고 하면 코드만 추가 해주면 끝이지만, 여러 서비스에서 사용하는 값이라면 각 서비스 별로 챙겨서 배포해야 하며, 그러다 보면 누락하는 서비스가 있을수도 있다. (그게 우리 서비스도 이랬던 적이 있더랬..) 또한 이런식으로 누락하는 서비스에서 발생하는 오류는 단순히 DB 코드를 추가했다가 생기는 오류보다 더 크리티컬 하다.

enum이 적극적으로 쓰이지 않던 때에는 거의 모든 상태값들을 코드 테이블을 하나 만들어놓고 다 때려 박았다. 사실 배포 문제에서 자유롭지 못한 문제가 있지만, 생각보다 우리가 코드라고 생각하는 아이들은 사실은 코드가 아닌 경우가 많다. 자주 바뀌는 값과 바뀌지 않는 값을 잘 구분해서 자주 바뀌지 않는 녀석들을 enum으로 선언해서 사용하자.

숫자만 넣어서 만들수는 없다

망치를 든 사람에게는 모든게 못으로 보인다고, enum의 맛을 알게 되면 모든 코드를 enum으로 바꾸고 싶은 욕망에 사로잡히게 된다. 그러나 이렇게 enum으로 하나둘씩 바꾸다 보면 숫자로만 돼있는 코드들은 enum으로 만들 수 없다는 것을 깨닿게 된다.
enum의 상태값들은 java의 필드명 정의하는 규칙과 같이 숫자로만 구성할 수는 없다.

사실 대부분 enum 상태값으로 정의하고자 하는 속성은 숫자가 아닌 다른 뜻을 갖고 있는 경우가 많이 있지만 진짜 숫자만 있는 경우라면? 대체할 만한 단어가 없다면??? 그럼 그냥 다른 문자를 붙이는 수밖에 없다.

1
2
3
enum Numbers {
_1, _2, _3;
}

별로 맘에는 안든다. 그렇지만 범위를 제한할 수 없는 int 필드보다는 낫다.

1
2
3
enum Numbers {
1ST, 2ND, 3RD;
}

뭐 이런것도 나쁘지는 않다.

좀 더 고급지게 사용해보자

자바의 enum은 다른 언어의 enum에 비해서 좀 더 활용도가 높다. 사실 java에서 enum은 하나의 완전한 클래스로 취급 되는데 이 때문에 enum도 인스터스 변수를 가질수도 있고, interface를 implements 하도록 하거나 abstract로 선언할 수도 있으며 method나 static method를 만들수도 있다.

아래 예제를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Sequences{
FIRST(1),
SECOND(2),
THIRD(3);

private int order;

Sequences(int order) {
this.order = order;
}

public int getOrder() {
return order;
}
}

이렇게 단순하게 사용하는 경우는 별로 없지만(심지어 java는 ordinal() 이라는 기본 메소드를 제공한다. 그러나 ordinal은 가급적 사용하지 않는것이 좋다. 이유는 후술) 코드에서 보이는 바와 같이 클래스의 모습과 크게 다르지 않다. 더욱이 자바의 enum은 이런 변수를 하나만 사용 가능한 것이 아니라 여러개가 사용 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Sequences{
FIRST(1, "1st"),
SECOND(2, "2nd"),
THIRD(3, "3rd");

private int order;
private String shorten;

Sequences(int order, String shorten) {
this.order = order;
this.shorten = shorten;
}

public int getOrder() {
return order;
}

public String getShorten() {
return shorten;
}
}

참고로 FIRST(1, "1st") 이렇게 enum을 선언하는 구문은 생성자 메소드를 호출하는 방식이기 때문에 생성자를 어떻게 만드느냐에 따라 다른 방식으로도 사용 가능하다. 또한 enum은 static 영역에 저장되기 때문에 enum을 호출할때마다 생성자를 호출하는것이 아니어서 알고리즘의 효율성을 그렇게까지 신경쓰지 않아도 된다.

내가 즐겨 쓰는 방법이다.

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
enum Media {
MOVIE("horror, comedy, sf, family"),
MUSIC("dance, balad, rock, classic");

private Set<String> genreSet;

Media(String genreString) {
this.genreSet = Arrays.asList(genreString.split(","))
.stream()
.map(String::trim)
.collect(Collectors.toSet());
}

public boolean contains(String genre) {
return this.genreSet.contains(genre);
}

public static Media determine(String genre) {
return Arrays.asList(Media.values()).stream()
.filter(media -> media.contains(genre))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("there is no matched genre"));
}

}

물론 아래 코드처럼 생성자에서 array를 바로 만들어서 받을수도 있겠으나 내 취향은 위 코드가 더 맞아서 주로 이렇게 쓴다. (무엇보다 set의 contains 시간 복잡도가 list보다 더 유리하다)

1
2
3
4
5
6
7
8
9
10
11
enum Media {

MOVIE(Arrays.asList("horror", "comedy", "sf", "family")),
MUSIC(Arrays.asList("dance", "comedy", "sf", "family"));

private List<String> genreSet;

Media(List<String> genreSet) {
this.genreSet = genreSet;
}
...

또한 enum에서 변수로 익명함수를 담고 있을 수도 있기 때문에 이를 사용하면 더욱더 활용도가 높아진다. 간단한 사칙연산을 수행하는 enum이다.
(Lombok도 잘 먹혀서, 아래부터는 lombok을 활용해 constructor를 만들어 보겠다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@AllArgsConstructor
enum Calc {
ADD("+", (a, b) -> a + b),
SUBTRACT("-", (a, b) -> a - b),
TIMES("*", (a, b) -> a * b),
MOD("/", (a, b) -> a / b);

@Getter
private String character;

private BiFunction<Integer, Integer, Integer> calculation;

public int calc(int a, int b) {
return calculation.apply(a, b);
}
}

아래는 위와 같은 일을 하는 코드를 abstract 메소드를 활용하여 만들어 본 코드이다. (java7)
이렇게도 사용 가능하니 적절히 필요에 따라 원하는 방법대로 사용하면 좋다.

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
@Getter
@AllArgsConstructor
public enum Calc {
ADD("+"){
@Override
public int calc(int a, int b){
return a + b;
}
},
SUBTRACT("−"){
@Override
public int calc(int a, int b){
return a - b;
}
},
TIMES("×"){
@Override
public int calc(int a, int b) {
return a * b;
}
},
MOD("÷"){
@Override
public int calc(int a, int b){
return a / b;
}
};

private String character;

public abstract int calc(int a, int b);
}

이런식으로 enum에 디자인 패턴을 얹어 사용하면 코드들이 단순 명료해 지는것을 느낄 수 있을 것이다.

ordinal()을 사용하지 않아야 하나?

아까 잠깐 언급하고 넘어간 ordinal() 메소드에 대해 이야기 해보자. enum은 순서를 가진 열거형이고, 그 순서값을 int로 반환하는 ordinal()이라는 메소드를 제공해준다. 그러나 ordinal을 사용하게 되면 나중에 유지보수에 문제가 발생하게 된다. (이 부분은 effective java item 35를 보면 자세히 나온다)

중간에 새로운 enum 요소가 끼어 드는 경우 새로운 요소 뒤에 위치한 enum들의 연산은 모두 망가지게 된다.

따라서 ordinal을 사용하는 것보다 차라리 위에서 만든 Sequence enum처럼 인스턴스 필드를 사용하여 int value를 담아두고 이를 return하는 getter를 사용하는것이 더 좋다.


내가 java 에서 enum을 사용하는 방법은 이정도이다. 물론 더 다양 하게 사용하는 방법도 있다. (enum 내부에 또 enum을 정의해 사용한다던지, 좀 다른 이야기지만 EnumMap을 사용하는 방법도 있고) 그러나 이정도만 알아도 java enum 초보 티는 좀 벗었다고 할 수 있지 않을까?

끗.