Home Enum 활용
Post
Cancel

Enum 활용

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;

더 자세한 내용은 아래 링크를 참고하면 된다.

java enum 활용기

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