안녕하세요. 이전 포스팅에서는 Enum의 정의 및 간단한 사용 방법 등에 대해서 알아 보았습니다. 이번 포스팅에는 인터넷에서 다양한 사람들이 소개하고 있는 사례들을 같이 보며 어떠한 경우에 Enum을 사용하면 좋을 지에 대해 같이 고민해 보는 시간을 가지면 좋을 것 같습니다. 타인의 블로그 내용은 내용 소개 후 출처를 아래에 밝히겠습니다. 혹시 문제시 된다면 바로 삭제하겠습니다.
특정한 데이터가 서로 연관이 있을 때 이를 Enum의 형태로 분류하는 것이 데이터를 관리하는 하나의 좋은 방법이라고 이전 포스팅에서 이야기 했습니다. 그런데 이렇게 분류된 Enum 각각의 인스턴스가 서로 다른 연산식을 처리해야하는 경우는 어떻게 해야할까요? 일반적으로는 Enum은 단순히 상수 열거 클래스라는 이미지가 강하고 상수를 열거하는 형태로만 사용 되고 연산은 Enum을 호출하는 쪽에서 하는 경우가 많습니다. 하지만 이는 상태와 행위가 명백하게 분리되는 것이며 이러한 코드가 많아 질 수록 로직을 확인하기 위해 코드를 타고 들어가야하는 경우가 많아질 것입니다. 결론부터 얘기하자면 상태와 행위가 한 곳에 있을 수 있도록 Enum을 관리하는 것이 바람직합니다. 아래의 예들을 보며 타입(데이터) 별로 다른 연산식을 Enum 을 통해 처리하는 것을 보겠습니다.
매출액에 대해서 계산하는 프로그램의 경우 매출액은 원금, 부가세, 공급액으로 분류할 수 있으며 각각을 계산하는 로직은 모두 다릅니다. 이 경우 매출액에 대한 계산 및 관리는 어떤 클래스에서 담당하는 것이 좋을까요? 일반적인 경우 매출액, 부가세, 공급액을 Enum 클래스의 상수로 분리하고 계산과 관련된 부분은 이를 호출하는 곳에서 하는 경우가 많은데요. 이 부분이 위에서 언급한 상태와 행위가 분리 된 것이라고 생각합니다. 아래의 코드는 상태와 행위가 한 곳에 있도록 매출액 클래스를 작성한 예제입니다.
public enum SaleMoney {
ORIGIN("매출액", origin -> origin),
SUPPLY("공급액", origin -> Math.round(origin.doubleValue()) * 10 /11),
VAT("부가세", origin -> origin / 11),
NOTHING("없음",origin -> 0L);
private final String name;
private final Function<Long, Long> expression;
SaleMoney(String name, Function<Long, Long> expression) {
this.name = name;
this.expression = expression;
}
public long calculate(long origin) {
return expression.apply(origin);
}
}
자바8 에서 Function Interface 와 BiFunction Interface 가 등장하면서 기존에 익명 함수를 통해 얻어야 했던 지저분한 코드는 사라지고 Enum 내에서도 관련 행위를 제어 할 수 있습니다. 이를 호출하는 곳에서는 아래와 같이 호출만 하면 되기에 결국 상태와 행위가 한 곳에서 관리할 수 있도록 되었다. 라고 볼 수 있습니다.
class SaleMoneyTest {
@Test
void sales() {
long originMoney = 10000L;
assertThat(
SaleMoney.ORIGIN.calculate(originMoney)
).isEqualTo(10000L);
assertThat(
SaleMoney.VAT.calculate(originMoney)
).isEqualTo(10000L / 11);
assertThat(
SaleMoney.SUPPLY.calculate(originMoney)
).isEqualTo(10000L * 10 / 11);
assertThat(
SaleMoney.NOTHING.calculate(originMoney)
).isEqualTo(0L);
}
}
특정한 값에 대해서 다양한 연산이 필요한 부분이 있다고 가정해 봅시다. 이런 경우 일반적으로 이를 호출하는 곳에서 다양한 평균값을 도출하는 경우가 많은데요, 이 경우에도 Enum을 사용하면 하나의 값에 대해 다양한 연산 로직을 한 곳으로 집중시킬 수 있습니다.
public enum Number {
AVG("평균", list -> {
return getSum(list) / list.size();
}),
SUM("합계", list -> {
return getSum(list);
});
private final String name;
private final Function<List<Integer>, Integer> expression;
Number(String name, Function<List<Integer>, Integer> expression) {
this.name = name;
this.expression = expression;
}
private static Integer getSum(List<Integer> list) {
return list.stream()
.reduce(Integer::sum)
.get();
}
public Integer calculate(List<Integer> list) {
return expression.apply(list);
}
public static void main(String[] args) {
System.out.println(Number.AVG.calculate(Arrays.asList(1,2,3,4,5)));
System.out.println(Number.SUM.calculate(Arrays.asList(1,2,3,4,5)));
}
}
// 실행결과 : 3, 15
위의 기능은 물론 따로 Enum으로 분리하지 않아도 문제가 없지만(단순히 설명을 위한 사례라고 생각해 주세요), 위와 같이 Enum을 사용하면 데이터에 관련한 연산들을 한 곳으로 모을 수 있다는 장점이 있습니다. 요약하자면 상수에 대해서 관련된 로직이 존재하는 경우 Enum내에서 관리함으로써 상태와 행위를 한 곳에서 관리할 수 있다는 장점이 생깁니다. 자바 8에 추가 된 Function
, BiFunction
등을 통해서 더 다양한 연산이 가능하기 때문에 상수가 나열되고 상수와 관련된 로직이 존재하는 경우 Enum
을 사용하면 좋을 것 같습니다.
Enum은 다양한 상수들을 그룹화 할 때도 유용하게 사용 될 수 있습니다. 다양한 상수값이 존재하고 이 상수를 특정한 기준에 맞게 각각 분류하고 싶은 경우에 많은 이점을 볼 수 있는데요 아래의 예시를 보며 이야기 해보겠습니다.
코딩과 관련되어 다양한 교육 및 취업 프로그램이 존재합니다. 다양한 상수값(프로그램 이름)을 교육 목적 및 취업 목적을 기준으로 분리하여 보겠습니다.
import java.util.Arrays;
import java.util.List;
public enum Program {
EDUCATION("교육용 프로그램", Arrays.asList("우아한 테크코스","삼성 소프트웨어","서울42","패스트 캠퍼스")),
FOR_JOB("취업용 프로그램", Arrays.asList("국비지원 아카데미","우아한 테크캠프","프로그래머스 관련"));
private final String name;
private final List<String> list;
Program(String name, List<String> list) {
this.name = name;
this.list = list;
}
public static Program findGroup(String name) {
return Arrays.stream(Program.values())
.filter(group -> find(group, name))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("없는 프로그램입니다."));
}
private static boolean find(Program group, String name) {
return group.list.stream()
.anyMatch(value -> value.equals(name));
}
public static void main(String[] args) {
System.out.println(Program.findGroup("우아한 테크코스"));
}
}
// 실행 결과 : EDUCATION (Program 타입으로 리턴됨)
위와 같이 특정한 기준에 의해 그룹화 되는 경우(내부 값은 상수일 때) Enum을 사용하면 가시성이 높아져 좋습니다. 뿐만 아니라 입력된 프로그램이 교육용인지 취업용인지 리턴하는 메소드 또한 같은 클래스로 들어가기 때문에 관련 행위를 함께 관리하고 있습니다.
서로 다르게 표기되지만 같은 의미를 나타내는 데이터들이 있습니다. 예를 들어 통과
라는 기준을 0 또는 1
혹은 true or false
, Y or N
등의 방식으로 사용 할 수 있습니다. 이 경우 Enum을 사용하여 관리하면 아래와 같은 장점이 있습니다.
통과
라는 기준에서 보았을 때는 모두 같은 의미입니다. 이를 명시적으로 표현 할 수 있습니다.PASS
라는 형태로 관리되기에 유지보수 및 협업에 도움이 됩니다. public enum Standard {
PASS("Y", true, "1"),
FAIL("N", false, "0");
private final String expression1;
private final boolean expression2;
private final String expression3;
Standard(String expression1, boolean expression2, String expression3) {
this.expression1 = expression1;
this.expression2 = expression2;
this.expression3 = expression3;
}
}
태양계의 여덟 행성은 열거 타입을 설명하기에 좋은 예이다. 각 행성은 질량과 반지름이 존재하고 이 둘을 사용하여 표면중력을 계산할 수 있다. 이 부분을 Enum으로 구현하면 아래와 같다. 행성을 상수로 분리하고 각 행성마다 다른 질량과 반지름 표면중력을 함께 표현함으로써 관련 데이터를 하나의 클래스에서 관리하며 상태와 행위가 한 곳에 있다.
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // 질량(단위: 킬로그램)
private final double radius; // 반지름(단위: 미터)
private final double surfaceGravity; // 표면중력(단위: m / s^2)
// 중력상수(단위: m^3 / kg s^2)
private static final double G = 6.67300E-11;
// 생성자
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
위의 예제에서는 정적인 값에 대해서 Enum을 활용하는 부분이었다. 하지만 각 상수마다 다른 연산이 필요한 경우도 존재할 것이다. 아래의 예제는 상수마다 다른 연산을 하는 예제인데, 앞장에서 람다를 이용해 표현했던 것과 같은 방식으로 구현되어 있다. 다만 자바8 이하의 버전을 사용 할 경우 아래와 같이 추상메서드를 정의하고 익명함수를 통해 구현할 수 있다.
import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;
// 코드 34-6 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입 (215-216쪽)
public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
public abstract double apply(double x, double y);
// 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽)
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
각 요일 별 시급과 초과근무에 대한 일급을 어떻게 계산하면 좋을까? 가장 깔끔한 방법은 새로운 상수를 추가할 때 잔업수당 전략
을 선택하는 방법이다. 잔업수당 계산을 PayType
으로 옮기고 PayrollDay 에서 적절한 타입을 선택한다. 이 후 계산 하는 부분을 전략 열거타입으로 위임한다면 매 경우 달라지는 부분에 대해서 Switch 분기를 타지 않고 사용이 가능 할 것이다.
import static effectivejava.chapter6.item34.PayrollDay.PayType.*;
// 코드 34-9 전략 열거 타입 패턴 (218-219쪽)
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
SATURDAY(WEEKEND), SUNDAY(WEEKEND);
// (역자 노트) 원서 1~3쇄와 한국어판 1쇄에는 위의 3줄이 아래처럼 인쇄돼 있습니다.
//
// MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY,
// SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);
//
// 저자가 코드를 간결하게 하기 위해 매개변수 없는 기본 생성자를 추가했기 때문인데,
// 열거 타입에 새로운 값을 추가할 때마다 적절한 전략 열거 타입을 선택하도록 프로그래머에게 강제하겠다는
// 이 패턴의 의도를 잘못 전달할 수 있어서 원서 4쇄부터 코드를 수정할 계획입니다.
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
// PayrollDay() { this(PayType.WEEKDAY); } // (역자 노트) 원서 4쇄부터 삭제
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// 전략 열거 타입
enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 :
(minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
public static void main(String[] args) {
for (PayrollDay day : values())
System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
}
}
저는 사례들을 접했을 때 이런 생각을 했습니다. 상태와 행위를 한 곳에서 담당하기 위해서라면 클래스로 분리하면 되지 않을까? 그리고 그 클래스를 상속받는 형태로 사용하면 되는 것 아닐까? ← Enum 을 사용하면서 기존에 했던 생각에 대해서 제 개인적인 견해는 다음과 같습니다.
pacakge
를 추가함으로써 클래스 다이어그램이 복잡해진다.즉 요약하자면 불 필요한 상속을 사용해야 하며 가시성 또한 떨어지기 때문에 Enum을 활용하는 것이 좋다고 생각합니다. 추가적으로 익명함수를 통한 처리도 자주 사용되는 경우 중복의 이슈를 발생시킬 수 있지만 중복되어 사용되는 경우 메소드 분리 혹은 Util성 클래스 등을 통해 처리 할 수 있기 때문에 Enum 자체 문제가 되지는 않는 것 같습니다. 위 의견은 개인적인 생각이므로 정확한 이유나 다른 이유가 있다면 알려주시면 감사하겠습니다.
Enum을 적절히 활용하면 Type safe 한 코딩에서 가시적인 부분 등 많은 부분에서 이점을 얻을 수 있습니다.
🧇 잘 봤습니다. 많은 도움이 되었어요!!