공부방/이펙티브 자바

int 상수 대신 열거 타입을 사용해라(아이템 34) - Enum

EVO. 2023. 9. 23. 20:20

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

정수 열거 패턴 - 상당히 취약하다

정수 열거 패턴 단점

  • 타입 안전 (type safety)를 보장할 방법이 없다
  • 타입 안전이 보장되지 않아 동등연산자로 비교하더라도 컴파일러에서 오류를 잡지 못하고 런타임때 오류가 발생할 가능성이 있음
  • 접두어를 써서 이름 충돌을 방지시켜야 한다 
  • 상수의 값이 바뀌면 반드시 다시 컴파일 해야한다
  • 정수 상수는 문자열로 출력하기가 다소 까다롭다 
  • 문자열 열거 패턴으로 위의 문제를 해결할 수 있겠지만 문자열 값을 개발자가 하드코드 하기에 더 나쁘다

타입 안전을 보장할 수 없다

자바의 열거 타입 특징(장점)

  • 열거타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다
  • 열거타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로(private) 사실상 final 이다
  • 따라서 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함이 보장된다 (싱글턴을 일반화한 형태(?) - 아이템 3 ...아직 이 말이 뭔지 모르겠다)
  • 열거타입은 컴파일타임 타입 안전성을 제공 
  • 열거타입에는 각자의 이름공간이 있어서 이름이 같은 상수도 존재할 수 있다 
  • 열거타입에 새로운 상수를 추가하거나 순서를 바꿔도 다시 컴파일하지 않아도 된다 (상수 값으로 컴파일되지 않고 상수의 명으로 등록되기 때문.)
  • 열거타입의 toString메서드는 출력하기에 적합한 문자열을 내어준다
  • 임의의 메서드나 필드를 추가할 수 있고 임의의 인터페이스를 구현할 수도 있다 (이미 Enum은 Object, Comparable, Serializable을 높은 품질로 구현했으며, 그 직렬화 형태도 문제없이 동작하게끔 구현해놓았다)
  • 열거타입에 정의한 상수를 지우면 그걸 참조하는 클라이언트 프로그램은 다시 컴파일해서 제거된 상수를 참조하는 줄에서 유용한 메세지를 담은 컴파일 오류가 발생한다. 만약 다시 컴파일 하지 않으면 런타임에, 역시 같은 줄에서 유용한 예외가 발생한다. (정수 열거 패턴에서는 이런 장점을 못 얻는다)

타입 안전성 제공

열거타입에서 상수 각각을 특정 데이터와 연결

  • 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 된다.
  • 열거타입은 근본적으로 불변이라 모든 필드는 final 이어야 한다(아이템 17)
  • 필드를 public으로 선언해도 되지만, private으로 두고 별도의 public 접근자 메서드를 두는 게 낫다(아이템 16)

열거 타입을 선언한 클래스 혹은 그 패키지에서만 유용한 기능은
private이나 package-private 메서드로 구현한다 

구현된 열거타입 상수는 자신을 선언한 클래스 혹은 패키지에서만 사용할 수 있는 기능을 담게 된다. 일반 클래스와 마찬가지로, 그 기능을 클라이언트에 노출해야 할 합당한 이유가 없다면 private으로, 혹은 package-private으로 선언하라(아이템 15)

 

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

1. switch문 이용

단점:

  • throw문을 생략하면 컴파일 조차 안된다
  • 깨지기 쉬운 코드이다. 예를 들면 새로운 상수가 추가되면 case 문도 추가해야 한다. 혹시라도 깜빡하면 컴파일은 되지만 새로 추가한 연산을 수행하려 할 때 런타임 에러가 발생한다

2. 상수별 메서드 구현

  • 열거타입에 apply라는 추상메서드를 선언하고 각 상수에서 자신에 맞게 재정의하는 방법이다
  • apply를 재정의 해야 한다는 사실을 까먹지 않을 수 있다 
  • apply는 추상메서드 이므로 재정의 하지 않으면 컴파일 오류로 알려준다 

추가적으로 상수별 메서드와 함께 상수별 데이터를 결합한 방식이다

 

열거타입용 fromString 메서드 구현하기

