[item 34] int 상수 대신 열거 타입을 사용하라

김동훈·2023년 8월 18일
0

Effective-java

목록 보기
13/14

열거타입

public enum Enum {
    ONE(1),
    TWO(2),
    THREE(3),;
    private int number;

    Enum(int number) {
        this.number = number;
    }
}

열거타입 자체는 클래스이다.
상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 제공한다. 일반적인 클래스로 구현한다면 어떤 형태일지 한번 작성해보자.

public class RawEnum {

    static final RawEnum ONE = new RawEnum(1);
    static final RawEnum TWO = new RawEnum(1);
    static final RawEnum THREE = new RawEnum(1);
    
    private int number;

    public RawEnum(int number) {
        this.number = number;
    }
}

public static final 필드로 각 상수에 해당하는 인스턴스를 제공하고 있다.
이러한 클래스의 형태를 보면 열거 타입의 특징과도 일치한다.

열거 타입에 메서드나 필드를 추가하는 것도 가능하다.
당연히 열거 타입 자체는 클래스이니 가능한게 맞다.

두 enum원소의 숫자를 더하는 메서드와 Enum의 모든 원소를 더하는 메서드를 추가해보자.

public enum Enum {
    ONE(1),
    TWO(2),
    THREE(3),;
    private int number;

    Enum(int number) {
        this.number = number;
    }
    public int sum(Enum first, Enum second) {
        return first.number + second.number;
    }
    
    public int sumAll() {
        return Arrays.stream(values())
                .mapToInt(Enum::getNumber)
                .sum();    
    }

    private int getNumber() {
        return number;
    }
}

sumAll 메소드를 보면 values()라는 메소드를 사용하고 있다. 이 메소드는 Enum이 제공하는 정적 메소드이다.

하지만 Enum클래스를 봐도 어디에도 values() 라는 메소드는 존재하지 않는다. 이 메소드는 컴파일러가 자동 생성해주는 특별한메소드이다.

만약 상수별로 다르게 동작하게 만들고 싶다. 이런 경우 추상 메서드를 선언하여 상수별 메서드 구현을 하면 된다.

계산기를 Enum으로 작성하고, 상수 별로 계산되는 로직을 다르게 작성해보자.

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

    @Override
    public String toString() {
        int a = num;
        return symbol;
    }

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

}

매우 깔끔하다. apply는 추상메서드이므로, Operation의 상수가 apply를 재정의 하지 않으면 컴파일 오류가 나니 매우 안전하다.

열거 타입의 정적필드와 생성자의 관계

Operation에 문자열을 해당 열거타입 상수로 변환해주는 fromString메서드를 만들어보자.

코드가 너무 길어지니, 추가된 부분만 작성해놓겠다.

public enum Operation {

    ...    
    
    private static final Map<String, Operation> stringToEnum =
           Stream.of(values())
           .collect(toMap(Object::toString, e -> e));
           
    ...
       
    public static Optional<Operation> fromString(String symbol) {
        return Optional.ofNullable(stringToEnum.get(symbol));
    }

}

stringToEnum에 대해 알아볼 부분이 있다.

  1. Operation 상수가 stringToEnum 맵에 추가되는 시점
  2. 열거타입 상수는 생성자에서 자신의 인스턴스를 맵에 추가할 수 없다.
  3. 열거 타입 생성자에서 같은 열거 타입의 다른 상수에 접근 할 수 없다.

2번과 3번은 같은 맥락에서 이해할 수 있다.
하나씩 알아보자.

1. Operation 상수가 stringToEnum 맵에 추가되는 시점

열거 타입 상수 생성 후 정적 필드가 초기화 될 때이다.
열거 타입의 각 상수는 결국 public static final 인 인스턴스라고 했다. 곧 정적필드이다. Java에서 정적 필드는 클래스에 작성된 순서대로 초기화된다. Enum은 상수 선언을 맨 위에 해야하고 다른 정적 필드를 그 밑에 선언한다. 따라서 당연히 stringToMap의 초기화 시점은 각 상수들이 초기화 된 이후이다.

2 & 3 열거타입 생성자에서 정적필드의 접근 제한

자바 8 이전 방식을 설명하며 빈 해시맵에 상수를 추가한다고 설명했다. 그리고 생성자에서는 상수 변수를 제외한 정적 필드에 접근 할 수 없다고 한다.

우선 stringToEnum 맵을 빈 해시맵으로 바꾸고, 생성자에서 추가해보도록 하자.

    private static final Map<String, Operation> stringToEnum =
//            Stream.of(values()).collect(
//                    toMap(Object::toString, e -> e));
            new HashMap<>();
            
    Operation(String symbol) {
        this.symbol = symbol;
        stringToEnum.put("+", this.PLUS);
    }

이렇게 작성하면 stringToEnum.put("+", this.PLUE); 에 오류가 날 것이다. 메시지를 확인해보면, It is illegal to access static member 'stringToEnum' from enum constructor or instance initializer 즉 enum의 생성자에서 정적 필드에 접근할 수 없다고 한다.

왜 그럴까?

열거 타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화되기 전이라, 자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다. 이라고 한다. 즉 생성자가 실행될 때에는 정적필드가 초기화 되기 전이라는 것이다.

사실 일반적인 클래스에서의 생성자에서는 정적필드에 접근이 가능하다. 아직 초기화되지 않은 정적필드에 접근하면 null로 대체된다.
하지만 Enum에서는 제한한다.

나의 개인적인 생각으로는, enum이라는 특성에 그 이유가 있다고 생각한다.
Enum은 상수들의 집합이다. 이 상수들은 특정한 의미를 가져야한다. 만약 null로 대체가 가능하다면, 이 특정한 의미를 잃어버리게 되어 enum의 의미가 사라진다고 생각한다.

따라서, 일반적인 클래스처럼 null로 대체할 수 없고 그러니 접근을 제한한 것으로 보인다. stringToEnum같은 정적필드를 통해 사용자가 어떠한 행위를 할 지 모르니 역시 접근을 제한한 것이라고 생각한다.


참고

profile
董訓은 영어로 mentor

0개의 댓글