Language/Java

[effective java] 아이템 34. int 상수 대신 열거 타입을 사용하라.

JOYERIM 2023. 5. 14. 20:41

 

 

 

 

 

 

6장 열거 타입과 어노테이션

 

 

 

 

Java에는 특수한 목적참조 타입이 두 가지 있다. 하나는 클래스의 일종enum, 다른 하나는 인터페이스의 일종어노테이션이다. 이번 6장은 두 가지 타입을 잘 사용하는 방법에 대해 다룬다 !

 

 

 

 

 

 

 

 


 

 

 

 

이번 아이템은 열거 타입의 장점과 열거 타입을 "잘" 작성하는 방법에 대해 다룬다. 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

정수 열거 패턴
: 단점이 너무 많은 ..

 

 

 

🚩 단점

타입 안전을 보장할 수 없다.

가독성이 좋지 않다.

namespace를 지원하지 않는다.
: 이름 충돌을 방지하기 위해서는 접두어를 써야 한다.

수정될 경우, 클라이언트 측에서 컴파일해야 한다.
: 클라이언트 파일에 남겨지기 때문에, 상수의 값이 바뀌면 클라이언트도 반드시 컴파일해야 하며, 컴파일 하지 않을 경우 이상 동작을 할 수 있다.

정수 상수는 문자열로 출력하기 까다롭다.

 

문자열 열거 패턴도 있으나 더 별로다 ^^..

 

예시 코드

 

// 정수 열거 패턴 - 상당히 취약하다.
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;

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

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

열거 타입(enum type)
: 열거 패턴의 단점을 완벽 해소

 

 

 

열거 타입인스턴스public static final 필드로 공개되며, 외부에서 접근할 수 있는 생성자는 제공하지 않는다.

 

열거 타입singleton이라고 보면 이해하기 쉽다 !

 

 

 

🚩 장점

컴파일 시점에 타입 안전하다.
: 다른 타입의 값을 할당하려 하면 컴파일 에러가 발생한다.

namespace가 존재한다.

수정되더라도 클라이언트 측에서 컴파일하지 않아도 된다.

임의의 메소드, 필드, 인터페이스를 구현할 수 있다.

 

 

예시 코드

 

public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

열거 타입에 임의의 메소드나 필드를 구현할 수 있다.

 

 

아래 코드는 태양계의 여덟 행성을 표현하는 열거 타입이다.

 

 

@Getter
public enum Planet {
    MERCURY(3.302e+23, 2.439e6), // 생성자에서 데이터를 받아 인스턴스 필드에 저장할 수 있다.
    VENUS(4.869e+24, 6.052e6),
    EARTH(5.975e+24, 6.378e6),
    MARS(6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN(5.685e+26, 6.027e7),
    URANUS(8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.447e7);
    
    /* 
    * 열거 타입은 불변이므로 final해야 한다.
    * 필드를 public으로 선언하는 것보단 private으로 두고 별도의 public 접근자 메소드를 두는 것이 좋다.
    */
    private final double mass;            // 질량(단위: 킬로그램)
    private final double radius;          // 반지름(단위: 미터)
    private final double surfaceGravity;  // 표면중력(단위: m / s^2)
    
    // 중력상수 (단위: m^3 / kg s^2)
    private static final double G = 6.67300E-11;
    
    // 생성자
    Planet(double mass, double radius) {
        this.mass = mass;
        this.radius = radius;
        this.surfaceGravity = G * mass / (radius * radius);
    }
    
    public double surfaceWeight(double mass) {
        return mass * surfaceGravity;
    }
}

 

public class Main {
	public static void main(String[] args) {
    	double earthWeight = Double.parseDouble(args[0]);
        double mass = earthWeight / Planet.EARTH.surfaceGravity();
        for (Planet p : Palanet.values()) 
        	System.out.println("%s에서 무게는 %f이다. %n", p, p.surfaceWeight(mass));
    }
}

 

 

 

 

 

 

 

 


 

 

 

 

 

 

열거 타입이 지원하는 메소드 (일부)

 

 

values() : 자신 안에 정의된 상수들의 값배열에 담아 반환하는 정적 메소드 (선언된 순서로 저장)
valueOf() : 상수 이름을 입력받아 그 이름에 해당하는 상수로 변환
toString() : 상수 이름문자열로 반환 (재정의할 경우 fromString 재정의를 고려)

 

 

 

 

 

 

 

 


 

 

 

 

 

 

 

열거 타입에서 상수를 하나 제거한다면?

 

 

 

클라이언트제거한 상수를 참조하지 않으므로 아무 영향이 없다.

제거된 상수를 참조하는 클라이언트디버깅에 유용한 컴파일 에러가 발생한다.

 

 

 

 

 

 

 


 

 

 

 

 

 

상수마다 동작이 달라져야 할 경우
: switch 문 대신 상수별 메소드를 구현하자. 

 

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

 

/* 상수가 뜻하는 연산을 수행한다. */
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 DIVIDE: return x / y;
    }
    throw new AssertionError("알 수 없는 연산: " + this);
}

 

