티스토리 뷰

Backend/Java

Enum에서 상수를 사용하는 방법

ellie.strong 2022. 3. 17. 14:14
728x90

솔직히 아직 모든 내용을 이해한 것은 아니다. 하지만 일단 내가 이해한 부분까지 정리를 하고 추후에 더 추가 학습을 하는것이 좋을 것 같아 이렇게 글로 작성하게 됐다. 😄

 

문제 발생

public enum Result {

    WIN("승", (dealerScore, participantScore) ->
            (dealerScore < participantScore && participantScore <= 21) || (dealerScore > 21 && participantScore <= 21)),
    DRAW("무승부", (dealerScore, participantScore) ->
            Objects.equals(dealerScore, participantScore) && participantScore <= 21),
    LOSE("패", (dealerScore, participantScore) ->
            (dealerScore > participantScore && dealerScore <= 21) || participantScore > 21);
}

이 코드를 보시고 리뷰어 토니께서 21에 의미를 부여해주면 좋겠다는 리뷰를 해주셨다. 당연히 나는 평소처럼 이를 private static final 상수로 만들어주려고 했다. 하지만 생각지 못한 문제가 발생했다. 

첫번째 시도

public enum Result {

    WIN("승", (dealerScore, participantScore) -> // Illegal forward reference
            (dealerScore < participantScore && participantScore <= WINNING_SCORE) || (dealerScore > WINNING_SCORE && participantScore <= WINNING_SCORE)),
    DRAW("무승부", (dealerScore, participantScore) ->
            Objects.equals(dealerScore, participantScore) && participantScore <= WINNING_SCORE),
    LOSE("패", (dealerScore, participantScore) ->
            (dealerScore > participantScore && dealerScore <= WINNING_SCORE) || participantScore > WINNING_SCORE);
            
    private static final int WINNING_SCORE = 21;
}

이넘에서는 인스턴스를 가장 먼저 선언해주어야한다는 것을 알고있었기 때문에 인스턴스 선언 아래에 상수를 선언해주었다. 여기까지는 문제가 없었지만 선언한 상수를 인스턴스 생성자의 인자로 넘겨주는 방식을 사용하니 Illegal forward reference 컴파일 에러가 발생했다. 이때가지는 뭐가 문제인지 알지 못했다. 그래서 혹시 선언 순서가 문제인가해서 안 될걸 알면서도 한번 상수 선언을 맨 위로 올려보았다. 

두번째 시도

public enum Result {

    private static final int WINNING_SCORE = 21;

    //Illegal in enums, first statement has to be an instance
    WIN("승", (dealerScore, participantScore) -> 
            (dealerScore < participantScore && participantScore <= WINNING_SCORE) || (dealerScore > WINNING_SCORE && participantScore <= WINNING_SCORE)),
    DRAW("무승부", (dealerScore, participantScore) ->
            Objects.equals(dealerScore, participantScore) && participantScore <= WINNING_SCORE),
    LOSE("패", (dealerScore, participantScore) ->
            (dealerScore > participantScore && dealerScore <= WINNING_SCORE) || participantScore > WINNING_SCORE);
}

이렇게 선언하니 컴파일러가 아예 WIN, DRAW, LOSE를 인스터스 선언으로 인식하지 못해 컴파일 에러가 발생했다. 어떻게 해줘야할지 모르겠어서 아예 새로운 Rule 이넘을 생성해 WINNING_SCORE를 새로운 인스턴스로 선언하고, Rule.WINNING_SCORE를 사용하는 방식으로 적용했다. 그리고 이에 대해 리뷰어께 여쭈어봤다. 

 

이넘에서의 static 필드 초기화 순서

이펙티브 자바에서는 이넘을 이렇게 설명한다. 

열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다. 

이때 이 인스턴스는 static이기 때문에 생성문에서 다른 상수를 바로 참조할 수 없게된다. 또한 같은 이넘 내부의 값들도 선언되는 순서가 역전되면 참조할 수 없다. 무슨말인고 하면 이넘에서 static 필드는 선언되어있는 순서데로 초기화가 진행된다. 따라서 WIN, DRAW, LOSE가 초기화되는 시점에 WINNING_SCORE는 초기화되어 있지 않기 때문에 Illegal forward reference 컴파일 에러가 발생하는 것이다. 같은 이유로 DRAW에서는 WIN을 참조할 수 있지만, WIN에서는 DRAW를 참조하지 못한다. 

 