위 코드에서 toString이 반환하는 문자열을 해당 열거타입 상수로 변환해주는 코드를 추가할 수 있다.

(단 모든 상수의 문자열 표현이 고유해야 한다)

열거 타입용 fromString 메서드 구현하기

 

여기서 아직 이해가 힘든 구절이 있다. 이펙티브자바(216쪽) 4번째 줄: 
하지만 열거 타입 상수는 생성자에서 자신의 인스턴스를 추가할 수 없다.이렇게 하려면 컴파일오류가 나는데 만약 이 방식이 허용되었다면  런타임에 NullPointerException이 발생했을 것이다. 

열거타입의 정적 필드 중 열거타입의 생성자에서 접근할 수 있는 것은 상수 변수 뿐이다.(아이템 24) 열거타입 생성자가 실행되는 시점에는 정적 필드들이 아직 초기화되기 전이라, 자기 자신을 추가하지 못하게 하는 제약이 꼭 필요하다. 

 

상수별 메서드 구현에서 코드를 공유하고 싶을 때

 

1. 값에 따라 분기하여 코드를 공유하는 열거타입 - switch문 (과연 좋은 방법일까)

위 코드는 주말에는 2배비싼 가격을 지불해야 하고 평일에는 반값인 가격을 지불해야 하는 예제이다. 

분명 간결하지만, 관리 관점에서는 위험한 코드이다. 만약 공휴일 같은 날을 열거 타입에 추가하려면 그 값을 처리하는 case문을 잊지말고 쌍으로 넣어주어야 한다. 공휴일에도 2배 비싼 가격을 지불 받고 싶은데 까먹고 추가하지 않아서 반값에 가격을 파는 불상사(?)가 생길 수 있다. 

 

이에 해결하는 방법은 두가지가 있다.

첫째, 모든 상수에 똑같은 코드를 중복해서 넣기 

둘째, 계산 코드를 평일용과 주말용으로 나눠 각각을 도우미 메서드로 작성한 다음 각 상수가 자신에게 필요한 메서드를 적절히 호출 

사실 두 방식 모두 코드가 장황해져서 가독성이 크게 떨어지고 오류 발생 가능성이 높아진다. 

 

이를 모두 깔끔하게 해결할 수 있는 최고의 방법을 소개한다

 

전략 열거 타입 패턴

새로운 상수를 추가할 때 '전략'을 선택하도록 한다. 주말/평일 메서드를 private 중첩 열거타입으로 옮기고 PayrollDay 열거 타입의 생성자에서 적당한 것을 선택한다.

https://github.com/minsang-alt/java/blob/main/src/main/java/com/example/javaspring/enumTest/PayrollDay.java 에서        PayrollDay 열거 타입은 가격 계산을 전략 열거 타입에 위임하여, switch문이나 상수별 메서드 구현이 필요없게 된다. 이 패턴은 switch문보다 복잡하지만 더 안전하고 유연하다. 

 

 

정리 : 열거 타입은 언제 써야하는가?

  • 필요한 원소를 컴파일타임에 알 수 있는 상수 집합이라면 항상 열거타입을 사용해야 한다
  • 열거 타입에 정의된 상수 개수가 영원히 고정/불변일 필요는 없다
  • 열거 타입은 정수 상수보다 더 읽기 쉽고 안전하고 강력하다
  • 각 상수에 특정 데이터와 연결 짓거나 상수마다 다르게 동작해야 할때 열거타입을 쓰자
  • 하나의 메서드가 상수별로 다르게 동작해야 할 때 switch문 대신 상수별 메서드 구현을 사용하자 
  • 열거타입 상수 일부가 같은 동작을 공유한다면 전략 열거 타입 패턴을 사용하자 

 

출처 : https://www.yes24.com/Product/Goods/65551284

 

이펙티브 자바 Effective Java 3/E - 예스24

자바 플랫폼 모범 사례 완벽 가이드 - Java 7, 8, 9 대응자바 6 출시 직후 출간된 『이펙티브 자바 2판』 이후로 자바는 커다란 변화를 겪었다. 그래서 졸트상에 빛나는 이 책도 자바 언어와 라이브

www.yes24.com