■ 정수 열거 패턴 기법

자바에서 열거 타입을 지원하기 전에는 다음 코드처럼 정수 상수를 한 묶음 선언해서 사용하곤 했다.

public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;

public static final int ORANGE_NAVEL = 0
public static final int ORANGE_TEMPLE = 1;

정수 열거 패턴 기법에는 단점이 많다.

 

1. 타입 안전을 보장할 방법이 없으며 표현력이 좋지 않다. 예를들어 APPLE_FUJI를 사용해야할 곳에 ORANGE_NAVEL을 사용해도 둘다 정수 0이기 때문에 컴파일 때 문제가 없다. 또한 이름공간을 지원하지 않기 때문에 접두어를 사용해서 이름 충돌을 방지한다.

ELEMENT_MERCURY  //수은
PLANET_MERCURY   //수성

 

 

2. 정수 열거 패턴을 사용한 프로그램은 깨지기 쉽다. 평범한 상수를 나열한 것 뿐이라 컴파일하면 해당 값이 클라이언트 파일에 그대로 새겨진다. 만약 상수의 값이 바뀌면 반드시 다시 컴파일 해야 한다.

 

3. 정수 상수는 문자열로 출력하기 어렵다.

public static final int APPLE_FUJI = 0;
System.out.println(APPLIE_FUJI); // APPLIE_FUJI 가 아닌 0 출력

 

4. 같은 정수 열거 그룹에 속한 모든 상수를 한 바퀴 순회하는 방법도 마땅치 않다.

 

■ 열거 타입

정수 열거 패턴의 단점을 보완하고 여러가지 장점을 주는 대안이 바로 자바의 열거 타입(Enum Type)이다.

 

1. 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재한다. 열거 타입은 싱글턴을 일반화한 형태라고 볼 수 있다.

 

2. 열거 타입은 컴파일타임에 타입 안전성을 제공한다. 즉, 값이 같은 0이더라도 Apple 열거 타입에 Orange 열거 타입이 들어올 수 없다.

 

3. 열거 타입에는 각자의 이름 공간이 있어서 같은 상수도 공존한다.

 

4. 열거 타입의 toString 메서드는 출력하기에 적합한 문자열로 반환한다.

 

5. 열거 타입에는 임의의 메소드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수도 있다.

package ch6.hoon.item34;

enum WeekDay {

    MONDAY(0),
    TUESDAY(1),
    WEDNESDAY(2),
    THURSDAY(3),
    FRIDAY(4),
    SATURDAY(5),
    SUNDAY(6);

    //임의의 필드
    private final int value;

    WeekDay(int value) {
        this.value = value;
    }

    //메서드 구현
    public int getValue() {
        return value;
    }

    public static void main(String[] args) {
        System.out.println(WeekDay.MONDAY); //MONDAY 출력
        System.out.println(WeekDay.MONDAY.toString()); //MONDAY 출력
        System.out.println(WeekDay.MONDAY.getValue());  //0 출력

        for (WeekDay value : WeekDay.values()) {    //enum 순회
            System.out.println(value);
        }

        WeekDay monday = WeekDay.valueOf("MONDAY"); //String 인자를 기준으로 enum의 인스턴스를 가져온다
        System.out.println(monday);
    }

}

6. 열거 타입의 또 하나의 장점은 열거 타입에 선언한 상수 하나를 제거하더라도 제거한 상수를 참조하지 않는 클라이언트에는 아무 영향이 없다. 또한 그런 상수를 참조를 하더라도 컴파일 에러가 발생할 것이다.

 

열거 타입을 선언한 클래스 혹은 그 패키지에서 유용한 기능은 private 이나 package-private으로 구현한다. 널리 쓰이는 열거 타입은 톱레벨 클래스로 만들고, 특정 톱레벨 클래스에서만 사용한다면 해당 클래스의 멤버 클래스로 만든다.

 

■ 상수별 메서드 구현

열거 타입으로 구현하면서 타입의 메서드가 상수에 따라 다르게 동작해야하는 경우가 있다. 다음과 같이 swith문으로 해결할 수 있지만 새로운 상수가 추가되면 case문도 추가 해야한다.

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

    public double apply(double x, double y) {
        switch (this) {
            case PLUS:
                return x + y;
            case MINUS:
                return x - y;
            case TIMES:
                return x * y;
            case DIVDE:
                return x / y;
        }
        throw new AssertionError("알 수 없는 연산:" + this);
    }
}

 

