본문 바로가기
Java

Item34. int 상수 대신 열거 타입을 사용하라.

by 쁘니쁘나 2022. 6. 5.

 

자바에서 열거 타입이 생기기 전까지는 정수 상수를 열거하는 방법을 사용하였다.

 

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;

🚫 위와 같은 방식의 단점

  • 타입 안전 보장 X
  • 컴파일러가 이해하는 값은 정수이므로 ORANGE_NAVEL 을 건네야 하는 곳에 APPLE_FUJI를 보내고 동등연산자로 비교하더라도 컴파일러는 틀린것을 모름.
  • 상수의 값이 바뀌면 클라이언트도 다시 컴파일 해야함.
  • 문자열로 출력하기가 까다로움

 

하지만 열거타입을 사용하면 위의 단점을 해결할 수 있다❗

 

 

우선 열거타입이 무엇인지 알아보자.

 

열거타입이란??

서로 연관된 상수의 집합을 저장하는 자료형이다.
요일을 나타내는 월, 화, 수, 목, 금, 토, 일과 계절을 나타내는 봄, 여름, 가을, 겨울도 예로들 수 있다.

그럼 아까 위에서 나왔던 예제를 열거타입으로 바꿔보자.

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

훨씬 간결해 진것을 알 수 있다.

열거타입의 특징을 알아보자.

  • 열거타입 자체는 클래스 이다.
  • 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
  • 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final 이다.
  • 컴파일타임 타입 안정성을 제공한다. ( Apple 열거타입을 매개변수로 받는 메서드를 선언했다면, Apple의 세가지 값 중 하나임이 확실하다. )

 

여기서 끝이 아니라 열거 타입에는 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수도 있다.

 

열거 타입에 메서드나 필드를 추가하는건 뭘까?

 

아래는 열거 타입에 메서드와 필드를 추가한 예이다.

public enum Planet {
	MERCURY(3.302e+23, 2.439e6),
	VENUS (4.869e+24, 6.052e6),
	EARlli ( 5 • 975e+24, 6 • 378e6 ) ,
	MARS (6.419e+23, 3,393e6),
	JUPITER( l. 899e+27, 7.149e7 ) ,
	SATIJIIII (5,685e+26, 6,027e7),
	URANUS (8.683e+25, 2,556e7),
	NEPT1.NE ( l. 024e+26, 2 • 477e7 ) ;

	private final double mass; II 질량(단위: 킬로그램)
	private final double radius; II 반지롤(단위: 미터)
	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 I ( radius * radius ) ;
	}

	publie double mass ( ) { retu rn mass ; }
	public double radius( ) { return radius; }
	public double surfaceGravity( ) { return surfaceGravity; }

	public double surfaceWeight(double mass) {
		return mass * surfaceGravity; II F = ma
	}
}

 

위의 Planet 열거 클래스에서 어떤 객체의 지구에서의 무게를 입력받아 여덟 행성에서의 무게를 출력하는 것을 출력해보자.

public class WeightTable {
	public static void main(String 11 args) {

		double earthWeight = Double. parseDouble(args [0] ) ;
		double mass = earthWeight / Planet. EARTH. surfaceGravity() ;

		for (Planet p : Planet.values())
			System.out.printf("%s에서의 무게는 합이다.%11",p, p.surfaceWeight(mass));
	}
}

 

✅ 출력결과

MERCURY'에서의 무게는 69.912739이다.
VENUS에서의 무게는 167.434436이다.
EAR뼈人1의 무게는 185.00000001다.
MARS에서의 무게는 70.226739<시다.
JUPITER에서의 무게는 467.990696이다.
SAlURI떠서의 무게는 197.120111이다.
URPNUS에서의 무게는 167.398264이다.
NEPlUNE에서의 무게는 210.208751이다.

 

위 예제에서 알 수 있듯이 열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드인 values 를 제공한다. 값들은 선언된 순서로 저장되며 각 열거 타입 값의 toString 메서드는 상수 이름을 문자열로 반환하므로 println과 printf 로 출력하기에 딱 좋다.

 

근데 만약 위의 열거타입에서 상수를 하나 제거하면??

➡️ 제거한 상수를 참조하지 않는 클라이언트에는 아무 영향이 없다.

 

 

Planet 예에서 보여준 특성만으로 열거 타입을 사용하는 상황 대다수를 훌륭히 설명할 수 있다. 하지만 만약 상수마다 동작이 달라져야 하는 상황에는 어떻게 해야할까?

