개발하는 두더지

[Effective Java 규칙30] int 상수 대신 enum을 사용하라 본문

Java,Android

[Effective Java 규칙30] int 상수 대신 enum을 사용하라

덜지 2018. 10. 8. 17:08

[Effective Java 규칙30] int 상수 대신 enum을 사용하라

Effective Java 2/E 책과 구글링을 통해 내용을 정리하고 개인적인 견해가 포함된 글입니다.


자바 1.5에서 enum 이라는 새로운 참조 자료형이 추가되었다. enum이 등장하기 전까지는 int 형 상수들을 정의해서 enum을 흉내냈다.

public static final int APPLE = 1;
public static final int ORANGE = 2;


enum은 C++, C# 같은 언어에서 제공하는 enum 자료형과 비슷하지만 자바의 enum은 완전한 기능을 갖춘 클래스로, 다른 언어의 enum보다 강력하다. enum 자료형은 컴파일 시점 형 안전성(compile-time type safety)을 제공한다. Apple 형의 인자를 받는다고 선언한 메서드는 반드시 Apple 값 3개중 하나만 인자로 받는다. 다른 타입의 자료형을 인자로 전달하려하면 컴파일할 때 오류가 발생한다.

enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
enum Orange { NAVEL, TEMPLE, BLOOD }


int형으로 enum을 흉내내는 int enum 패턴의 문제점을 해결하는 것뿐만 아니라 enum 자료형은 메서드나 필드도 추가할 수 있고, Comparable 인터페이스, Serializable 인터페이스가 구현되어있다.

public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {


왜 enum 자료형에 메서드나 필드를 추가할까?

필요해 보이는 메서드를 추가시켜 enum 상수 묶음에서 점차 완전한 기능을 갖추는 추상화 단위로 업데이트를 해나갈 수 있기 때문이다.


아래는 enum을 이용하여 태양계의 8개 행성을 모델링하는 샘플 코드이다.

각 행성은 질량과 반지름 정보를 가지며, 이 두 속성으로부터 중력을 계산할 수 있다.

enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(1, 1),
EARTH(2, 2),
MARS(3, 3),
JUPITER(4, 4),
SATURN(5, 5),
URANUS(6, 6),
NEPTUNE(7, 7);

private final double mass;
private final double radius;
private final double surfaceGravity;

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;
}
}


enum은 원래 변경 불가능(immutable)하므로 모든 필드는 final로 선언되어야 한다(규칙15). 어떤 물체의 지표면상 무게를 입력받아서 모든 8개 행성 표면에서 측정한 무게로 변환을 간단하게 할 수 있다.

public static void main(String[] args) {
double earthWeight = 10L;
double mass = earthWeight / Planet.EARTH.surfaceGravity();
for(Planet p : Planet.values()) {
System.out.printf("Weight on %s is %f\n", p, p.surfaceWeight(mass));
}
}


또 다른 예로, 기본적인 4가지 산술 연산을 표현하는 enum 자료형을 만든다면 아래와 같다.

enum Operation {

PLUS("+") { double apply(double x, double y) { return x + y; }},
MINUS("-") { double apply(double x, double y) { return x - y; }},
TIMES("*") { double apply(double x, double y) { return x * y; }},
DEVIDE("/") { double apply(double x, double y) { return x / y; }};

private final String symbol;
private static final Map<String, Operation> stringToEnum
= new HashMap<>();

// toString으로 뱉어내는 문자열을 다시 enum 상수로 변환하기위해 저장
static {
for(Operation op : values()) {
stringToEnum.put(op.toString(), op);
}
}

Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }

// String to Enum
public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
}

abstract double apply(double x, double y);
}

public static void main(String[] args) {
double x = 10L;
double y = 15L;
for(Operation op : Operation.values())
System.out.printf("%f %s %f = %f\n", x, op.toString(), y, op.apply(x, y));
}



급여 명세서에 찍히는 요일을 표현하는 Enum 자료형과 직원의 시급과 해당 요일에 일한 시간을 인자로 주면 해당 요일의 급여를 계산하는 메서드가 있다고 하자. 주중에는 초과근무 시간에 대해서만 초과근무 수당을 주고, 주말에는 전부 초과근무 수당을 처리해야 한다.

이때 유지보수가 간결하고, 안전하며, 헷갈리지 않고, 코드가 중복되지 않고, 가독성이 좋으며, 오류 발생 확률이 낮아지게 작성한 코드는 아래와 같다.


enum PayrollDay {

MONDAY(PayType.WEEKDAY), TUESDAY(PayType.WEEKDAY), WEDNESDAY(PayType.WEEKDAY),
THURSDAY(PayType.WEEKDAY), FRIDAY(PayType.WEEKDAY), SATURDAY(PayType.WEEKEND),
SUNDAY(PayType.WEEKEND);

private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }

double pay(double hoursWorked, double payRate) {
return payType.pay(hoursWorked, payRate);
}

private enum PayType {
WEEKDAY {
double overtimePay(double hours, double payRate) {
return hours <= HOURS_PER_SHIFT ? 0 :
(hours - HOURS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
double overtimePay(double hours, double payRate) {
return hours * payRate / 2;
}
};

private static final int HOURS_PER_SHIFT = 8;
abstract double overtimePay(double hrs, double payRate);

double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
}

새로운 enum 상수를 추가해도 초과근무 수당 계산 정책을 반드시 선택하게 되어있다. 전략패턴처럼 구현되어 있다.



일반적으로 enum은 int 상수와 성능면에서 비등하다. 자료형을 메모리에 올리고 초기화하는 공간적/시간적 비용 때문에 약간 손해보긴하지만 요즘 디바이스에서는 아무 문제가 없다.

요약하자면, enum을 사용한 코드는 int 상수를 열거한것보다 가독성도 높고, 안전하고, 더 강력하다. 상당수의 enum은 생성자나 멤버가 필요없지만 메서드나 멤버필드를 추가해서 기능을 향상시키는 것도 좋은 방법이다. 여러 enum 상수가 공통 기능을 이용하는 일이 생길 때 마지막 예제인 정책 enum 패턴 사용을 고려해보면 좋을 것 같다.
















Comments