Enum에서 final로 멤버변수를 정의해야하는 이유
안녕하세요. yeTi입니다.
오늘은 Java
의 Enum
을 지역 변수처럼 사용하려고 시도하면서 겪은 현상을 공유하고자 합니다.
현상
다음 테스트 코드와 같이 enum
을 여러 곳에서 지역 변수로 할당받아 Enum
의 내부 변수(message
)를 변경하는 작업을 수행했더니
의도와는 다르게 message
의 값이 이전 값을 계속 유지하는 버그가 나타났습니다.
class EnumTest {
@Test
void constructor() {
EnumType responseType1 = EnumType.INTERNAL_SERVER_ERROR;
responseType1.changeMessage("yeti");
EnumType responseType2 = EnumType.INTERNAL_SERVER_ERROR;
responseType2.changeMessage("yeti2");
assertEquals("yeti Internal Server Error", responseType1.getMessage());
assertEquals("yeti2 Internal Server Error", responseType2.getMessage());
}
private enum EnumType {
INTERNAL_SERVER_ERROR("500", "Internal Server Error");
private final String code;
private String message;
EnumType(String code, String message) {
this.code = code;
this.message = message;
}
public String getMessage() {
return this.message;
}
public void changeMessage(String user) {
this.message = user + " " + this.message;
}
}
}
...
Expected :yeti Internal Server Error
Actual :yeti2 yeti Internal Server Error
배경지식
Enum
은 타입 세이프
를 위해 값의 제약을 위해 활용하거나 싱글톤 패턴을 적용할때 사용한다는 것 정도만 알고 있었지
Enum
이 어떠한 특성을 가져서 타입 세이프
하고 싱글톤 패턴
에 사용할 수 있는 것인지는 알지 못하고 있었습니다.
특성
Java
의 Enum
은 클래스
로 public static final field
를 활용하여 만들어졌습니다.
따라서 클래스를 활용하여 Enum
을 흉내낼 수 있습니다.
MyEnum.java
public abstract class MyEnum<E extends MyEnum<E>> implements Comparable<E>, Serializable {
private final String name;
private final int ordinal;
protected MyEnum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public final Class<E> getDeclaringClass() {
Class<?> clazz = getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == MyEnum.class) ? (Class<E>)clazz : (Class<E>)zuper;
}
@Override
public final int compareTo(E o) {
MyEnum<?> other = (MyEnum<?>)o;
MyEnum<E> self = this;
if (self.getClass() != other.getClass() && // optimization
self.getDeclaringClass() != other.getDeclaringClass())
throw new ClassCastException();
return self.ordinal - other.ordinal;
}
}
EnumClass.java
public class EnumClass extends MyEnum {
public static final EnumClass INTERNAL_SERVER_ERROR = new EnumClass("500", "Internal Server Error");
private final String code;
private String message;
private EnumClass(String code, String message) {
super("name", 0);
this.code = code;
this.message = message;
}
public String getMessage() {
return this.message;
}
public void changeMessage(String user) {
this.message = user + " " + this.message;
}
}
MyEnumTest.java
class MyEnumTest {
@Test
void constructor() {
EnumClass responseType1 = EnumClass.INTERNAL_SERVER_ERROR;
responseType1.changeMessage("hshwang");
EnumClass responseType2 = EnumClass.INTERNAL_SERVER_ERROR;
responseType2.changeMessage("hshwang2");
assertEquals("yeti Internal Server Error", responseType1.getMessage());
assertEquals("yeti2 Internal Server Error", responseType2.getMessage());
}
}
...
Expected :yeti Internal Server Error
Actual :yeti2 yeti Internal Server Error
위의 구현된 내용과 같이 Enum
은 private 생성자를 가지기 때문에 인스턴스를 생성할 수 없고, 멤버 변수들은 public static final field
의 특성을 가지기 때문에 상수처럼 사용할 수 있습니다.
이러한 특성으로 무조건 하나의 인스턴스를 가지도록 보장이 되기 때문에 싱글톤
패턴으로 활용할 수 있는 것이고, 멤버 변수들을 가지고 상수의 셋으로 사용할 수 있기 때문에 컴파일 단계에서 오류를 알 수 있어 타입 세이프
를 보장할 수 있다고 할 수 있습니다.
참고 자료
- Enum Types - The Java™ Tutorials
- Effective Java Third Edition - Chapter 6. Enums and Annotations
- 자바 깊이 알기 / Enum의 원리와 구현
해결
Enum
의 본질적으로 불변의 성격을 가지기 때문에 모든 멤버 변수는 final
로 정의하여 사용해야 합니다.
왜냐하면 위에서 설명한것처럼 Enum
타입은 인스턴스를 생성할 수 없고, 모든 열거 인자들이 public static final
로 정의되기 때문에 thread간 공유가 가능하여
멤버 변수를 final
로 설정하지 않으면 의도하지 않게 변경된 값을 사용할 가능성이 존재하기 때문입니다.