[이펙티브 자바 아이템34. int 상수 대신 열거 타입을 사용하라]

박상준·2024년 6월 9일
0

이펙티브 자바

목록 보기
11/16

열거 타입이란?

  • 일정 개수의 상수 값을 정의한 타입이다.
  • ex
    • 사계절, 태양계의 행성, 카드 게임의 카드 종류 등이 있다.

기존 정수 열거 패턴의 문제점

  • 타입의 안정성이 부족하다.
    • 타입 혼동으로 인한 오류 기능
  • 표현력 부족
    • 서로 다른 그룹의 상수끼리 비교가 가능하다.
  • 이름의 충돌
    • 접두어 사용으로 이름 충돌을 방지해야한다는 문젝 있다.
  • 클라이언트 코드의 꺠짐
    • 상수 값이 변경되는 경우 클라이언트 재컴파일이 필요하다.
  • 가독성이 떨어진다.
    • 값이 숫자로만 표현되어서 디버깅이 매우 어렵다.
  • 순회가 어렵다
    • 상수 개수의 파악이나 순회 방법이 어렵다
  • 문자열 열거 패턴의 문제점
    • 하드코딩으로 인한 버그 발생 및 성능 저하가 우려된다.

자바의 열거 타입의 사용

  • 열거 타입은 완전한 형태의 클래스이다.
  • 각 상수는 열거 타입의 인스턴스로 생성된다.
  • 생성자를 제공치 않아서 인스턴스를 직접 생성할 수 없다.
  • 컴파일타임시에 타입 안정성을 제공한다.
  • 이름 공간을 가지고 있어 이름 충돌을 방지할 수 있다.
  • toString 같은 메서드로 의미 있는 문자열을 출력할 수 있다.

예제

  • 정수 열거 패턴
    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 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;
    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 mass() { return mass; }
    public double radius() { return radius; }
    public double surfaceGravity() { return surfaceGravity; }
    public double surfaceWeight(double mass) {
        return mass * surfaceGravity;
    }
}
  • 행성에서의 무게 계산
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));
        }
    }
}
  • 특정 클래스나 패키지 내에서만 유용한 기능은 private 이나 package-private 메서드로 구현한다.
  • 널리 쓰이는 열거 타입은 톱레벨 클래스로서, 특정 톱레벨 클래스에서만 쓰인다면 멤버 클래스로 만든다.

연산 열거 타입과 각 상수의 동작 정의

  • 예제 - 연산 열거 타입
    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);
    
        // valueOf 메서드를 구현하는 방법
        private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(Collectors.toMap(Object::toString, e -> e));
    
        public static Optional<Operation> fromString(String symbol) {
            return Optional.ofNullable(stringToEnum.get(symbol));
        }
    }
    • 각 연산이 상수별로 다른 동작을 하도록 apply 메서드를 재정의하였음.
    • 문자열 표현을 위하여 symbol 필드를 추가하고 toString 메서드를 재정의하였다.
    • fromString 메서드는 문자열로부터 해당 연산을 찾을 수 있도록 하는 코드임.

규칙

  • 톱레벨 클래스
    • 일반적으로 사용되는 열거 타입의 경우 톱레벨 클래스로 정의한다
  • 멤버 클래스
    • 해당 멤버 클래스에서만 사용하는 열거타입의 경우 해당 클래스의 멤버 클래스로 정의한다

      public class Member {
          private int memberId;
          private MemberType memberType;
          
          private enum MemberType {
              ADMIN("ADMIN"), USER("USER");
              
              private String memberType;
              
              MemberType(String memberType) {
                  this.memberType = memberType;
              }
              
              public String getMemberType() {
                  return memberType;
              }
          }
      }
    • Member 외에는 만약 사용되는 않는다면 이런식으로 클래스 내부에서 멤버로 정의할 수 있음.

자바 열거 타입의 활용

  • 급여 명세서에서 요일을 표현하는 열거 타입을 통해 상수별 메서드 구현 및 전략 열거 타입 패턴 구현가능

    급여 명세서에서 사용할 요일 열거 타입

    • switch 문을 이용하여 요일에 따라 분기하는 예시
      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 문을 놓칠 가능성이 높다.
      • 이를 방지하기 위해 전략 열거 타입 패턴을 사용한다고 한다.
    • 전략 열거 타입 패턴
      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 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;
                  }
              };
              
              private static final int MINS_PER_SHIFT = 8 * 60;
              
              abstract int overtimePay(int mins, int payRate);
              
              int pay(int minsWorked, int payRate) {
                  int basePay = minsWorked * payRate;
                  return basePay + overtimePay(minsWorked, payRate);
              }
          }
      }
      • 전략 열거 타입에서 별도의 상수별로 다른 동작을 하도록 설정할 수 있다
      • 해당 예제는 주중에는 8시간 외의 일한 시간에는 별도의 8시간 외의 야근수당에 대해서는 0.5 배의 페이를 추가하고, 주말에는 all time 0.5 배를 지급하는 별도의 동작을 정의할 수 있다.

정리

  • 정수 상수보다 열거 타입을 사용하자.
  • 하나의 메서드가 상수별로 다르게 동작해야할 경우 별도의 상수별 메서드 구현을 사용하자.
  • 열거 타입 상수 일부가 같은 동작을 공유하면 전략 열거 타입 패턴을 사용하도록 하자.
profile
이전 블로그 : https://oth3410.tistory.com/

0개의 댓글