IT/Java

Enum에서 final로 멤버변수를 정의해야하는 이유

yeTi 2022. 6. 7. 15:31

안녕하세요. yeTi입니다.
오늘은 JavaEnum 을 지역 변수처럼 사용하려고 시도하면서 겪은 현상을 공유하고자 합니다.

현상

다음 테스트 코드와 같이 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 이 어떠한 특성을 가져서 타입 세이프하고 싱글톤 패턴에 사용할 수 있는 것인지는 알지 못하고 있었습니다.

특성

JavaEnum클래스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의 본질적으로 불변의 성격을 가지기 때문에 모든 멤버 변수는 final 로 정의하여 사용해야 합니다.

왜냐하면 위에서 설명한것처럼 Enum 타입은 인스턴스를 생성할 수 없고, 모든 열거 인자들이 public static final 로 정의되기 때문에 thread간 공유가 가능하여

멤버 변수를 final 로 설정하지 않으면 의도하지 않게 변경된 값을 사용할 가능성이 존재하기 때문입니다.