우선 switch 문을 이용해 상수의 값에 따라 분기하는 방법을 시도해보자.

 

열거 타입의 상수 별 메서드 구현

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

동작은 하겠지만 깨지기 쉬운코드이다.

새로운 상수를 추가하면 해당 case 문도 추가해야 하며, 혹시라도 추가를 깜빡하면 컴파일은 되지만 새로 추가한 연산을 수행하려 할 때 “알 수 없는 연산” 이라는 런타임 오류를 내며 프로그램이 종료된다.

다행히 열거 타입은 상수별로 다르게 동작하는 코드를 구현하는 더 나은 수단을 제공한다.

 

상수별 메서드 구현

열거타입에 apply라는 추상 메서드를 선언하고 각 상수에서 자신에 맞게 재정의하는 방법.

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

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

}

✅ apply 메서드가 상수 선언 바로 옆에 붙어 있으니 새로운 상수를 추가 할 때 apply 도 재정의해야 한다는 사실을 잊기 어렵다.

✅ apply 가 추상 메서드이므로 재정의하지 않았다면 컴파일 오류로 알려준다.

이제 상수별 메서드 구현을 상수별 데이터와 결합하는 방법을 보자.

 

상수별 클래스 몸체와 데이터를 사용한 열거 타입

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 ( III" ) {
		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);

}

 

위의 예제가 얼마냐 편하냐면

public static void main(String [] args) {

	double x = Double.parseDouble (args[0]);  // 2
	double y = Double.parseDouble(args[1]);  // 4

	for (Operation op : Operation.values())
		System.out.printf("%f %s %f = %f%n", x, op, y, op. apply(x, y));

}

아래를 출력하기위해 위의 간단한 코드이면 된다.

 

✅ 실행결과

2.000000 + 4.000000 = 6.000000
2.000000 - 4.000000 = -2.000000
2.000000 * 4.000000 = 8.000000
2.000000 / 4.000000 = 0.500000

 

마지막으로 전략 열거 타입 패턴을 알아보자. 전략 열거 타입 패턴은 아래와 같은 경우를 해결하기 위해 등장했다.

 

enum PayrollDay {
	MONDAY, TUESDAY, WECNESDAY, THURSDAY, FRIDAY, SATURDAY, SLNDAY;

	private static final int MINS_PER_SHIFT = 8 * 60;

	int pay(int minutesWorked, int payRate) {
		int basePay = minutesWorked * payRate;
		int overtimePay;

		switch(this) {
			case SATlltDAY : case SUNDAY : // 주말
			overtimePay = basePay / 2;
			break;
			default : // 주중
				overtimePay = minutesWorked <= MINS_PER_SHIFT ? 0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
		}
		return basePay + overtimePay;
	}
}

➡️ 분명 간결하고 정상으로 보지이지만 위험한 코드다. 휴가와 같은 새로운 값을 열거 타입에 추가하려면 그 값을 처리하는 case 문을 쌓으로 넣어줘야 한다. 안그러면 휴가 기간에 열심히 일해도 평일과 똑같은 임금으로 처리되는 불상사가 나타난다!!

 

이를 해결하기위한 전략 열거 타입 패턴을 알아보자.

 

전략 열거 타입 패턴

enum PayrollDay {

	MONDAY, TUESDAY, WE마JESDAY, THURSDAY, FRIDAY,
	SATURDAY(PayType.WEEKEND), SUNDAY(PayType.WEEKEND);

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

	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;
			},
		}
		WEEKBIID {
			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(minsWorkecl, payRate) ;
		}
	}
}

➡️ 잔업수당 계산을 PayType 으로 옮기고 PayrollDay 열거 타입의 생성자에서 이 중 적당한 것을 선택한다. 그러면 PayrollDay 열거 타입은 잔업수당 계산을 그 전략 열거 타입에 위임하여, switch 문이나 상수별 메서드 구현이 필요 없게 된다. 복잡하지만 switch 문보다 안전하고 유연하다.

 

 

 

핵심정리❗❗

열거 타입은 확실히 정수 상수보다 뛰어나다. 더 읽기 쉽고 안전하고 강력하다.
대다수 열거 타입이 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작하게 할 때는 필요하다.
드물게는 하나의 메서드가 상수별로 다르게 동작해야 할 때도 있다. 이런 열거 타입에서는 switch 문 대신 상수별 메서드 구현을 사용하자. 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.

 

 

 

 

참고


도서 - Effective Java

https://codingwell.tistory.com/116

댓글