Book/Effective Java

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

kkkkkkkkkkkk 2022. 9. 2. 14:30

자바에서 열거 타입을을 제공하기 전에 정수 상수를 한 묶을 선언해서 사용했다.

 

public static final int A = 1;
public static final int B = 1;
public static final int C = 1;

 

위와 같은 열거 패턴의 단점

  • 타입 안전을 보장하는 방법이 없으며 표현력도 좋지 않다.
  • A 라는 상수를 사용해야하는 메서드에 B 라는 상수를 사용해도 컴파일러는 해줄 수 있는게 없다.
  • 동등성을 검증할 때도 경고 메시지를 출력하지 않는다.
  • 상수의 값이 바뀌면 클라이언트도 반드시 다시 컴파일 해야한다.
  • 상수의 갯수를 파악하기 어렵다.
  • 문자열을 상수로 만들 경우 하드 코딩하게 만들기 때문에 안정적이지 못하다.

 

단점을 보완하기 위해 열거타입이 제공된다.

 

public enum SampleEnum {
    A,B,C,D
}

 

열거 타입 특징

  • 인스턴스를 통제
  • 만들어진 인스턴스들은 딱 하나씩만 존재한다. → final을 보장한다.
    • 각각의 이름 공간이 이 있음
  • 싱글턴은 원소가 하나뿐인 열거 타입, 거꾸로 열거 타입은 싱글턴을 일반화한 형태
  • 열거 타입에 선언된 상수들의 값을 배열에 담아 반환하는 메서드 values()를 제공한다.
  • 클라이언트에 노출해야 할 이유가 없으면 해당 패기지에서만 사용하는 기능이라면 private 나 default 로 접근 제어자 사용

 

 

열거 타입 장점

  • 컴파일타임 Type Safe
  • 기존의 열거 타입 패턴보다 추상적으로 구상 할 수 있다.
    • 메서드나 필드를 추가
    • 임의의 인터페이스를 구현

 

열가 타입 상수 각각을 특정 데이터와 연결하려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.

 

public enum SampleEnum {
    A(1),
    B(2),
    C(3),
    D(4);

    private final int num;

    SampleEnum(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }
}

 

 

연산 열거 타입

public enum Operation {
    PLUS,MINUS,TIMES,DIVIDE;

    public double apply(double x, double y) {
        switch (this){
            casePLUS: return x + y;
            caseMINUS: return x - y;
            caseTIMES: return x * y;
            caseDIVIDE: return x / y;
        }
        throw new AssertionError("알 수 없는 연산 : " + this);
    }
}

위의 연산 열거 타입은 동작은 한다.

 

문제점

  • 새로운 상수를 추가하면 switch문에도 로직을 추가해야 한다

 

apply() 메서드를 추상메서드로 작성하고 각 상수에 오버라이드 해서 기능을 구현

public enum OperationSolution1 {
    PLUS{
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS{
        @Override
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES{
        @Override
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE{
        @Override
        public double apply(double x, double y) {
            return x / y;
        }
    };

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

이러한 구현 방법의 문제점

  • 새로운 상수를 추가할 때 apply() 를 재정의 해야함 → 재정의 하지 않는 경우 컴파일 에러

 

상수별 메서드 구현을 데이터와 결합하여 구현

public enum OperationSolution2 {
    PLUS("+") {
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    },
    MINUS("-") {
        @Override
        public double apply(double x, double y) {
            return x - y;
        }
    },
    TIMES("*") {
        @Override
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE("/") {
        @Override
        public double apply(double x, double y) {
            return x / y;
        }
    };

    private final String symbol;

    OperationSolution2(String symbol) {
        this.symbol = symbol;
    }

    @Override
    public String toString() {
        return symbol;
    }

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

 

열거 타입의 상수 이름을 받아 해당 상수를 반환해주는 valueOf() 제공

double apply = OperationSolution2.valueOf("PLUS").apply(1, 14);

 

toString을 재정의 하려거든 formString 메서드도 함계 제공하는 걸 고려하자

private static final Map<String, OperationSolution2> stringToEnum =
            Stream.of(values()).collect(Collectors.toMap(Object::toString, oper -> oper));

public static Optional<OperationSolution2> formString(String symbol) {
    return Optional.ofNullable(stringToEnum.get(symbol));
}

 

전략 열거 타입 패턴

public enum OperationType {
    PLUS1(PLUS),
    MINUS1(MINUS),
    TIMES1(TIMES),
    DIVIDE1(DIVIDE);

    private final Operation operation;

    OperationType(Operation operation) {
        this.operation = operation;
    }

    enum Operation {
        PLUS{
            @Override
            double apply(double x, double y) {
                return x + y;
            }
        },
        MINUS{
            @Override
            double apply(double x, double y) {
                return x - y;
            }
        },
        TIMES{
            @Override
            double apply(double x, double y) {
                return x * y;
            }
        },
        DIVIDE{
            @Override
            double apply(double x, double y) {
                return x / y;
            }
        };

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

전략 열거 타입 패턴도 좋지만 상수별 동작을 혼합해 넣을 때는 switch 문이 좋은 선택이 될 수 있다.

 

정리

  • 필요한 원소를 컴파일타임에 다알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자
  • 열거 타입에 정의된 상수 개수가 영원히 공정 불변일 필요는 없다.
  • 하나의 메서드가 상수별로 다르게 동작해야 할 때 상수별 메서드를 구현하자.
  • 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.