Java9부터 JLS에 이와 관련된 내용이 추가되었다고 한다. 

Note that static fields that are constant variables are initialized before other static fields. [...] When such fields are referenced by simple name, they will never be observed to have their default initial values.

 

그렇다면 어떻게 해결해야할까??

해결하는 방법으로는 3가지 정도가 있지만 그 중 중첩(nested) 클래스 사용이 가장 일반적이라고 한다. 

 

중첩(Nested) 클래스 사용 

중접 클래스 중 정적 멤버 클래스를 이용해 상수를 선언해서 사용할 수 있다. 중첩 클래스는 별도로 초기화되므로 참조 가능하다. 

멤버 클래스에서 바깥 인스턴스에 접근할 일이 없으므로 정적 멤버 클래스로 사용하기를 권장한다. (이펙티브 자바 3/E 아이템 24)

public enum Result {

    WIN("승", (dealerScore, participantScore) -> 
            (dealerScore < participantScore && participantScore <= Constants.WINNING_SCORE) || (dealerScore > Constants.WINNING_SCORE && participantScore <= Constants.WINNING_SCORE)),
    DRAW("무승부", (dealerScore, participantScore) ->
            Objects.equals(dealerScore, participantScore) && participantScore <= Constants.WINNING_SCORE),
    LOSE("패", (dealerScore, participantScore) ->
            (dealerScore > participantScore && dealerScore <= Constants.WINNING_SCORE) || participantScore > Constants.WINNING_SCORE);
    
    private static class Constants { // 정적 멤버 클래스
        private static final int WINNING_SCORE = 21;
    }
}

메서드 사용

정적 메서드를 선언해 사용할 수 있다. 하지만 메서드 호출 자체는 상수 표현식(constant expression)이 아니기 때문에 어노테이션 요소 값으로 넣을 수 없는 등의 사용에 제약이 있을 수 있다고 한다. 

public enum Result {

    WIN("승", (dealerScore, participantScore) ->
            (dealerScore < participantScore && participantScore <= getWinningScore()) || (dealerScore > getWinningScore() && participantScore <= getWinningScore())),
    DRAW("무승부", (dealerScore, participantScore) ->
            Objects.equals(dealerScore, participantScore) && participantScore <= getWinningScore()),
    LOSE("패", (dealerScore, participantScore) ->
            (dealerScore > participantScore && dealerScore <= getWinningScore()) || participantScore > getWinningScore());

    private static int getWinningScore() {
        return 21;
    }
}

Qualified Name 사용

qualified name이란 simple name인 WINNING_SCORE의 계층을 모두 표현한 Result.WINNING_SCORE를 의미한다. 하지만 이 방법은 Java9부터는 JLS에서 더 이상 작동을 보장하지 않으므로 사용을 권장하지 않는다고한다. (일단 Java11을 사용하는 내 코드에서는 컴파일 에러가 나지 않으며, 테스트 결과 잘 작동하는 것으로 확인되어, 아직 무슨말인지 잘 이해가 가지는 않는다. 😅 )

public enum Result {

    WIN("승", (dealerScore, participantScore) ->
            (dealerScore < participantScore && participantScore <= Result.WINNING_SCORE) || (dealerScore > Result.WINNING_SCORE && participantScore <= Result.WINNING_SCORE)),
    DRAW("무승부", (dealerScore, participantScore) ->
            Objects.equals(dealerScore, participantScore) && participantScore <= Result.WINNING_SCORE),
    LOSE("패", (dealerScore, participantScore) ->
            (dealerScore > participantScore && dealerScore <= Result.WINNING_SCORE) || participantScore > Result.WINNING_SCORE);

    private static final int WINNING_SCORE = 21;
}

 

이번 블랙잭 미션의 실 코드에는 적용하지 않았지만, 또 하나의 무기가 생긴것 같아 기쁘다 👍

그리고 내가 생각하기에 이넘이 상수들의 나열을 대체한 기술인 만큼 이넘 안에 또 상수를 선언하기보다는 중첩 클래스를 쓰는 방식이 더 좋아보이기는 한다. 

 

Ref.

https://stackoverflow.com/questions/30169153/illegal-forward-reference-in-enum

https://docs.oracle.com/javase/specs/jls/se9/html/index.html

 

728x90
댓글
공지사항
최근에 올라온 글