위 코드는 새로운 상수를 추가하면 case 문도 추가해야 한다.

 

이 방법보다 열거 타입에 apply라는 추상 메소드를 선언하고 각 상수에 맞게 재정의하자 ! 이를 상수별 메소드 구현이라고 한다.

 

 

 

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

 

apply가 추상 메소드이므로 재정의하지 않으면 컴파일 에러로 알 수 있다.

 

 

 

 

 

 

 


 

 

 

 

 

 

열거 타입의 생성자 내에서 다른 상수를 사용할 경우

 

예시 코드

 

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

 

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

/* 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다. */
public static Optional<Operation> fromString(String symbol) {
    return Optional.ofNullable(stringToEnum.get(symbol));
}

 

⭐⭐⭐⭐⭐(제대로 이해 못 함)

 

 

fromString 메소드는 toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해주는 사용자 정의 메소드이다. fromString 메소드는 내부적으로 stringToEnum 맵을 사용하여 문자열Operation을 매핑하고 있다.

 

stringToEnum 맵열거 타입의 상수들을 생성한 후 정적 필드가 초기화될 때 추가된다. values 메소드를 사용하여 열거 타입의 상수들을 스트림으로 변환하고, toMap 메소드를 사용하여 맵으로 변환하고 있다.

 

열거 타입 생성자에서는 자기 자신을 맵에 추가할 수 없으므로, 별도의 정적 필드인 stringToEnum 맵을 사용하고 있다. 열거 타입의 생성자가 실행되는 시점에는 정적 필드들이 초기화되기 전이므로 자기 자신을 추가하지 못하게 하는 제약이 필요하다. 

 

 

 

 


 

 

 

 

 

 

 

 

열거 타입 상수끼리 코드 공유하는 경우
: 전략 열거 타입 패턴

 

 

 

상수별 메소드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다. 어떻게 하면 효율적으로 작성할 수 있을까? 주중/주말 일당 계산 메소드를 가진 열거 타입 예시로 살펴보자.

 

switch 문 사용

👉 새로운 값을 추가하려면 case를 추가해야 한다.

 

enum PayrollDay {
    MONDAY, TUESDAY, WEDSDAY, 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;
    }
}

 

핵심 로직 코드를 모든 상수에 중복해서 넣는다.
or
평일용/주말용으로 나눠 각각을 도우미 메소드로 작성한 다음 각 상수가 자신에게 필요한 메소드를 적절히 호출한다.

👉 가독성이 좋지 않고, 오류 발생 가능성이 오히려 더 높아진다.

 

 

 

전략 열거 타입 패턴

👉 해당 예시에서는 가장 좋은 방법 ! switch 문보다 가독성은 떨어지나 안전하고 유연하다.

 

새로운 상수를 추가할 때 "전략"을 선택하도록 하자.

 

enum PayrollDay {
    MONDAY, TUESDAY, WEDSDAY, THURSDAY, FRIDAY, 
    SATURDAY(PayTyoe.WEEKEND), SUNDAY(PayType.WEEKEND);
    
    private final PayType payType;
    
    PayrollDya(PayType payTyoe) {this.payType = payType;}
    
    int pay(int minutesWorked, int payRate) {
    	return payType.pay(minutesWorked, payRate);
    }
    
    /* 전략 열거 타입 */
    enum PayType {
        WEEKDAY {
            int overtimePay(int minusWorked, int payRate) {
                return minusWorked <= MINS_PER_SHIFT ? 0 :
                (minusWorked - MINS_PER_SHIFT) * payRate / 2;
            }
        },
        WEEKEND {
            int overtimePay(int minusWorked, int payRate) {
                return minusWorked * 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);
        }
    }
}

 

 

 

switch 문전략 열거 타입 패턴의 장단점을 잘 고려해서 판단하자 !

 

 

 

 


 

 

 

 

열거 타입 작성 기타 팁

 

 

◾ 열거 타입은 불변이므로 final해야 한다.(아이템 17)
◾ 필드를 public으로 선언하는 것보단 private으로 두고 별도의 public 접근자 메소드를 두는 것이 좋다.(아이템 16)
◾ 열거 타입을 선언한 클래스 혹은 패키지에서만 유용한 기능은 private 혹은 package-private 메소드로 구현하라.(아이템 15)
◾ 널리 쓰이는 열거 타입은 톱레벨 클래스로, 특정 톱레벨 클래스에서만 쓰인다면 해당 클래스의 멤버 클래스로 만든다.(아이템 24)

 

 

 

 

 

 

 

 


 

 

 

 

 

 

열거 타입은 언제 쓰면 좋을까?
: 필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합일 경우

 

 

열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다. 열거 타입은 나중에 상수가 추가돼도 바이너리 수준에서 호환되도록 설계되었다.