Effective Java | #34. int 상수 대신 열거 타입을 사용하라

보람·2022년 5월 7일
0

Effective-Java

목록 보기
18/25

열거 타입

  • 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입
  • 사계절, 태양계의 행성 등이 좋은 예

정수 열거 패턴

public static final int APPLE_FUJI         = 0;
public static final int APPLE_PIPPIN       = 1;
public static final int APPLE_GRANNY_SMITH = 2;
  • 자바에서 열거타입을 지원하기 전에는 정수 열거 패턴을 한 묶음 선언해서 사용했다.
  • 타입 안전 보장 X, 표현력도 낮고 단점이 많다

문자열 열거 패턴

얘는..얘는..!! 더 나쁘다..!!😭

  • 문자열 값을 그대로 하드코딩하면 문자열에 오타가 있어도 컴파일 확인 X -> 런타임 버그 발생
  • 문자열 비교에 따른 성능 저하 발생

이런 열거 패턴의 단점을 말끔히 씻어주는..

열거 타입(enum type)

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
  • 완전한 클래스이다

  • 상수 하나당 (위 Apple 열거 클래스는 3개의 상수를 갖는다) 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개

  • (enum의 생성자는 private만 허용) 사실상 final 클래스임 -> 인스턴스들은 딱 하나씩만 존재 보장 => 인스턴스 통제된다.

  • 타입 안전성 제공

    • Apple 의 세 가지 값 외에 값을 보내면 컴파일 오류 발생
  • 열거 타입에는 각자의 이름공간이 있어서 이름이 같은 상수 공존 가능 (두개의 다른 열거타입에서 같은 이름의 상수가 존재해도 된다는 의미인듯?)

  • toString 메서드는 열거 타입의 적절한 문자열을 내어줌

  • 임의의 메서드나 필드 추가 가능

public enum Planet {
    MERCURY(3.302e+23, 2.439e6),
    VENUS  (4.869e+24, 6.052e6),
    ..길어서 생략
    
    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;

    // 생성자자에서 데이터(mass, radius)를 받아
    Planet(double mass, double radius) {
    	//인스턴스 필드에 저장
        this.mass = mass;
        this.radius = radius;
        //아래는 단순히 코드 최적화를 위함임
        surfaceGravity = G * mass / (radius * radius); 
    }
  • 열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
  • 열거 타입은 근본적으로 불변이라 모든 필드는 final이어야 함(item-17)
  • private 필드 + public 접근자 추천 (item-16)
  • Planet.values() : Planet 열거 타입 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드

열거타입 위치

  • 열거 타입 클래스 혹은 그 패키지에서만 유용한 기능은(클라이언트에게 노출할 이유가 X) private or package-private 메서드로 구현(item-15)
  • 널리 쓰이는 열거 타입은 톱레벨 클래스로 만들고
  • 특정 톱레벨 클래스에서만 쓰인다면 해당 클래스의 멤버 클래스(item-24)로 만들기

상수별로 다르게 동작할때는 switch 보다는 apply를

  • switch 는 상수 추가시 새로 수정해야 하기 때문에 빼먹을 위험이 있다.
    • 기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 유용
  • apply는 상수별로 다르게 동작하는 코드를 구현하는 추상 메서드, 즉 각 상수에서 자신에 맞게 재정의하는 방법
  • toString()을 오버라이딩하면 원하는 문자열 출력 가능
//연산자 열거 타입 클래스
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; }
    };
    
    private final String symbol;

    //생성자에서 symbol 받아서 넣어줌
    Operation(String symbol) { this.symbol = symbol; }

	//해당 상수 출력시 +나 -같은 문자열이 나오게 할 수 있다.
    @Override public String toString() { return symbol; }

    // 추상 메서드인 apply를 선언하여 상수가 추가되면 무조건 apply로 구현하게끔 한다.
    public abstract double apply(double x, double y);
}

열거 타입 상수끼리의 코드 공유 어려움

모든 상수에 중복해서 넣거나 필요한 메서드를 적절히 호출하게 한다면 가독성도 크게 떨어지고 오류 발생 가능성이 높아진다.

  • 가장 깔끔한 방법은 새로운 상수 추가시(생성자에서) 필요한 전략을 선택하도록 하는 것이다.
    아래는 상수별 급여를 정확히 계산하는 방법을 전략 열거 타입 패턴을 사용하여 구현된 예제이다.
package effectivejava.chapter6.item34;

import static effectivejava.chapter6.item34.PayrollDay.PayType.*;

// 코드 34-9 전략 열거 타입 패턴 (218-219쪽)
enum PayrollDay {
    //WEEKDAY | WEEKEND 는 잔업수당을 계산하는 PayType 전략 열거타입의 인스턴스
    //PayrollDay 생성자에서 이중 적당한 것을 선택
    MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY), THURSDAY(WEEKDAY), FRIDAY(WEEKDAY), 
    SATURDAY(WEEKEND), SUNDAY(WEEKEND);

	//사용하게 될 수당 타입
    private final PayType payType;

    //PayrollDay 생성자에서 잔업수당 set
    PayrollDay(PayType payType) { this.payType = payType; }
    
    int pay(int minutesWorked, int payRate) { //수당 함수
        //PayrollData 생성시 넣어준 전략 타입에 의해서 수당이 구해짐
        //switch 나 상수별 메서드 구현(apply)가 필요 없어짐 -> 복잡하지만 더 안전하고 유연함
        return payType.pay(minutesWorked, payRate);
    }

    // PayrollDay 열거 타입 내부에 선언된 PayType 전략 열거 타입
    enum PayType {
        WEEKDAY {
            int overtimePay(int minsWorked, int payRate) {
            //주간 추가 근무수당 : 8시간보다 이상으로 일한 경우 더 일한 시간 만큼의 절반
                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; //8시간

		//수당 반환 함수
        int pay(int minsWorked, int payRate) {
            int basePay = minsWorked * payRate;
            //기본수당 + 추가수당
            return basePay + overtimePay(minsWorked, payRate);
        }
    }

    public static void main(String[] args) {
        for (PayrollDay day : values())
            System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
    }
}

열거 타입이 적합한 경우

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

핵심 정리

  • 열거 타입은 확실히 정수 상수보다 뛰어남
    • 더 읽기 쉽고 안전&강력
  • 대다수 열거 타입이 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작하게 할 때는 필요
  • 드물게는 하나의 메서드가 상수별로 다르게 동작해야 할때가 있는데 이런 경우 switch보다는 상수별 메서드 구현할 것
  • 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.
profile
백엔드 개발자

0개의 댓글