[Effecttive Java] 6장 열거 타입과 애너테이션 - 아이템 34. int 상수 대신 열거 타입을 사용하라

배상규·2023년 12월 11일
0

이펙티브 자바

목록 보기
12/12
post-thumbnail

열거 타입이란?

일정 개수의 상수 값을 정의한 다음 그외의 값은 허용하지 않은 타입

그렇다면 이런 열거 타입을 왜 사용할까? 열거 타입 이전에 사용 했던 정수 열거 패턴을 살펴보며 이유를 알아보자!

정수 열거 패턴 단점

//정수 열거 패턴
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;
  • 타입 안전을 보장할 방법이 없으며, 표현력 또한 좋지 않다.
  • 평범한 상수를 나열이기 때문에 프로그램이 깨지기 쉽다.
  • 정수 열거 그룹에 속한 모든 상수를 순회하는 방법도 마땅치 않고, 상수가 몇 개인지 알 수 없다.

열거 타입

열거 타입은 앞선 정수 열거 패턴의 단점을 말끔이 없애 준다.

/// 간단한 열거 타입
public enum Apple {FUJI, PIPPIN, GRANNY_SMITH}
public enum Orange {NAVEL, TEMPLE, BLOOD}

열거타입의 특징

  • 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
  • 열거 타입 선언으로 만들어진 인스턴스는 딱 1개만 존재한다.

열거타입의 장점

  • 타입의 안전성을 제공한다. ex) Apple 열거 타입 인수에 Orange를 넘길 수 없다.
  • 같은 이름 상수 공존: 각자의 이름 공간이 있기 때문에
  • 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수 있다.

데이터와 메서드를 갖는 열거 타입

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.477e7);

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

열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.

  • 열거타입은 불변이기에 모든 필드는 final이어야 한다.
  • 필드를 public으로 선언해도 되지만, private으로 두고 별도의 public 접근 메서드를 두는 것이 낫다.

열거타입의 배열

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

자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드 값들은 선언된 순서로 저장된다.

상수별 메서드 구현

switch를 이용한 구현은 새로운 상수를 추가할 때마다 해당 case문도 추가해야해서 깨지기 쉽다.

상수별 메서드 구현
열거 타입에 추상 메서드를 선언하고, 각 상수별 클래스 몸체(constant-specific class body)를 각 상수에 맞게 재정의하는 방법

import java.util.*;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toMap;

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

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

상수별 동작 혼합 : 전략 열거 타입 패턴

열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.

package effectivejava.chapter6.item34;

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

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

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

추가하려는 메서드가 의미상 열거타입에 속하는 경우 다음과 같이 전략 열거 타입 패턴을 사용한다.

그렇지 않은 경우에는 switch를 적용해서 간단하게 만든다.


열거타입을 언제 사용할까?

  • 필요한 원소를 컴파일 타임에 다 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자

    • Ex) 태양계 행성, 한 주의 요일, 체스말
  • 열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요는 없다.

    • Ex) 메뉴 아이템, 연산 코드, 명령줄 플래그
  • 열거타입의 성능은 상수와 별반 다르지 않다

profile
기록에 성장을

0개의 댓글