Skip to content

item 34 JihoonKim

hoonti06 edited this page Aug 8, 2020 · 4 revisions

[item34] int 상수 대신 열거 타입을 사용하다

정수 열거 패턴

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
  • 타입 안전하지 않다.
     APPLE_FUJI == ORANGE_NAVEL // 경고 메시지 하나 뜨지 않는다.
  • 포현력도 좋지 않다.
  • namespace를 지원하지 않아 접두어를 쓰는 등의 방법으로 이름 충돌을 방지해야 한다
  • 컴파일하면 그 값이 클라이언트 파일에 그대로 새겨진다.(JSL-13.1)
    • 상수의 값이 바뀌면 클라이언트도 반드시 다시 컴파일해야 함
  • 정수 대신 문자열 상수를 사용하는 변형 패턴(문자열 열거 패턴, string enum pattern)도 존재한다

열거 타입의 특징

  • 완전한 형태의 클래스(C, C++, C#의 enum과는 다르다)
  • 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다
  • 밖에서 접근할 수 있는 생성자를 제공하지 않아 사실상 final
  • 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없다
  • 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재
  • 컴파일타임 타입 안전성 제공
    • 매개변수로 건네받은 참조는 null 또는 해당 enum 중 하나(다른 타입일 경우 컴파일 에러)
  • namespace 존재하여 이름이 같은 상수도 공존 가능
  • 필드의 이름만 공개되기 때문에 enum에 상수 추가나 순서 변경이 있더라도 다시 컴파일하지 않아도 된다.
  • enum의 toString()은 출력에 적합한 문자열을 내준다.
  • 임의의 메서드나 필드를 추가할 수 있고, 임의의 인터페이스를 구현하게 할 수도 있다.

데이터와 메서드를 갖는 열거 타입

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.447e7);

	// 모든 field final
	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;
		this.surfaceGravity = G * mass / (radius * radius);
	}

	public double surfaceWeight(double mass) {
		return mass * surfaceGravity;
	}
}
  • 열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
  • 모든 field는 final이어야 한다. (참고로, field를 private으로 두고 public 접근자 메서드를 두는 게 낫다)
  • 자신 안에 정의된 상수들의 값을 배열에 담아(인스턴스 배열) 반환하는 정적 메서드 values()를 제공한다. (선언된 순서)
  • 상수 이름을 입력받아 그 이름에 해당하는 상수(인스턴스)를 변환해주는 valueOf(String)이 자동 생성된다.
  • 각 열거 타입 값의 toString()은 상수 이름을 문자열로 반환한다
  • 상수가 제거되면 해당 상수를 참조하고 있던 클라이언트는 컴파일할 때 오류가 발생할 것이다.(정수 열거 패턴에서는 이런 걸 기대할 수 없다)

상수별 메서드 구현

// 좋지 않은 형태
public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;
	
	// 상수가 뜻하는 연산 수행
	public double apply(double x, double y) {
		switch(this) {
			case PLUS:   return x + y;
			case MINUS:  return x - y;
			case TIMES:  return x * y;
			case DIVIDE: return x / y;
		}
		throw new AssertionError("알 수 없는 연산: " + this);
	}
}
  • throw문에 도달할 일이 없지만 생략하면 컴파일되지 않는다.
  • 상수가 새로 추가되었을 때 case문이 추가되지 않으면 런타임에 오류가 발생한다.
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; }
    public abstract double apply(double x, double y);
	@override public String toString() { return 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));
}
  • apply()가 추상 메서드라서 재정의하지 않으면 컴파일 에러가 발생한다.
  • 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.

전략(strategy) 열거 타입 패턴

// 좋지 않은 형태
enum PayrollDay {
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

	private static final int MINS_PER_SHIFT = 8 * 60;

	int pay(int minutesWorked, int payRate) {
		int basePay = minutesWorked * payRate;
		int overtimePay;
		switch(this) {
			case SATURDAY: case SUNDAY: // 주말
				overtimePay = basePay / 2;
				break;
			default: // 주중
				overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : 
					(minutesWorked - MINS_PER_SHIFT) * payRate / 2;
		}
		return basePay + overtimePay;
	}
}
  • 간결하나, 관리 관점에서 위엄한 코드
  • 새로운 값을 enum에 추가하려면 case문을 잊지 말고 쌍으로 넣어줘야 하는 것
  • 상수별 메서드 구현은 코드가 장황해져 가독성이 크게 떨어지고 오류 발생 가능성이 높다
// 전략(strategy) 열거 타입 패턴 적용
enum PayrollDay {
	MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), 
	THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
	
	SATURDAY(WEEKEND), SUNDAY(WEEKEND);

	private final PayType payType;

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

	int pay(int minutesWorked, int payRate) {
		return payType.pay(minutesWorked, payRate);
	}

	// nested class
	enum PayType {
		WEEKDAY {
			int overtimePay(int minutesWorked, int payRate) {
				return minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
			}
		},

		WEEKEND {
			int overtimePay(int minutesWorked, int payRate) {
				return minutesWorked * payRate / 2;
			}
		};

		abstract int overtimePay(int minutesWorked, int payRate);
		private static final int MINS_PER_SHIFT = 8 * 60;

		int pay(int minutesWorked, int payRate) {
			int basePay = minutesWorked & payRate;
			return basePay + overtimePay(minutesWorked, payRate);
		}
	}
}
  • 잔업 수당 계산을 private 중첩 enum 타입인 PayType에 위임한다.
  • switch문보다는 복잡하지만 더 안전하고 유연하다

switch문을 이용한 기능 수행

// Thirdparty에서 가져온 Operation 열거 타입을 이용해야 할 때
public static Operation inverse(Operation op) {
	switch(op) {
		case PLUS:   return Operation.MINUS;
		case MINUS:  return Operation.PLUS;
		case TIMES:  return Operation.DIVIDE;
		case DIVIDE: return Operation.TIMES;
		
		default: throw new AssertionError("알 수 없는 연산: " + op);
	}
}
  • switch문이 열거 타입의 상수별 동작을 구현하는 데 적합하지 않으나, 상수별 동작을 혼합에 넣을 때는 좋은 선택이 될 수 있다.

핵심 정리

  • 열거 타입이 정수 상수보다 더 읽기 쉽고 안전하고 강력하다
  • 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작하게 할 수 있다.
  • 하나의 메서드가 상수별로 다르게 동작해야 할 때는 switch문 대신 상수별 메서드 구현을 사용하자
  • 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자
  • 필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자
    • E.g. 태양계 행성, 체스 말, 메뉴 아이템, 연산 코드, 명령줄 플래그 등
    • 정의된 상수 개수가 영원히 불변일 필요는 없다. (상수 추가돼도 바이너리 호환 가능)

참고

Clone this wiki locally