Enum 은 왜 탄생했을까?
과거에는 어떤방식으로 상수를 정의했었나?
1. final 상수
우선적으로 final
제어자를 이용하여 변수를 상수화 하는 방식이다.
final
제어자를 할당함으로써 한번 지정하며 바뀌지 않게 설정되며, 동시에 static
을 사용하여 메모리에 한번만 할당 되게 설정한다. 하지만 해당 방식은 접근제어자들 때문에 가독성이 좋지 않다는 단점이 존재한다.
1
2
3
4
5
6
7
8
9
class EnumExample {
private final static int MONDAY = 1;
private final static int TUESDAY = 2;
private final static int WEDNESDAY = 3;
private final static int THURSDAY = 4;
private final static int FRIDAY = 5;
private final static int SATURDAY = 6;
private final static int SUNDAY = 7
}
2. interface 상수
인터페이스에는 항상 추상 메소드만을 선언할 수 있는 것은 아니다. 인터페이스 내에서도 상수를 선언할 수 있는데, 인터페이스의 멤버변수에는 public static final 속성을 생략하여 선언 할 수 있는 특징이 있어 코드를 좀 더 간결하게 작성이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface MONTH {
int JANUARY = 1;
int FEBRUARY = 2;
int MARCH = 3;
int APRIL = 4;
int MAY = 5;
int JUNE = 6;
int JULY = 7;
int AUGUST = 8;
int SEPTEMBER = 9;
int OCTOBER = 10;
int NOVEMBER = 11;
int DECEMBER = 12;
}
하지만 이 방법도 문제가 존재하는데, 상수가 결국은 정수값이라는 문제이다. 상수라는 것은 ‘변하지 않는 값’ 을 의미하지만 ‘고유한 값’ 과도 일맥상통한다.
이러한 측면에서 인터페이스 방식은 잘못된 상수가 변수에 할당 될 수 있고, 컴파일 에러 없이 실행이 되기 때문에 프로그램 크기가 커질수록 개발자의 실수가 잦아지며 이러한 제약적이지 않는 요소들 때문에 프로그램에 버그가 발생하게 된다.
Enum 의 등장
앞서말한 문제들로 Java 에서는 1.5 버전 이후 상수만을 다루는 enum 타입 클래스를 만들었다.
Java 에서 열거형(Enumeration)의 줄임말로, 상수들의 집합을 효과적으로 관리하기 위해 사용된다. 이는 일반적으로 고정된 값들의 집합을 나타내며, 주로 코드의 가독성을 높이고 유지보수성을 향상시키는 데 사용된다.
Enum을 정의할 때는 ‘enum’ 키워드를 사용한다. 예를들어, 요일을 나타내는 Enum 을 정의할 때 다음과 같이 작성할 수 있다.
1
2
3
4
5
6
7
8
9
public enum Day {
SUNDAY,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
}
위 예시에서는 Day 라는 Enum 타입을 정의하고, 이 Enum 에는 일주일의 요일들이 각각 상수로 정의되어 있다.
Enum 은 Class 와 유사한 구조를 가지고 있으며, 각각의 상수는 Enum 타입의 인스턴스이다. 이러한 상수들은 보통 대문자로 작성되며, 해당 Enum 내에서 고유한 값으로 사용된다. 또한 Enum 은 필요에 따라 메서드를 가질 수 있는데, 각 상수는 특정한 동작을 수행하기 위한 메서드를 가질 수 있으며 Enum 타입 자체도 메서드를 추가하여 특정한 기능을 제공할 수 있다.
Enum 은 코드의 가독성을 높이고 안정성을 제공하는 데 매우 큰 도움이 된다. 상수 집합이 고정되어 있으므로 코드에서 사용되는 값들을 명확하게 정의할 수 있고, 컴파일 시간에 오류를 최소화 할 수 있다.
이러한 특징들로 인해 Java Enum 은 프로그래밍에서 자주 사용되며, 코드를 보다 간결하고 이해하기 쉽게 만들어준다.
Enum 의 참조 방식
Enum 타입 객체도 하나의 데이터 타입이므로 변수를 선언하고 사용하면 된다.
1
2
3
// 열거타입 변수 = 열거타입.열거상수;
Week monday = Week.MONDAY;
Week sunday = Week.SUNDAY;
한가지 알아둘 점은 enum 타입은 특수한 클래스란 점인데, primitive 타입이 아닌 reference 타입으로 분류된다. 그래서 enum 상수 값은 heep 영역에 저장되게 된다.
String 처럼 스택 영역에 있는 변수들이 힙 영역에 있는 데이터의 주소값을 저장함으로써 참조 형태를 띄게 된다. 그래서 다음과 같이 같은 enum 타입 변수 끼리 같은 상수 데이터를 바라봄으로써 둘이 주소를 비교하는 연상 결과는 true 가 된다.
1
2
3
4
5
Week today = null; // 참조 타입이기 때문에 null도 저장 가능
today = Week.SUNDAY;
// 주소값 비교
System.out.println(today == Week.SUNDAY); // true
Enum 메소드의 종류
메소드 | 설명 | 리턴 타입 |
---|---|---|
name() | 열거 객체의 문자열을 리턴 | String |
ordinal() | 열거 객체의 순번(0 부터 시작) 을 리턴 | int |
compareTo() | 열거 객체를 비교해서 순번 차이를 리턴 | int |
valueOf(String name) | 문자열을 입력받아서 일치하는 열거 객체를 리턴 | enum |
values() | 모든 열거 객체들을 배열로 리턴 | enum[] |
Enum 활용 예 - 현금영수증 타입별 금액 계산
1. Enum 타입별 연산식 처리
- 매출금의 경우 원금액 / 공급가액 / 부가세액 3가지로 분류할 수 있다.
- 원금액 - 발생한 결제의 총금액
- 공급가액 - 원금액을 1.1 로 나눈 금액
- 부가세액 - 공급가액의 10%
이러한 경우 enum을 통해 각 매출타입이 연산의 책임을 갖도록 구현 할 수 있다. Java 8 이전엔 해당 경우 익명클래스 or 인터페이스 구현체을 사용해 코드로 작성했지만 Function 인터페이스 의 등장으로 상대적으로 가볍게 구현이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum AmountType {
ORIGIN_AMOUNT("원금액", amount -> amount),
SUPPLY_AMOUNT("공급가액", amount -> Math.round(amount.doubleValue() / 1.1)),
VAT_AMOUNT("부가세액", amount -> Math.round(amount.doubleValue() / 11);
private final String type;
private final Function<int, int> calculate;
AmountType(String type, Function<int, int> calculate) {
this.type = type;
this.calculate = calculate;
}
public int getAmount(int amount) {
return calculate.apply(amount);
}
public String getType() {
return type;
}
}
위 enum 코드를 사용하게 되면 해당 타입에 직접적으로 처리하게 된다.
1
2
3
4
5
6
7
8
9
10
public class BillingInfo {
int amount = 10000;
// 원금액
int originAmount = AmountType.ORIGIN_AMOUNT.getAmount(amount);
// 공급각액
int supplyAmount = AmountType.SUPPLY_AMOUNT.getAmount(amount);
// 부가세액
int vatAmount = AmountType.VAT_AMOUNT.getAmount(amount);
}
2. 타입별 그룹화
결제방식에 따란 서로 다른 수수료를 부가해야하는 비지니스 로직에서, 각 결제방식이 어느 그룹에 포함되는지에 대한 분기로직이 필요하다.
별도의 클래스나 DB로 관리하기엔 코드가 분산되고 관리하기가 애매해지므로 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
public enum PayType {
// 현금
BANK_TRANSFER("계좌이체"),
UNACCOUNT_DEPOSIT("무통장 입금"),
FIELD_PAYMENT("현장결제"),
TOSS("토스"),
// 결제대행사
CARD("신용카드"),
PAYCO("페이코"),
KAKAO_PAY("카카오페이"),
NAVER_PAY("네이버페이"),
// 기타결제
POINT("포인트"),
COUPON("쿠폰");
private final String name;
PayType(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
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
public enum PayGroup {
CASH("현금", Arrays.asList(PayType.BANK_TRANSFER, PayType.UNACCOUNT_DEPOSIT, PayType.FIELD_PAYMENT, PayType.TOSS)),
CARD("결제대행사", Arrays.asList(PayType.CARD, PayType.PAYCO, PayType.KAKAO_PAY, PayType.NAVER_PAY)),
ETC("기타", Arrays.asList(PayType.POINT, PayType.COUPON)),
EMPTY("없음", Collections.EMPTY_LIST);
private final String group;
private final List<PayType> payList;
PayGroup(String group, List<PayType> payList) {
this.group = group;
this.payList = payList;
}
public static PayGroup findByPayType(PayType payType) {
return Arrays.stream(PayGroup.values())
.filter((payGroup) -> payGroup.hasPayCode(payType))
.findAny()
.orElse(EMPTY);
}
public boolean hasPayCode(PayType payType) {
return payList.stream().anyMatch((pay) -> pay == payType);
}
public String getGroup() {
return group;
}
public List<PayType> getPayList() {
return payList;
}
}
결제수단의 그룹에 관한 분기는 PayGroup 을 통해 처리 할 수 있게 되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package org.example;
import java.util.Arrays;
public enum StatusEnum {
SUCCESS(0), FAIL(1);
private final int statusCode;
StatusEnum(int statusCode) {
this.statusCode = statusCode;
}
static StatusEnum getStatusFromCode(int statusCode) {
return Arrays.stream(values()).filter((status) -> status.getStatusCode() == statusCode)
.findAny().orElseThrow(IllegalArgumentException::new);
}
public int getStatusCode() {
return statusCode;
}
}