맞는데 왜 틀릴까..?

Java/Effective Java

[Effective Java] Item 34. int 상수 대신 열거 타입을 사용하라

안도일 2023. 5. 16. 16:00

 

정수 열거 패턴

 

자바에서 열거 타입을 지원하기 전에는 다음 코드처럼 정수 상수를 한 묶음으로 선언해서 사용했다.

 

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;

 

하지만 이 정수 열거 패턴은 상당히 취약하다.

 

 

1. 타입 안전을 보장할 방법이 없으며 표현력도 좋지 않다.

 

  • 오렌지를 건네야 할 메서드에 사과를 보내고 == 로 비교하더라도 컴파일러는 경고메시지를 보내지 않는다.
  • 자바가 정수 열거 패턴을 위한 별도 namespace를 지원하지 않기 때문에 접두어를 써서 이름 충돌을 방지한다.        ex) ELEMENT_MERCURY (수은), PLANET_MERCURY (수성)

 

 

2. 정수 열거 패턴을 사용한 프로그램은 깨지기 쉽다.

 

  • 평범한 상수를 나열한 것 뿐이라 컴파일하면 그 값이 클라이언트 파일에 그대로 새겨진다.
  • 따라서 상수의 값이 바뀌면 클라이언트도 반드시 다시 컴파일해야 한다.

 

 

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

 

  • 그 값을 출력하거나 디버거로 살펴보면 단지 숫자로만 보여서 도움되지 않는다. 
  • 정수 대신 문자열 상수를 사용하는 변형 패턴이 있는데, 이 변형은 더 나쁘다.
  • 상수의 의미를 출력할 수 있다는 점은 좋지만, 프로그래머가 실수로 문자열 상수의 이름 대신 문자열 값을 사용할 수 있고, 그 문자열에 오타가 있어도 컴파일러는 확인할 수 없다.

 

그러니 자바에서 제공해주는 열거 타입을 사용하자.


 

열거 타입

 

자바의 열거 타입은 완전한 형태의 클래스라서 다른 언어의 열거 타입보다 훨씬 강력하다.

 

public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }

public enum Orange { NAVEL, TEMPLE, BLOOD }

 

  • 열거 타입 자체는 클래스
  • 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
  • 열거 타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final이다. 따라서 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함이 보장된다.

 

열거 타입 장점

 

 

열거 타입은 컴파일타임 타입 안전성을 제공한다.

  • Apple 열거 타입을 매개변수로 받는 메서드를 선언했다면 건네받은 참조는 Apple의 3가지 값 중 하나가 확실하다. 만약 다른 타입의 값을 넘기려 하면 컴파일 오류가 난다.

 

열거 타입에는 각자의 namespace가 있어서 이름이 같은 상수도 평화롭게 공존한다.

  • Day.SUNDAY와 Holiday.SUNDAY는 다르다

 

열거 타입에 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일하지 않아도 된다.

  • 공개되는 것은 오직 필드의 이름뿐이라, 정수 열거 패턴과 달리 상수 값이 클라이언트로 컴파일되어 각인되지 않기 때문이다.

 

열거 타입의 toString 메서드는 출력하기에 적합한 문자열을 내어준다.

 


 

열거 타입 확장

 

열거 타입에는 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현하게 할 수도 있다.

 

열거 타입에 메서드나 필드가 왜 필요할까?

 

태양계의 8개 행성을 예로 들어보자.

 

8개의 행성 데이터와 메서드를 갖은 열거 타입

 

 

 

  • 열거 타입 상수 오른쪽 괄호 안 숫자는 생성자에 넘겨지는 매개변수다.
  • 열거 타입 상수 각각을 특정 데이터와 연결 지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
  • 열거 타입은 근본적으로 불변이라 모든 필드는 final이어야 한다.
  • 필드를 public으로 선언해도 되지만, private으로 두고 별도의 public 접근자 메서드를 두자.

 

Planet 열거 타입은 단순하지만 강력하다.

어떤 객체의 지구에서의 무게를 입력받아 8개 행성에서의 무게를 출력하는 일을 다음과 같이 짧은 코드로 작성할 수 있다.

 

 

  • 열거 타입은 자신 안에 정의된 상수들의 값을 배열에 담아 반환하는 정적 메서드 values를 제공한다.
  • 값들은 선언된 순서로 저장된다.
  • 각 열거 타입 값의 toString 메서드는 상수 이름을 문자열로 반환한다.

 

 

열거 타입에서 상수 제거

 

열거 타입에서 상수 하나를 제거하면 어떻게 될까?

 

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

 

제거된 상수를 참조하는 클라이언트 :

클라이언트 프로그램을 다시 컴파일하면 제거된 상수를 참조하는 줄에서 디버깅에 유용한 메시지를 담은 컴파일 오류가 발생한다. 컴파일하지 않으면 런타임에 유용한 예외가 발생한다.

 

 

