Java를 배울 때 클래스, 인터페이스 뿐만 아니라 enum이라고 하는 것을 배우게 된다. 클래스야 객체 지향에서 객체를 애초에 만드는 가장 대표적인 '설계도'이기에 당연히 중요하고, 인터페이스도 다형성의 관점에서 필요하다는 것도 알겠는데, 처음에 enum은 왜 쓰는지 몰랐다. 그냥 상수 모아둔 것 아닌가 했다.
하지만, 우아한형제들 기술블로그를 보고 아 이렇기 때문에 쓰는 거구나 했다. 이 포스팅에서는 enum이 뭔지, 이걸 왜 쓰는건지 저 블로그 포스팅을 기준으로 작성한다.
그리고 이를 활용해서 저의 자바 미니 프로젝트에 어떻게 적용하였는지 정리하였다.
오라클 문서에 따르면, 미리 정의된 상수들의 특별한 집합이라고 생각하면 된다. 사실 영단어의 의미를 봐도 짐작할 수 있는게, enum(enumeration) 의 뜻을 열거라는 뜻이다. 즉 상수들을 열거한 것이 바로 enum이다.
기본적으로 enum에 열거된 상수들은 추가적인 객체 생성 없이 외부에서 사용가능하고(enum 내부 메소드와는 별개로), 불변이기에 상수들 앞에 아무것도 안붙지만 public static final
이다. 그리고 기본적으로 final
인 상수이기에 모두 대문자로 적는 것을 원칙으로 한다.
아래는 가장 기본적인 enum 작성 방법이다.
public enum Day {
SUNDAY, MONDAY, TUESDAY, WEDNESDAY,
THURSDAY, FRIDAY, SATURDAY;
}
열거시에는 콤마(,)로, 끝날 시에는 세미콜론(;)으로 마무리한다.
또한 다른 클래스들처럼 생성자를 만들 수도 있다. 아래와 같은 예시를 보도록 하자.
public enum Planet {
MERCURY (3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7);
private final double mass; // in kilograms
private final double radius; // in meters
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
}
private double mass() { return mass; }
private double radius() { return radius; }
// universal gravitational constant (m3 kg-1 s-2)
public static final double G = 6.67300E-11;
double surfaceGravity() {
return G * mass / (radius * radius);
}
double surfaceWeight(double otherMass) {
return otherMass * surfaceGravity();
}
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("Usage: java Planet <earth_weight>");
System.exit(-1);
}
double earthWeight = Double.parseDouble(args[0]);
double mass = earthWeight/EARTH.surfaceGravity();
for (Planet p : Planet.values())
System.out.printf("Your weight on %s is %f%n",
p, p.surfaceWeight(mass));
}
}
Your weight on MERCURY is 66.107583
Your weight on VENUS is 158.374842
Your weight on EARTH is 175.000000
Your weight on MARS is 66.279007
Your weight on JUPITER is 442.847567
Your weight on SATURN is 186.552719
Your weight on URANUS is 158.397260
Your weight on NEPTUNE is 199.207413
코드에서 상수(질량, 반지름)
의 형태로 생성자를 만든 것을 알 수 있다. 이를 이용해서 만유인력에 의한 중력가속도 surfaceGravity
와 무게 = 질량 * 중력가속도
라는 공식으로 사용자가 main()
아규먼트에 double
형태로 숫자를 입력하면 행성별 사용자의 무게를 출력하는 것을 확인할 수 있다.
그리고, 이 코드에서 각 상수들을 배열과 같이 차례대로 열거해서 사용하고 싶다면 .values()
메소드를 사용하면 되는 것 역시 알 수 있다(그리고 이 메소드는 배열을 반환하는 것이 맞다). main()
에서 for-each
문을 보면 확인할 수 있다.
그러면 이러한 열거형 enum은 왜 쓰는걸까?
우아한형제들 기술블로그로 가보자. 이 포스팅에 따르면 enum을 사용하는 이유는 크게 다음과 같다.
- 문자열과 비교해, IDE의 적극적인 지원을 받을 수 있다.
- 자동완성, 오타검증, 텍스트 리팩토링 등등
- 허용 가능한 값들을 제한할 수 있다.
- 리팩토링시 변경 범위가 최소화 된다.
- 내용의 추가가 필요하더라도, Enum 코드외에 수정할 필요가 없다.
이를 생각하며 쓰임새를 계속 알아보도록 하자.
예를 들어 다음과 같은 정산 시스템을 생각해보자.
public class LegacyCase {
public String toTable1Value(String originValue) {
if("Y".equals(originValue)) {
return "1";
} else {
return "0";
}
public String toTable2Value(String originValue) {
if("Y".equals(originValue)) {
return true;
} else {
return false;
}
}
}
여기서 문제는 받아보는 originValue
는 "Y", "N"으로 고정된 반면, table의 경우에는 종류별로 어떤 것은 "1","0", 어떤 것을 true, false 형태이기에, 메소드를 하나하나 다 만들어주는 "번거로움"이 존재한다.
또한 "Y" 나 "1"이나 true나 같은 의미인데 경우에 따라 어떤 값이 나오는지 확인하려면 일일히 하나하나 클래스 및 메소드 하나하나 다 찾아야 한다.
이를 개선하기 위해 enum으로 해당부분을 추출하고 enum에서만 관리를 한다면 어떨지 살펴보자.
public enum TableStatus {
Y("1", true),
N("0", false);
private String table1Value;
private boolean table2Value;
TableStatus(String table1Value, boolean table2Value) {
this.table1Value = table1Value;
this.table2Value = table2Value;
}
public String getTable1Value() {
return table1Value;
}
public boolean isTable2Value() {
return table2Value;
}
}
이렇게 관리하면 사용할 때 다음과 같이 깔끔하게 작성 가능하다.
String table1Value = TableStatus.getTable1Value();
boolean table2Value = TableStatus.isTable2Value();
이번에는 조금 더 간단한 사례를 보도록 하자. 예를 들어 간단한 사칙연산이 가능한 계산기를 만든다고 해보자.
enum이 없는 경우 다음과 같이 작성을 하였다.
public class LegacyOperation {
private String operator
public LegacyOperation(String operator) {
this.operator = operator;
}
public double apply(double a, double b) {
switch (operator) {
case "+":
return a + b;
case "-":
return a - b;
case "*":
return a * b;
case "/":
return a / b;
}
throw new AssertionError("Not handled operation : " + operator);
}
}
enum이 있는 경우 아래와 같이 작성도 가능하다. 참고로 Java 8부터는 문자열도 물론이지만 enum(열거형) 타입 역시 switch문으로 가능하다.
public enum Operation {
PLUS, MINUS, TIMES, DIVIDE;
public double apply(double a, double b) {
switch (this) {
case PLUS:
return a + b;
case MINUS:
return a - b;
case TIMES:
return a * b;
case DIVIDE:
return a / b;
}
throw new AssertionError("Not handled operation : " + this);
}
}
조금 더 직관적이긴 한데... 차이를 전혀 모르겠다. 이정도만 알면 enum은 왜쓰지 생각이 들지도 모르겠다. 이를 enum 생성자를 통해서 리펙토링 해보자.
사실 위 기능은 위와 같은 코드보다는 아래와 같이 각 메소드를 만들어 연산을 별도로 분리하는 것이 객체지향 관점에서는 조금더 맞다. 결국 메소드 하나당 복합기능이 아닌 한 기능만 수행하도록 하는 것이 좀 더 적합하기 때문이다.
public enum Operation {
PLUS {
@Override
public double apply(double a, double b) {
return a + b;
}
},
MINUS {
@Override
public double apply(double a, double b) {
return a - b;
}
},
TIMES {
@Override
public double apply(double a, double b) {
return a * b;
}
},
DIVIDE {
@Override
public double apply(double a, double b) {
return a / b;
}
};
// 추상 메소드를 활용한 상수별 메소드 구현
public abstract double apply(double a, double b);
}
그런데... 자바에는 람다 표현식이 있다. 이걸 통해 좀더 깔끔하게 작성해보면 아래와 같다.
(참고로 Java 8 부터는 enum에서 각각의 인자값으로 Function
을 이용한 람다 표현식으로 작성이 가능하다!)
public enum Operation {
PLUS ((a,b) -> a + b),
MINUS ((a,b) -> a - b),
TIMES ((a,b) -> a * b),
DIVIDE ((a,b) -> a / b);
private final BiFunction<Double,Double,Double> operation ;
Operation(BiFunction<Double,Double,Double> operation) {
this.operation = operation;
}
public Double compute(Double x, Double y) {
return operation.apply(x,y);
}
}
물론 Function
이나 BiFunction
같은 인터페이스에 의존해서 사용이 가능해서 완벽하게 독립적이진 않다. 참고로 두 인터페이스는 자바 8부터 사용이 가능한 인터페이스로 자세한 내용은 오라클 공식 문서(Function, BiFunction)을 참고하기 바란다.
이 부분의 핵심은 이것이다. Entity 클래스에 필드로 계산기가 필요한 경우 아래에 각각의 연산자 이름 String을 다 지정하는 것이 아닌 그냥 enum 하나만 지정해서 알아서 enum 상수들이 처리하도록 하면 된다.
@Column
@Enumerated(EnumType.STRING)
// enum
private Operation operation;
예를 들어 결제 시스템을 만든다고 가정해보자. 결제 데이터는 결제 종류와 수단 등 다양한 형태로 표현이 됩니다. 또 결제 수단이 카드로 똑같더라도 우리은행 체크카드인지 현대 신용카드인지 내부적으로도 갈립니다.
이러한 다양한 분류는 수많은 조건식과 조건문으로 마주하게 됩니다.
코드 길이적인 뿐만 아니라 만약 어떤 결제 수단이 사용되었는지, 그리고 해당 결제가 어떤 결제에 속하는지 파악을 그냥 조건문으로만 진행하게 된다면, 여러가지 문제가 발생한다.
이를 개선 하기 위해 enum을 사용한 접근법은 아래와 같다.
그런데... 순회는 어떻게 하지...?는 아래 문법을 참고하면 된다.
public static PayGroup findByPayCode(String code){
return
//PayGroup의 Enum 상수들을 순회하며
Arrays.stream(PayGroup.values())
//payCode를 갖고 있는게 있는지 확인한다.
.filter(payGroup -> payGroup.hasPayCode(code))
//다 찾아보고 없으면 EMPTY를 반환한다.
.findAny()
.orElse(EMPTY);
}
그런데 결제 종류 같은 대분류는 enum으로 해놓고 결제 수단 같은 경우 그냥 문자열이면 실수로 파라미터로 잘못 전달된 값이 관리가 전혀 되지 않습니다. 그레서 수단 역시도 enum으로 해야됩니다.
public enum PayType {
ACCOUNT_TRANSFER("계좌이체"),
...
}
해당 사항을 적용한 깃 커밋을 첨부하였다.
RDBMS 대신 메모장을 사용한 형태지만, 사용자가 본인 정보 수정을 할 때 enum으로 처리하였다.
package com.concert.mini.project.common;
import java.util.Arrays;
public enum UserInfo {
NAME("이름"),
ID("아이디"),
PASSWORD("비밀번호"),
EMAIL("이메일"),
PHONENUMBER("전화번호");
private String info;
UserInfo(String info) {
this.info = info;
}
public String getInfo() {
return this.info;
}
public static UserInfo getUserInfo(String info) {
return Arrays.stream(values()).filter(x -> x.getInfo().equals(info)).findAny().orElse(null);
}
}
그리고 UserController에서는 문자열로 입력받은 정보를 UserInfo 타입으로 변경하였다. (line 11~14)
...
public boolean edit(User user, String userInput, String info) {
UserInfo userInfo = UserInfo.getUserInfo(userInput);
return userDAO.editUser(user, userInfo, info);
}
...
컨트롤러와 연결된 UserDAOImpl에서는 UserInfo를 가지고 switch로 해당 정보를 찾아 수정하는 기능을 진행했다. (line 49~68)
...
@Override
public boolean editUser(User user, UserInfo userInfo, String info) {
switch (userInfo) {
case NAME:
user.setName(info);
break;
case PASSWORD:
user.setPassword(info);
break;
case EMAIL:
user.setEmail(info);
break;
case PHONENUMBER:
user.setPhoneNumber(info);
break;
default:
return false;
}
return saveUser();
}
...
확실히 코드가 직관적인 것을 알 수 있었고, 실수로 문자열을 잘못 입력하였다고 하더라도 없으면 switch문의 default로 들어가서 false 반환 후 오류 메시지를 출력하도록 하였기에 실수로 DB에 접근하는 일이 없게 된다.
물론 여기서도 조금 더 수정이 가능할 것으로 보인다. 아마 구현도 구현이지만 이런 약간 추상적인 개념들에 대해서도 생각을 해보면 더욱 더 지식이 쌓여 리펙토링하는데 더 용이하고, 애초에 구현할 때 부터 그렇게 생각해서 코드를 짜게 되지 않을까 한다.
기본적으로 enum은 type-safety하다. 그렇기에 어디선가 잘못된게 있을 때 바로바로 런타임에러 뜨기 전 컴파일러 차원에서 체크가 가능하다. 그리고 개발자 입장에서는 코드 가독성이 좋아진다. 일거양득의 효과인 것이다.
공부하면 공부할수록 알아야 할 것은 더 많아짐을 느끼며 포스팅을 마친다.