해당 포스트는 "Effective Java" 책의 내용을 요약한 것이다.



※ int 상수 대신 enum을 사용하라.

: enum은 열거 상수별로 하나의 객체를 public static final 필드 형태로 제공하는 것이다. enum은 자료형의 개체 수를 엄격히 통제된다. 싱글턴 패턴을 일반화 한것으로 싱글턴 패턴은 본질적으로 보면 열거 상수가 하나뿐인 enum과 같다. 다음은 enum의 한 예이다.

enum Operation {
	PLUS, MINUS, TIMES, DIVIDE;
	
	double apply(double x, double y){
		switch(this){
		case PLUS: return x+y;
		case MINUS: return x-y;
		case TIMES: return x*y;
		case DIVIDE: return x/y;
		}
		throw new AssertionError("Unknown op: " + this);
	}
}

위 코드는 enum 상수에 따라 분기하는 switch 문을 사용했다. switch문은 새로운 enum 상수를 추가하고 case문을 추가 안해도 컴파일이 된다. 이렇게 switch문은 깨지기 쉬운 오류이다. 따라서 다른 방법으로 enum 자료형에 추상 메서드를 선언하고 각 상수별 클래스 몸체 안에 실제 메서드를 재정의 하는 방식(상수별 메서드 구현)을 사용해야 한다. 

enum Operation {
	PLUS("+"){
		double apply(double x, double y){return x+y;}
	}, 
	MINUS("-"){
		double apply(double x, double y){return x-y;}
	}, 
	TIMES("*"){
		double apply(double x, double y){return x*y;}
	}, 
	DIVIDE("/"){
		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;}
	
	abstract double apply(double x, double y);
}

위와 같이 코딩하면 상수를 추가해도 apply 메서드 구현을 잊을 가능성도 없고 잊더라도 컴파일러가 오류를 낸다. 위와 같은 상수별 메서드 구현의 단점은 enum 상수끼리 공유하는 코드를 만들기가 힘들다. 다음은 급여 계산 코드인데 주중과 주말의 초과근무 수당을 주는 방식이 달라 switch 문으로 주말과 주중을 구분한다. 그리고 enum 상수끼리 코드를 공유해야 되서 switch문을 활용했다.

enum PayrollDay{ MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY; private static final int HOURS_PER_SHIFT=0; double pay(double hoursWorked, double payRate){ double basePay = hoursWorked*payRate; double overtimePay; switch(this){ case SATURDAY: case SUNDAY: overtimePay = hoursWorked*payRate/2; break; default: overtimePay = hoursWorked <= HOURS_PER_SHIFT ? 0 : (hoursWorked - HOURS_PER_SHIFT) * payRate /2; } return basePay+overtimePay; } }

위 코드는 비교적 간결하다. 하지만 초과근무수당 외에 휴가 수당 등 다른 급여 계산을 추가로 적용할 시 case문에 추가해야 하고 구현을 잊을 가능성 또한 높다. 이를 대안하는 방법으로 정책 enum 패턴이 있다. 정책 enum 패턴은 private로 선언된 중첩 enum 자료형을 넣고 중첩 enum을 enum 생성자의 인자로 받게 하는 것이다. 이 패턴을 사용할 경우 switch 구문을 없앨 수 있고 switch 구문보다는 복잡하지만 안전하고 유연성이 높다. 다음은 위 예제를 정책 enum 패턴으로 구현한 코드이다. 

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

    double pay(double hoursWorked, double payRate){
        return payType.pay(hoursWorked, payRate);
    }

    private enum PayType{
        WEEKDAY{
          double overtimePay(double hours, double payRate){
              return hours <= HOURS_PER_SHIFT ? 0 : (hours - HOURS_PER_SHIFT) * payRate / 2;
          }
        },
        WEEKEND{
            double overtimePay(double hours, double payRate){
                return hours * payRate /2;
            }
        };

        private static final int HOURS_PER_SHIFT = 8;

        abstract double overtimePay(double hrs, double payRate);

        double pay(double hoursWorked, double payRate){
            double basePay = hoursWorked * payRate;
            return basePay + overtimePay(hoursWorked,payRate);
        }
    }
}


enum은 int 상수와 성능 면에서 비슷하지만 enum 자료형은 메모리에 올리고 초기화하는 공간적/시간적 비용 때문에 약간 손해를 보긴 한다. 그러나 휴대전화처럼 시스템 자원 요구량에 민감한 기기에 돌릴 프로그램이 아니라면 실제로는 별 차이 없다. enum은 고정된 상수 집합이 필요할 때 사용해야 한다.


※ ordinal 대신 객체 필드를 사용하라.

public enum Ensemble {
    SOLO, DUET, TRIO, QUARTET, QUIINTET,
    SEXTET, SEPTET, OCTET, NONET, DECTET;

    public int numberOfMusicions() { return ordinal() + 1; }
}
위와 같이 enum 상수에 정수 값을 매칭할 때 ordinal을 사용할 수 있다. 하지만 ordinal을 사용할 시 부작용이 너무 많다. 위 enum 상수들의 중간에 다른 값이 들어갈 경우 다른 enum 상수에 매칭된 값이 다 달라진다. 또한 enum 상수에 14를 매칭하고 싶을 경우에 필요없는 enum 상수를 별도로 추가해줘야 한다. 따라서 ordinal을 사용해서는 안 된다. ordinal은 EnumSet이나 EnumMap처럼 일반적인 용도의 enum 기반 자료 구조에서 사용할 목적으로 설계한 메서드이다. ordinal 대신 다음 코드와 같이 객체 필드를 사용해야 한다.
public enum Ensemble {
    SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUIINTET(5),
    SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
    NONET(9), DECTET(10), TRIPLE_QUARTET(12);