일반 클래스와 마찬가지로, 그 기능을 클라이언트에 노출해야 할 합당한 이유가 없다면 private으로, 혹은 pacakge-private으로 선언하라.

 

널리 쓰이는 열거 타입은 톱레벨 클래스로 만들고, 특정 톱레벨 클래스에서만 쓰인다면 해당 클래스의 멤버 클래스로 만들자.

 


 

상수마다 동작이 달라져야 하는 상황

 

예를 들어 사칙연산 계산기의 연산 종류를 열거 타입으로 선언하고, 실제 연산까지 열거 타입 상수가 직접 수행하면 좋겠다.

 

 

값에 따라 분기하는 열거 타입

 

 

  • 동작은 하지만 만족하지 못한다.
  • 마지막 throw 문은 실제로 도달할 일은 없지만 기술적으로는 도달할 수 있기 때문에 생략하면 컴파일조차 되지 않는다.
  • 새로운 상수를 추가하면 해당 case문도 추가해야 한다.
  • 깜빡한다면 컴파일은 되지만 런타임 오류가 난다.

 

 

상수별 메서드 구현 : 열거 타입에 apply라는 추상 메서드를 선언하고 각 상수별 클래스 몸체, 즉 각 상수에서 자신에 맞게 재정의 하자

 

 

상수별 메서드 구현을 활용한 열거 타입

 

 

  • 새로운 상수를 선언할 때 apply를 재정의해야 한다는 사실을 잊기 어렵다.
  • apply가 추상 메서드이므로 재정의하지 않았다면 컴파일 오류로 알려준다.

 

 

 

상수별 메서드 구현을 상수별 데이터와 결합

 

Operation의 toString을 재정의해 해당 연산을 뜻하는 기호를 반환하도록 함.

 

 

  • toString이 계산식 출력을 편하게 해 준다.
  • 열거 타입에는 상수 이름을 입력받아 그 이름에 해당하는 상수를 반환해 주는 valueOf(String) 메서드가 자동 생성된다.
  • 열거 타입의 toString 메서드를 재정의하려거든 toString이 반환하는 문자열을 해당 열거 타입 상수로 변환해 주는 fromString 메서드도 함께 제공하자.

 

열거 타입용 fromString 메서드

 

 

 


 

상수별 메서드 구현에는 열거 타입 상수끼리 코드를 공유하기 어렵다.

 

 

급여명세서에서 쓸 요일을 표현하는 열거 타입을 예로 살펴보자.

 

 

직원의 시간당 기본임금과 그날 일한 시간이 주어지면 일당을 계산해 주는 메서드

주중에 오버타임이 발생하면 잔업수당이 주어지고, 주말에는 무조건 잔업수당이 주어진다.

 

 

  • 간결하지만 관리 관점에서는 위험하다.
  • 휴가와 같은 새로운 값을 열거 타입에 추가하려면 그 값을 처리하는 case문을 잊지 말고 넣어줘야 하기 때문이다.
  • 해당 case를 넣지 않는다면 프로그램은 잘 돌아가지만 휴가 기간에 열심히 일해도 평일과 똑같은 임금을 받게 된다.

 

 

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

 

전략 열거 타입 패턴

 

 

  • 잔업수당 계산을 private 중첩 열거 타입(PayType)으로 옮기고 PayrollDay 열거 타입의 생성자에서 이 중 적당한 것을 선택한다.
  • 이러면 PayrollDay 열거 타입은 잔업수당 계산을 그 전략 열거 타입에 위임하여 switch 문이나 상수별 메서드 구현이 필요 없게 된다.

 


 

열거 타입에서의 switch 문

 

  • switch 문은 열거 타입의 상수별 동작을 구현하는데 적합하지 않다.
  • 하지만 기존 열거 타입에 상수별 동작을 혼합해 넣을 때는 switch 문이 좋은 선택이 될 수 있다.

 

아래처럼 각 이미 구현되어 있는 Operation 열거 타입 연산의 반대 연산을 반환하는 메서드가 필요하다고 할 때 사용하면 효과적이다.

 

 

 

결론

 

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

태양계 행성, 한 주의 요일처럼 본질적으로 열거 타입을 사용할 때 사용하자.

메뉴, 연산 등 허용하는 값 모두를 컴파일타임에 이미 알고 있을 때 사용하자.

 

열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요 없다.

 

  • 열거 타입은 읽기 쉽고 안전하고 강력하다.
  • 대다수 열거 타입이 명시적 생성자나 메서드 없이 사용되지만, 각 상수를 특정 데이터와 연결 짓거나 상수마다 다르게 동작해야 할 때 필요할 수 있다. 이럴 때는 switch 문 대신 상수별 메서드 구현을 사용하자.
  • 열거 타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자.