Enum
Enumerance type으로 열거형이라는 뜻이다. JAVA 1.5부터 생겨난 기능으로 열거체를 정의할 수 있는 클래스이다. 주로 서로 관련있는 상수들끼리 모아 상수들을 대표할 수 있는 이름으로 타입을 정의하는데 사용된다.
- 비교 시 실제 값 뿐만 아니라 타입도 체크할 수 있다.
- 상수 값이 재정의되어도 다시 컴파일할 필요가 없다.
사용 방법
1
2
3
public enum Month {
JAN, FEB, MAR, ...
}
위와 같이 정의할 수 있고 Month.JAN과 같이 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum Color {
RED(2),
BLUE(3),
GREEN(9);
private final int value;
Color(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
혹은 위와 같이 특정 값을 직접 저장하여 정의할 수 있다.
주요 메서드
- values()
열거된 모든 원소를 배열에 담아 순서대로 리턴해준다.
1
2
3
4
5
6
7
8
for (Month mon : Month.values()) {
System.out.println(mon);
}
//결과
JAN
FEB
MAR
- ordinal()
원소에 열거된 순서를 정수 값으로 리턴해준다.
1
2
3
4
5
6
Month month = Month.FEB;
System.out.println(month.ordinal());
//결과
1 (JAN = 0, FEB = 1, MAR = 2)
- valueOf()
매개변수로 주어진 String과 열거형에서 일치하는 이름을 갖는 원소를 리턴한다.
일치하지 않는 경우 IllegalArgumentException 발생.
1
2
3
4
5
6
Month month = Month.valueOf("FEB");
System.out.println(month);
//결과
FEB
- eqauls()
객체가 해당 열거체 상수와 동일한 지 판단한다. 동일하다면 true.
Enum의 장점
- 코드가 단순해지고 가독성이 좋아진다.
- 허용 가능한 값들을 제한하여 type safe를 보장한다.
- 구현의 의도가 명확하게 열거임을 나타낼 수 있다.
- 값이 추가되거나 변경되는 경우 유지 보수가 용이하다.
- 싱글톤을 보장한다.
type safe를 보장한다.
Enum 타입은 고정된 상수들의 집합으로 런타임이 아닌 컴파일 타임에 모든 값을 알고 있어야 한다. 즉 다른 패키지나 클래스에서 enum 타입에 접근해 동적으로 어떤 값을 정해줄 수 없고 컴파일 시 타입 안정성이 보장된다. 이 때문에 생성자의 접근 제어자가 private인 것이다.
그리고 특정 범위의 값만 사용 가능하도록 하기 때문에 컴파일 오류나 런타임 예외를 줄일 수 있다.
1
2
3
4
5
6
public enum Number {
RANGE_MIN(1),
RANGE_MAX(99);
/...
}
어떠한 범위를 enum을 통해 관리한다고 가정한다면 위의 RANGE_MIN과 같은 것을 사용하게 된다.
enum을 통해 관리하지 않으면 직접 값을 넣어주어야 하는데, 실수로 1이나 99가 아닌 다른 값을 넣을 수도 있고 다른 타입의 값도 넣을 수 있다. 이런 부분을 방지해준다.
싱글톤을 보장한다.
하나의 JVM에 하나의 인스턴스만 존재한다는 것이다. 멀티 스레드에 의해 클래스의 같은 인스턴스가 재사용된다.
enum이 싱글톤이 보장되는 이유는 크게 3가지가 있다.
- clone 미지원
enum의 clone() 메서드를 살펴보면 아래와 같다.
1
2
3
4
5
6
7
8
/* Throws CloneNotSupportedException.
* This guarantees that enums are never cloned, which is necessary to preserve their "singleton" status.
* Returns: (never returns)
*/
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
clone()은 보통 값만 같고 서로 다른 객체가 반환되기를 기대하고 사용한다.
하지만 enum은 인스턴스를 재사용하고 각 인스턴스의 값이 하나씩만 존재해야 하므로 clone을 지원하지 않는다.
- 역직렬화로 인한 중복 인스턴스 생성 방지
enum은 기본적으로 serializable이 가능하다. serializable interface를 구현할 필요가 없어 역직렬화 시 새로운 객체가 생성될 걱정을 하지 않아도 된다.
- Reflection을 이용한 enum의 인스턴스화를 금지한다.
Enum의 생성자는 Sole Constructor이다. 컴파일러에서 사용하고 사용자가 직접 호출할 수 없다. 따라서 enum -> getConstructor -> newInstance로 사용하는 객체 생성 흐름이 적용되지 않는다.
Enum의 활용
데이터들 간의 연관관계 표현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum Switch {
ON("켜짐", 1),
OFF("꺼짐", 2);
private String state;
private int value;
/...
public Switch opposite() {
if (this == Switch.ON) {
return Switch.OFF;
}
return Switch.ON;
}
어떤 부분에서는 Switch의 ON을 표현할 때 “켜짐”을 받아 조건에 사용해 스위치를 켜는 행위를 정의했을 수 있고, 어떤 부분에서는 1 값을 받았을 때 스위치를 켜는 등의 행위를 정의했을 수 있다.
그러나 Enum을 활용하면 위와 같이 “켜짐”, 1과 같은 결국 같은 의미의 타입만 다른 값들을 하나로 묶을 수 있다. 추가적으로 ON에 필요한 타입이 생긴다면 Enum 상수와 get 메서드만 추가하면 된다.
상태와 행위를 한 곳에서 관리
서로 다른 계산식을 적용해야 할 경우가 있다.
1
2
3
4
5
6
7
8
9
10
11
public static long calc(String code, long originValue) {
if ("CODE_A".equals(code)) {
return originValue;
} else if ("CODE_B".equals(code)) {
return originValue * 10;
} else if ("CODE_C".equals(code)) {
return originValue * 100;
} else {
return 0;
}
}
이렇게 Code별로 다른 계산식을 적용해야 한다고 가정하면 코드는 코드대로 조회하고 계산은 별도의 클래스와 메서드를 통해 진행해야 함을 알 수 있다.
1
2
3
String code = selectCode();
long originValue = 10000L;
long result = Calculator.calc(code, originValue);
뽑아낸 코드에 따라 지정된 메서드에서만 계산되기를 원하는데, 현재 상태로는 강제할 수 있는 수단이 없다. 지금은 문자열 인자를 받고, long 타입을 리턴하는 모든 메서드를 사용할 수 있는 상태이다. 실수할 확률이 높은 것이다.
이러한 부분을 Enum을 활용한다면?
1
2
3
4
5
6
7
8
9
10
11
12
public enum CalculatorType {
CODE_A(value -> value),
CODE_B(value -> value * 10),
CODE_C(value -> value * 100),
CODE_ETC(value -> 0L);
private Function<Long, Long> expression;
CalculatorType(Function<Long, Long> expression) { this.expression = expression; }
public long calc(long value) { return expression.apply(value); }
}
코드를 정의해놓고 그 코드가 본인의 계산식을 갖도록 만든 것이다.
(JAVA8이 업데이트 되면서 인자값으로 함수를 사용할 수 있다.)
이렇게 한다면 직접 코드에게 계산을 요청할 수 있게 된다.
1
2
3
String code = selectCode();
long originValue = 10000L;
long result = code.calc(originValue);
데이터 그룹 관리
결제에는 결제 종류와 결제 수단이라는 2가지 형태로 표현된다.
- 결제 종류 : 현금, 카드, 기타
- 결제 수단 : 계좌이체, 카카오페이, 네이버페이, 배민페이, 포인트, 기타
결제된 건이 어떤 결제수단으로 진행되고, 해당 결제 방식이 어느 결제 종류에 속하는 지 확인해야 한다고 하면 아래와 같이 표현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
public static String getPayGroup(String payCode) {
if ("ACCOUNT_TRANSFER".equals(payCode) || "REMITTANCE".equals(payCode) || ...) {
return "CASH";
} else if ("PAYCO".equals(payCode) || "BAEMIN_PAY".equals(payCode) || ...) {
return "CARD";
} else if ("POINT".equals(payCode) || "COUPON".equals(payCode) {
return "ETC";
} else {
return "EMPTY";
}
}
이렇게 구현함으로써 발생하는 문제는 아래와 같다.
- 결제 종류와 결제 수단의 관계를 파악하기 어렵다.
- 결제 종류가 결제 수단을 포함하고 있는 관계인데 위 상태에서는 파악하기 힘들다.
- 입력값과 결과값이 예측 불가능하다.
- 결제 수단의 범위를 지정할 수 없어 문자열이라면 전부 파라미터로 전달 가능하다.
- 결과를 받는 쪽에서도 문자열을 받기 때문에 결제 종류로 지정된 값만 받을 수 있도록 검증하는 코드가 필요해진다.
- 그룹별 기능을 추가하기 어렵다.
- 결제 종류에 따라 추가 기능이 필요할 경우 각 결제 종류에 따라 if문으로 메서드를 실행하도록 해야한다.
이를 enum으로 전환하면?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public enum PayGroup {
CASH("현금", Arrays.asList("ACCOUNT_TRANSFER", "REMITTANCE", ...)),
CARD("카드", Arrays.asList("PAYCO", "BAEMIN_PAY", ...)),
ETC("기타", Arrays.asList("POINT", "COUPON")),
EMPTY("없음", Collections.EMPTY_LIST);
private String title;
private List<String> payList;
/...
public static PayGroup findByPayCode(String code) {
return Arrays.stream(PayGroup.values())
.filter(payGroup -> payGroup.hasPayCode(code))
.findAny()
.orElse(EMPTY);
}
public boolean hasPayCode(String code) {
return payList.stream()
.anyMatch(pay -> pay.equlas(code));
}
}
code를 받으면 본인들이 갖고있는 문자열들을 확인해 어느 Enum 상수에 포함되어 있는지 확인할 수 있도록 할 수 있다.
관리 주체를 PayGroup에게 준 것이고 이제는 PayGroup에게 물어보는 방식으로 해결할 수 있는 것이다.
1
2
String payCode = selectPayCode();
PayGroup payGroup = PayGroup.findByPayCode(payCode);
하지만 여기서도 문제가 하나 남아있다. 여전히 결제 수단(“BAEMIN_PAY”)은 문자열인 것이다.
파라미터로 전달된 값이 잘못되었을 경우가 있을 때 관리가 되지 않는다. 따라서 결제 수단도 Enum으로 전환해 해결할 수 있다.
1
2
3
4
5
6
7
8
9
10
public enum PayType {
ACCONUT_TRANSFER("계좌이체"),
REMITTANCE("무통장입금"),
PAYCO("페이코"),
BAEMIN_PAY("배민페이")
/...
;
/...
}
이렇게 정의한 PayType을 PayGroup에서 사용하도록 하면 된다.
1
2
3
4
5
CASH("현금", Arrays.asList(PayType.ACCOUNT_TRANSFER, PayType.REMITTANCE, ...),
/...
private String title;
private List<PayType> payList;
더 자세한 내용은 아래 링크를 참고하면 된다.