티스토리 뷰
* 이펙티브 자바 2/E를 읽고 공부하기 위해 기록한 게시글입니다.
30. int 상수 대신 enum을 사용하라
열거타입(enumerated type)은 고정 개수의 상수들로 값이 구성되는 자료형이다.
enum 자료형이 등장하기 이전에는 통상 int형의 상수를 정의해서 열거타입을 흉내냈다.
public static final int APPLE_FUJI = 1;
public static final int APPLE_PIPPIN = 2;
public static final int ORANGE_NAVEL = 1;
public static final int ORANGE_BLOOD = 2;
이러한 방법은 int enum 패턴이라고도 하는데, 형 안전성 관점에서도 그렇고 편의성 관점에서도 그렇고 문제점들이 있다.
- int enum 그룹별로 별도의 namespace를 만들어주지 않기 때문에 앞에 접두어를 따로 붙여줘야 한다
(APPLE_, ORANGE_)
- ORANGE_를 인자로 받을 메서드에 APPLE_을 인자로 넘겨도 문제없이 동작한다. 의도와는 다르게 동작.
- 컴파일 시점 상수이기 때문에 상수를 사용하는 클라이언트 코드와 함께 컴파일된다. 상수값이 변경되면 클라이언트도 다시 컴파일 해야한다.
-> 프로그램이 깨지기 쉽다.
이러한 문제점들을 해결하기 위해 바로 등장한 것이 enum 자료형.
public enum Apple {
FUJI, PIPPIN;
}
public enum Orange {
NAVEL, BLOOD;
}
- 자바의 enum 자료형은 완전한 기능을 갖춘 클래스로, 다른 언어의 enum보다 강력하다.
- enum의 기본적 아이디어는, 열거 상수별로 인스턴스를 만들어 public static final 필드 형태로 제공하는 것이다.
- enum 자료형은 사실상 final으로 선언된 것이나 마찬가지인데, 접근 가능한 생성자가 아예 없기 때문이다.
-> enum 자료형으로 새로운 객체를 생성하거나, 상속을 통한 확장을 할 수 없기 때문에 이미 선언된 enum 상수 이외의 객체는 사용할 수 없다.
= enum 자료형의 개체 수는 엄격히 통제된다.
= 사실상 싱글톤과 같다. 아니 사실상이 아니고 그냥 싱글톤이다.싱글톤을 일반화한 형태.
(enum 자료형이 동일성 비교(==)가 가능한 이유가 서로 싱글톤 객체이기 때문)
* enum 자료형은 컴파일 시점 형 안전성을 보장한다.
- Apple 형의 인자를 받는다고 선언한 메서드는 반드시 Apple 자료형에 속한 값만 받는다. 다른 자료형을 넣으려고 하면 컴파일 시점에서 오류가 발생한다. (== 비교도 마찬가지)
* enum 자료형은 같은 이름의 상수가 평화롭게 공존할 수 있도록 해준다.
- namespace가 각각 분리되기 때문이다. (APPLE.VAL, ORANGE.VAL 같은 이름의 상수 동시 존재 가능)
* enum 자료형은 toString 메서드를 호출하면 문자열로 쉽게 변환할 수 있다.
(출력 형식은 toString을 재정의하면 바꿀 수 있다)
** toString으로 뱉어내는 문자열을 가지고 다시 enum 객체로 변환할 수 있는 fromString 메서드 제공을 고민해봐라
* enum 자료형은 임의의 메서드나 필드도 추가할 수 있고, 심지어 임의의 인터페이스를 구현할 수도 있다.
- enum 자료형은 이미 Object에 정의된 모든 메서드들이 포함되어 있으며, Comparable, Serializable 인터페이스는 이미 구현이 되어있다.
enum 자료형에 메서드나 필드를 추가하려는 이유는 상수에 데이터를 연계시켜 응집도를 높이기 위함
- 상수에서부터 시작해서 점차 완전한 기능을 갖춘 추상화 단위까지 진화할 수 있다.
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS(4.869e+24, 6.052e6),
EARTH(5.975e+24, 6.378e6);
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;
this.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 상수에 데이터를 넣기 위해서는 객체 필드를 생성하고 생성자를 통해 받은 데이터를 필드에 저장하면 된다.
(** 외부에서 접근해서 새로운 enum 인스턴스를 만드는 생성자가 아니다 **)
- enum은 원래 변경 불가능하므로, 모든 필드는 final으로 선언되어야 한다.
- 필드는 public 보다는 private로 선언한 다음 접근자 메서드를 통해 접근하는 것이 낫다.
enum 상수에 가능한 연산 중에선 enum이 정의된 클래스나 패키지 안에서만 사용되어야만 하는 것이 있을 수 있다.
- 그런 연산은 private나 default 메서드로 선언하는 것이 좋다.
- 이외에도 굳이 클라이언트에게 공개할 필요가 없는 메서드는 다 private로 닫아놔라.
웬만한 경우에는 저 Planet enum 객체 정도 수준만으로도 충분하다.
하지만 더 많은 기능을 원할 수도 있다. 상수들이 제각기 다른 방식으로 동작하도록 만들어야 한다면?
-> enum 자료형에 abstract 메서드를 선언하고, 각 상수별 클래스 몸체 안에서 각각 재정의하도록 만들 수 있다.
(이와같은 메서드를 상수별 메서드 구현이라고 부른다)
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);
private static final Map<String, Operation> stringToEnum = new HashMap<>();
static { // static 초기화 블록
for (Operation op : values()) {
stringToEnum.put(op.toString(), op);
}
}
@Override
public String toString() {
return symbol;
}
// toString으로 뱉어낸 객체를 가지고 다시 enum 객체를 만들어내는 메서드
public static Operation fromString(String symbol) {
return stringToEnum.get(symbol);
}
}
* enum의 static 초기화 블럭은 상수가 만들어지고 난 다음에 실행된다.
(+) 왜 그럴까? 원래 순서는 static 초기화 블록 -> 인스턴스 초기화 블록 -> 생성자 아닌가?
"static initialization occurs top to bottom."
static 초기화는 위에서부터 아래로 순차적으로 진행된다.
그리고 enum 상수들은 암묵적으로(implicitly) static final로 선언되어 있음.
-> 그렇기 때문에 맨 앞에 위치한 상수들이 뒤에 위치한 static 초기화 블록보다도 먼저 초기화를 진행하는 것.
결국 대단한게 아니고 선언 위치의 차이일 뿐이었던 것이다.
코드를 위에서 아래로 싹 훑으면서 static 초기화를 먼저 싹 하고
-> 다시 훑으면서 인스턴스 초기화를 싹 하고
-> 다시 훑으면서 생성자를 순서대로 실행하고 이런 동작방식 이였던 것이다.
이는 enum에서 뿐만 아니라 일반 클래스에서도 동일하다! 와 대박!
enum 자료형에는 자동으로 생성된 valueOf(String) 메서드가 있는데, 이 메서드는 상수의 이름을 통해서 상수 그 자체로 변환하는 역할을 한다. (상수의 이름에 해당하는 상수가 없다면 IllegalArgumentException 발생)
Operation plus = Operation.valueOf("PLUS");
System.out.println(plus); // Output: +
Operation none = Operation.valueOf("none"); // Exception
System.out.println(none);
enum에 단점이 있다면, enum 상수끼리 공유하는 코드를 만들기가 어렵다는 것이다.
* enum 타입 속에 enum 타입을 넣은 중첩 enum 타입을 만들 수 있다
- 이를 통해 내부에 전략 enum 상수를 만들고 외부의 상수가 전략을 선택해서 사용하도록 만들 수 있다.
public 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);
}
// 내부 정책 enum타입
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 hours, double payRate);
double pay(double hoursWorked, double payRate) {
double basePay = hoursWorked * payRate;
return basePay + overtimePay(hoursWorked, payRate);
}
}
}
(결론)
enum 타입은 단순히 상수를 사용하는 것에 비해 많은 장점을 갖고있다.
- 더 강력하고, 가독성도 높고, 안전하고, 데이터에 관계된 메서드를 추가해서 기능을 향상시킬 수도 있다.
'JAVA' 카테고리의 다른 글
Override 애노테이션을 일관되게 붙여라 (0) | 2022.07.27 |
---|---|
enum에서 ordinal 메서드를 사용하지 마라 (0) | 2022.07.27 |
배열보다 리스트를 사용해라 (0) | 2022.07.26 |
unchecked warning은 제거하라 (0) | 2022.07.26 |
raw type은 사용하지 마라 (0) | 2022.07.25 |