    private final int numberOfMusicions;
    Ensemble(int size) { this.numberOfMusicions = size; }
    public int numberOfMusicions() { return numberOfMusicions; }
}


※ 비트 필드(bit field) 대신 EnumSet을 사용하라.

public class Text{
    public static final int STYLE_BOLD = 1;
    public static final int STYLE_ITALIC = 2;
    public static final int STYLE_UNDERLINE = 4;
    public static final int STYLE_STRIKETHROUGH = 8;

    public void applyStyles(int styles){ ... }
}
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
위 코드는 비트 필드를 표현한 예이다. 비트 필드를 사용하면 일반적인 list나 set보다 빠릅니다. 하지만 비트 필드 요소들을 순차적으로 보기 힘들고 비트 필드를 출력한 결과를 이해하기 힘들다는 단점이 있습니다. 비트 필드의 성능을 유지하고 단점을 보완하는 게 EnumSet이다. 
public class Text{
    public enum Style{
        BOLD, ITALIC, UNDERLINE, STRIKETHROUGH
    }
    //어떤 종류의 Set의 구현체이건 다 들어갈 수 있지만, EnumSet이 제일 좋다.
    public void applyStyles(Set<Style> styles){ ... }
}

클라이언트 코드
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));



※ ordinal을 배열 첨자로 사용하는 대신 EnumMap을 이용하라.

다음과 같은 클래스가 있다고 가정하자. 

class Herb {
    enum Type {
        ANNUAL, PERENNIAL, BIENNIAL
    }
    final String name;
    final Type type;
    public Herb(String name, Type type) {
        this.name = name;
        this.type = type;
    }
}

이 허브들을 품종별로 나열해야 한다고 해보자. 품종별로 Set 객체를 만든 후 각 객체에 맞는 허브를 넣으면 된다.

Herb[] garden = ... ; Set<Herb>[] herbsByType = (Set<Herb>[]) new Set[Herb.Type.values().length]; for(int i=0; i<herbsByType.length; i++) { herbsByType[i] = new HashSet<Herb>(); } for(Herb h : garden) { herbsByType[h.type.ordinal()].add(h); }

위에서 ordinal 인덱스를 이용해 배열을 참조하고 있다. orinal 값은 enum 상수가 변경될 시 변경될 가능성이 높다. 또한 잘못 참조하게 될 경우 배열 범위를 넘어서는 결과도 나올 수 있다. 이러한 단점을 해결하기 위해 EnumMap을 사용하면 된다. EnumMap은 enum 상수를 키로 사용할 목적으로 설계되어 성능이 아주 우수하다.

Map<Herb.Type, Set<Herb>> herbByType = new EnumMap<Herb.Type, Set<Herb>(Herb.Type.class);
for (Herb.Type t : Herb.Type.values()) {
    herbByType.put(t, new HashSet<Herb>());
}
for(Herb h : garden) {
    herbByType.get(h.type).add(h);
}

참고로 EnumMap 생성자는 키의 자료형을 나타내는 Class 객체를 인자로 받는다. 

또 다른 ordinal을 사용하는 코드를 보자. 다음 코드는 상태 변화 정보를 표현하는 예이다.

public enum Phase {
    SOLID, LIQUID, GAS;
    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
        //아래 배열의 행은 상전이 이전 enum, 열은 상전이 이후 enum를 나타냄
        private static final Transition[][] Transitions = {
                {null, MELT, SUBLIME},
                {FREEZE, null, BOIL},
                {DEPOSIT, CONDENSE, null}
        };

        public static Transition from(Phase src, Phase dst) {
            return Transitions[src.ordinal()][dst.ordinal()];
        }
    }
}
위 코드는 만약 Phase나 Transition을 수정해야 할 경우 제대로 고치지 않으면 NullPointException이나 배열 크기 이외의 값을 참조할 가능성이 높다. 또한 Phase 값이 추가 될 경우 Transitions 배열 크기는 엄청 커지게 된다. 따라서 ordinal 대신해서 EnumMap을 사용하는 게 좋다.
public enum Phase {
    SOLID, LIQUID, GAS;
    public enum Transition {
        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
        BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID), 
        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);

        private final Phase src;
        private final Phase dst;

        private Transition(Phase src, Phase dst) {
            this.src = src;
            this.dst = dst;
        }

        private static final Map<Phase, Map<Phase, Transition>> m = 
                new EnumMap<Phase, Map<Phase, Transition>>(Phase.class);

        static {
            for (Phase p : Phase.values()) {
                m.put(p, new EnumMap<Phase, Transition>(Phase.class));
            }
            for(Transition trans : Transition.values()) {
                m.get(trans.src).put(trans.dst, trans);
            }
        }

        public static Transition from(Phase src, Phase dst) {
            return m.get(src).get(dst);
        }
    }
}
위 코드는 복잡해 보이지만 ordinal보다 안정성이 뛰어나고 만약 새로운 Phase가 추가된더라도 ordinal과 달리 수정을 별로 안 해도 된다. 새로운 상태 PLASMA를 추가하려고 할 시 ordinal을 사용한 코드의 Transitions는 3*3 행렬에서 4*4행렬로 된다. 하지만 위 코드에서는 단지 Transition 상수만 몇 개 추가해주면 된다.



※ Override 어노테이션은 일관되게 사용하라.

Override 어노테이션을 사용하면 심각한 오류를 미리 막을 수 있다. 상위 클래스를 상속받아 재정의하는 모든 메서드에는 무조건 Override 어노테이션을 사용해야 한다. 단, abstract 추상 클래스를 상속 받아 재정의하는 메서드에 대해서는 Override 어노테이션을 꼭 붙일 필요는 없다.

+ Recent posts