이펙티브 자바 #item34 int 상수 대신 열거 타입을 사용하라

임현규·2023년 2월 13일
0

이펙티브 자바

목록 보기
34/47
post-thumbnail

상수 정수 열거 패턴의 문제점

상수를 이용해서 열거 패턴을 사용하면 구현은 쉽지만 문제점이 있다. 바로 실수할 가능성이 높아지고, 컴파일러를 통해서 어떤 오류나 경고도 볼 수 없다. 코드를 살펴 보자

public class FruitFood {
	public static final int APPLE_JUICE = 0;
    public static final int APPLE_MANGO_JUICE = 1;
    public static final int APPLE_CIDAR = 2;
    
    public static final int ORANGE_JUICE = 0;
    public static final int ORANGE_MANGO_JUICE = 1;
    public static final int ORANGE_CIDAR = 2;
}

데이터 타입의 연산자를 기준으로 컴파일되는 문제

일부로 실수를 유도하도록 코드를 짯는데 만약에 APPLE_JUICE == ORANGE_JUICE 를 비교한다고 가정해보자 결과는 true이다. 우리는 의미상 구분을 했지만 정수형 상수이므로 컴파일러는 2개의 상수 간의 차이점을 인지하지 못한다. 그리고 정수형이기 때문에 실수로 다음과 같은 코드를 짤 수도 있다.

int mix_juice = APPLE_JUICE * ORANGE_JUICE;

진짜 이런 실수를 할 지는 모르겠지만, 실제로 이렇게 코드를 짜도 컴파일러는 어떤 오류도, 경고도 출력하지 않는다. 나중에 테스트 때 코드가 터지고 디버깅해서야 발견할 수 있는 코드이다. 심지어 단순 정수형이기 때문에 디버깅도 쉽지 않다.

상수를 전부 조회하기 힘들다.

위의 코드만으로는 모든 상수를 조회할 수 없다. 만약 조회하는 메서드를 만든다고 해도 문제이다. 정수형 값이 저장되어 있기 때문에 순회를 한다면 단순히 [0, 1, 2, 0, 1, 2]를 줄 것이다. 문제는 이렇게 주면 우리는 해당 숫자가 어떤의미를 가지고 있는지 파악하기 힘들다.

열거형은 Enum이다.

위의 2개의 문제는 enum으로 말끔히 해결할 수 있다. enum은 기본적으로 싱글턴 구조이며 열거형으로 선언한 상수를 인스턴스 클래스 형태로 제공한다. 기본이 class이기 때문에 왠만한 Object에서 제공하는 것 모두 제공하며, Comparable과 Serializable이 구현되어 있어 직렬화도 전혀 문제가 없다. 심지어 toString까지 지원해서 enum 인스턴스를 출력하면 인스턴스 이름을 자동으로 출력해준다.

enum FruitFood {
    APPLE_JUICE, APPLE_MANGO_JUICE, APPLE_CIDER, ORANGE_JUICE, ORANGE_MANGO_JUICE, ORANGE_CIDER
}

위의 형태로 열거형으로 짜면 상수 조회 또한 매우 쉽다.

    @Test
    void test2() {
        System.out.println(FruitFood.APPLE_CIDER.equals(FruitFood.ORANGE_CIDER));
        System.out.println(Arrays.toString(FruitFood.values()));
    }

위의 코드를 실행하면

다음과 같이 열거형 상수 인스턴스의 이름이 알아서 toString()이 구현되어 있어 따로 구현하지 않아도 쉽게 볼 수 있고 열거형 상수들의 목록을 조회해볼 수 있다.

enum은 클래스다

java의 enum은 다른 언어와 달리 단순 열거형 상수만이 아닌 클래스를 제공한다. 그래서 abstract를 활용한 추상화 메서드나 인스턴스 메서드, 정적 메서드 모두 제공하고 필요하면 인스턴스 변수 또한 활용할 수 있다.

public enum Planet {
	MERCURY(..., ....),
    VENUS(.., ....),
    EARTH(..., ....),
    MARS(..., ....),
    JUPITER(..., ....),
    SATURN(..., ....),
    URANUS(..., ....),
    NEPTUNE(..., ....);
    
    private final double mass;
    private final double radius;
    private final double surfaceGravity;
    
    private static final double G = 6.67300E-11;
    
    Planet(double mass, double radius) {
    	this.mass = mass;
        this.radius = radius;
        surfaceGravity = G * mass / (radius * radius);
    }
    
    public double mass() {return mass;}
    public double radius() {return radius;}
    public double surfaceGravity() {return surfaceGravity;}
	
    public double surfaceWeight(double mass) {
    	return mass * surfaceGravity; // F = ma
    }
}

클래스처럼 사용가능한 enum이다. enum을 활용하면 굉장히 쉽게 메서드를 제공해줄 수 있고, 다양하게 활용할 수 있다. 생성자가 private 또는 private-package인 이유는 오직 하나의 인스턴스만 생성되는 것이 보장되는 객체가 바로 enum이기 때문이다.

기능을 상수화하기

예를 들면 연산과 같은 기능을 상수화하고 싶을 수 있다. 각 객체별 메서드를 구현하고 순회할 때 특히 유연하다

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;}};
    
 	public abstract double apply(double x, double y);
}

위와 같은 코드를 짜면 Operation.values()를 활용해 for문을 활용할 수 있다. 그 이외에도 메서드 자체를 객체화했기 때문에 다양하게 활용가능하다.

전략 패턴의 활용

목록이 정해져 있고, if문을 줄이고 싶다면 전략 패턴을 활용할 수 있다.

enum PayRollDay {
    MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), THURSDAY(WEEKDAY), FRIDAY(
        WEEKDAY), SATURDAY(WEEKEND), SUNDAY(WEEKEND);

    private final PayType payType;

    PayRollDay(PayType payType) {
        this.payType = payType;
    }

    int pay(int minutesWorked, int payRate) {
        return payType.pay(minutesWorked, payRate);
    }

    enum PayType {
        WEEKDAY {
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked <= MINS_PER_SHIFT ? 0
                    : (minsWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND{
            int overtimePay(int minsWorked, int payRate) {
                return minsWorked * payRate / 2;
            }
        };

        abstract int overtimePay(int mins, int payRate);

        private static final int MINS_PER_SHIFT = 8 * 60;

        int pay(int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            return basePay + overtimePay(minsWorked, payRate);
        }
    }
}

전략 패턴을 활용하면 if 문 없이 전략패턴으로 구현된 열거형 객체를 각 요일마다 주입해서 효율적으로 코드를 짤 수 있다.

profile
엘 프사이 콩그루

0개의 댓글