상수별 메서드 구현은 열거 타입에 추상 메서드를 선언하고 각 상수별로 클래스 몸체를 상수가 자신에 맞게 재정의해서 개선할 수 있다.

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;
        }
    },
    DIVDE("/") {
        public double apply(double x, double y) {
            return x / y;
        }
    };

    private final String symbol;

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

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

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

 

toString 메서드를 재정의했다면 toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 fromString 메서드도 함께 제공하는걸 고려해보자. 의 코드에서 toString 메소드를 재정의해 기존 상수의 이름이 아닌 각 연산자의 기호를 반환하도록 구현하였다. 반대로 fromString 메소드를 구현하여 연산자 기호를 매개변수로 전달하면 알맞은 열거 타입 객체를 반환하도록 해보자.

 

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

//Optional로 반환하여 값이 존재하지않을 상황을 클라이언트에게 알린다.
public static Optional<Operation> fromString(String symbol) {
    return Optional.ofNullable(stringToEnum.get(symbol));
}

 

여기서 중요하게 봐야할 것은 Map에 Operation 상수가 추가되는 시점이다. Operation 상수들은 정적 필드가 초기화되는 시점에 추가된다. 열거 타입에서 정적 필드는 열거 타입 상수가 생성된 후에 초기화 된다. 그렇기 때문에 열거 타입 생성자에서 정적 필드를 참조하려고 하면 컴파일 에러가 발생한다. 

 

■ 전략 열거 타입 패턴

상수별 메서드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다는 단점이 있다.

public enum PayrollDay {
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY;

    private static final int MINS_PER_SHIFT = 8 * 60;

    int pay(int minutesWorked, int payRate) {
        //기본 급여
        int basePay = minutesWorked * payRate;
		//잔업수당
        int overtimePay;
        switch (this) {
        	//주말
            case SATURDAY:
            case SUNDAY:
                overtimePay = basePay / 2;
                break;
            //주중
            default:
                overtimePay = minutesWorked <= MINS_PER_SHIFT ?
                        0 : (minutesWorked - MINS_PER_SHIFT) * payRate / 2;
        }

        return basePay + overtimePay;
    }
}

위의 코드에서 휴가와 같은 새로운 상수가 추가되면 휴가에 맞는 급여를 처리하는 case문을 추가해줘야 한다는 단점이 있다. 상수별 메서드 구현으로 급여를 계산하는 몇가지 방법이 있다.

 

1. 잔업 수당을 계산하는 코드를 모든 상수에 넣는 방법

 

2. 계산 코드를 평일용과 주말용으로 나눠 각각 도우메 메소드로 작성한 다음 각 상수가 자신에게 필요한 메소드를 적절히 호출하는 방법

 

3. 평일 잔업수당 계산 메소드를 구현해두고 주말 상수에서만 재정의해서 쓰는 방법

 

하지만 1, 2번 방식은 코드의 가독성이 떨어지고 오류가 발생한 가능성이 높아지고, 3번의 경우 새로운 상수가 추가되는 경우 switch와 똑같이 메소드를 잊지않고 재정의 해야하는 경우가 생길 수 있다.

 

가장 깔끔한 방법은 새로운 상수를 추가할 때 마다 잔업수당 '전략'을 선택하도록 하는 것이다. 잔업 수당 계산을 private 주엋ㅂ 열거타입으로 옮기고 PayrollDay 열거 타입은 잔업수당 계산을 그 전략 열거 타입에 위염하여, swithch문이나 상수별 메서드 구현이 필요 없게 된다. switch 문보다 복잡하지만 더 안전하고 유연하다.

public enum PayrollDay {
    MONDAY(PayType.WEEKDAY),
    TUESDAY(PayType.WEEKDAY),
    WEDNESDAY(PayType.WEEKDAY),
    THURSDAY(PayType.WEEKDAY),
    FRIDAY(PayType.WEEKDAY),
    SATURDAY(PayType.WEEKEND),
    SUNDAY(PayType.WEEKEND);
    
    private final PayType payType;

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

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

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

        abstract int overtimePay(int minutesWorked, int payRate);
        private static final int MINS_PER_SHIFT = 8 * 60;

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

 

■ 열거 타입 사용

필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자. 또한 열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다.

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기