item 34 : int 상수 대신 열거 타입을 사용하라
1. int 상수 패턴, 문자열 상수 패턴 개념과 문제점(int enum pattern)
열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입이다.
1) int 상수 패턴의 문제점
아래의 코드는 경우의 수가 한정될 때 각 경우를 상수 값으로 치환하여 표현하는 것이다.
타입 안전을 보장할 방법이 없으며 표현력도 좋지 않다.
오렌지를 건네야 할 메서드에 사과를 보내 고 동등 연산자(=)로 비교하더라도 컴파일러는 아무런 경고 메시지를 출력하지 않는다.
정수 열거 패턴을 위한
별도 이름공간(namespace)
을 지원 하지 않는다.
사과용 상수의 이름은 모두 APPLE_로 시작하고, 오렌지용 상수는 ORANGE_로 시작한다. 정수 열거 패턴을 위한 별도 이름공간
을 지원하지 않기 때문에 어쩔 수 없이 접두어를 써서 이름 충돌을 하는 것이다.
정수 열거 패턴을 사용한 프로그램은 깨지기 쉽다.
평범한 상수를 나열한것 뿐이라 컴파일하면 그 값이 클라이언트 파일에 그대로 새겨진다.
따라서 상수의 값이 바뀌면 클라이언트도 반드시 다시 컴파일해야 한다.
정수 상수는 문자열로 출력하기 까다롭다.
값을 출력하거나 디버거로 살펴보면 (의미가 아닌) 단지 숫자로만 보여서 썩 도움이 되지 않는다. 같은 정수 열거 그룹에 속한 모든 상수를 한 바퀴 순회하는 방법도 마땅치 않다. 심지어 그 안에 상수가 몇 개인지도 알 수 없다.
2) 문자열 열거 패턴 (string enum pattern)
정수 대신 문자열 상수를 사용하는 변형 패턴도 있는데 더 안좋다. 수의 의미를 출력할 수 있다는 점은 좋지만, 경험이 부족한 프로그래머가 문자열 상수의 이름 대신 문자열 값을 그대로 하드코딩하게 만들기 때문이다.
이렇게 하드코딩한 문자열 에 오타가 있어도 컴파일러는 확인할 길이 없으니 자연스럽게 런타임 버그가 생긴다. 문자열 비교에 따른 성능 저하 역시 당연한 결과다.
2. 자바 열거 타입(enum type)
열거 패턴들의 단점을 말끔히 씻어주는 동시에 여러 장점을 안겨주는 대안
1) 자바 열거 타입(enum type)
자바의 열거 타입은 완전한 형태의 클래스라서 (단순한 정숫 값일 뿐인) 다른 언어의 열거 타입보다 훨씬 강력하다.
2) 열거 타입의 아이디어
열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다.
열거 타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final이다.
인스턴스가 통제된다. 인스턴스들은 오직 하나만 존재한다.
원소가 하나이면 싱글턴으로 볼 수 있다. 싱글턴은 원서가 하나뿐인 열거 타입이라고 할 수 있고, 거꾸로 열거 타입은 싱글턴을 일반화한 형태라고 할 수 있다.
열거 타입은 컴파일타임 타입 안전성을 제공한다.
이전의
int 상수 패턴
처럼ORANGE
가 갈 곳에APPLE
이 간다면, 명확히 타입 에러가 발생한다.타입이 다른 열거 타입 변수에 할당하려 하거나 다른 열거 타입의 값끼리 == 연산자로 비교하려는 꼴이기 때문이다.
네임스페이스를 제공하여, 이름이 같은 상수도 평화롭게 공존할 수 있다.
APPLE.RED
와ORANGE.RED
는 구분된다.
toString()
이 출력하기에 적합한 문자열을 내어준다.
열거 타입에는 다양한 메서드나 필드도 추가 가능하다.
추가로 임의의 인터페이스도 구현하게 할 수 있다.
공개되는 것이 오직 필드의 이름뿐이라, 정수 열거 패턴과 달리 상수 값이 클라이언트로 컴파일되어 각인되지 않기 때문이다
3) 예시
태양계의 여덞 행성에 대한 열거 타입을 만드는 것도 그리 어렵지 않다.
각 행성에 는 질량과 반지름이 있고, 이 두 속성을 이용해 표면중력을 계산할 수 있다. 따 라서 어떤 객체의 질량이 주어지면 그 객체가 행성 표면에 있을 때의 무게도 계산할 수 있다.
열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드 에 저장하면 된다.
열거 타입은 근본적으로
불변
이라 모든 필드는final
이어야 한다.필드를 public으로 선언해도 되지만, private으로 두고 별도의 public 접근자 메서드를 두는 게 낫다.
Planet의 생성자에서 표면중력을 계산해 저장한 이유는 단순히
최적화
를 위해서다.
열거 타입(enum)을 정의할 때 각 열거 상수에 특정한 데이터를 연결하는 방식과 불변성을 유지하는 방법 좀 더 자세히
1. 열거 타입 상수와 데이터 연결
열거 타입에서 각 상수(행성)에 데이터를 연결하려면, 해당 데이터를 생성자를 통해 받아 필드에 저장한다. Planet
예제에서 mass
(질량)와 radius
(반지름) 데이터를 각 행성에 연결하는 방식이 이에 해당한다.
위처럼 각 행성 상수에 mass
와 radius
값을 전달하면, 이 값들이 생성자에 의해 인스턴스 필드에 저장된다. 이를 통해 각 상수는 자신만의 고유한 데이터를 가지게 된다.
2. 불변성과 필드 선언
열거 타입은 불변성을 유지하는 것이 중요하다. 즉, 한 번 생성된 열거 타입 상수의 데이터는 수정할 수 없어야 한다. 이를 위해 모든 필드는 final
로 선언된다.
이렇게 필드를
final
로 선언하면 초기화된 후 변경이 불가능하다.
열거 타입인 Planet
은 각 행성의 mass
, radius
, surfaceGravity
값을 생성 시점에만 설정하고 이후에는 변경할 수 없다.
3. 접근자 메서드의 사용
비록 final
필드라 하더라도 직접 값을 읽기 위해서는 직접 필드를 공개(public으로 선언)하기보다는 private으로 선언하고 public 접근자 메서드를 추가하는 것이 좋다.
위처럼 mass
, radius
, surfaceGravity
를 반환하는 public 메서드를 제공하여 값을 읽도록 하는데, 이 방식이 선호되는 이유는 다음과 같다:
캡슐화: 내부 구현에 대한 세부 정보를 숨기고 필요한 정보만 외부에 공개할 수 있다.
추후 변경 용이성: 필드나 메서드를 수정하거나 추가할 때도, 외부 사용 방식에 영향을 주지 않고 유연하게 대응할 수 있다.
이렇게 Planet
예제에서 각 행성은 final
필드로 선언된 불변 데이터를 가진다. 데이터에 접근할 때는 mass()
, radius()
, surfaceGravity()
와 같은 접근자 메서드를 통해서만 값을 얻을 수 있도록 하여 안전하고 일관된 방식으로 필드에 접근하게 된다.
4) enum.value()
value()메서든느 정적 메서드로 자신 안에 정의된 상수들의 값을 배열에 담아 반환해준다. 값들은 선언된 순서로 저장된다.
toString 메서드 : 상수 이름을 문자열로 반환하므로 println과 printf로 출력하기 좋음
열거 타입에서 상수를 하나 제거하면 어떻게 되지?
제거한 상수를 참조하 지 않는 클라이언트에는 아무 영향이 없다. WeightTable 프로그램에서라면 단지 출력하는 줄 수가 하나 줄어들 뿐이다.
그렇다면 제거된 상수를 참조하는 클라이언트는 어떻게 될까?
클라이언트 프로그램을 다시 컴파일하면 제 거된 상수를 참조하는 줄에서 디버깅에 유용한 메시지를 담은 컴파일 오류가 발생할 것이다.
정수 열거 패턴에서는 기대할 수 없는 가장 바람직한 대응
3. 열거타입 상수마다 동작이 달라지는 메서드 구성
클라이언트 코드
1) switch
문을 사용하여 열거 상수별 동작 구현(bad)
switch
문을 사용하여 열거 상수별 동작 구현(bad)
apply
메서드가switch
문을 사용하여this
에 따른 연산을 수행한다.
동작은 하지만 마지막의 throw 문은 실제로는 도달할 일 이 없지만 기술적으로는 도달할 수 있기 때문에 생략하면 컴파일조차 되지 않는다.
더 나쁜 점은 깨지기 쉬운 코드라는 사실이다.
새로운 상수를 추가하면 해당 case 문도 추가해야 한다. 혹시라도 깜빡한다면, 컴파일 은 되지만 새로 추가한 연산을 수행하려 할 때
알 수 없는 연산
이라는 런타임 오류를 내며 프로그램이 종료된다.
2) switch 단점 개선: 상수별 메서드 구현을 사용하여 동작 정의
열거 타입에서 상수별로 서로 다른 동작을 정의하고 싶을 때, 각 상수마다 메서드를 개별적으로 정의할 수 있다. 이를 통해 각 상수가 서로 다른 계산 방식이나 로직을 가질 수 있다.
상수별 클래스 몸체: 각 열거 상수(
PLUS
,MINUS
,TIMES
,DIVIDE
)는apply
메서드를 개별적으로 구현한다.장점:
추가 시 안전성: 새 연산을 추가할 때 각 상수별로
apply
추상메서드를 재정의하지 않으면 컴파일러가 경고를 띄워, 구현 누락을 방지한다.간결하고 유지보수가 용이: 각 연산의 동작이 해당 상수에 직접 정의되어 있어 코드가 더 직관적이며 유지보수에 용이하다.
3) 생성자 이용 및 공통 메서드 재정의
열거 타입 상수 정의: 각 연산(
PLUS
,MINUS
,TIMES
,DIVIDE
) 상수는 개별apply
메서드를 구현하여 상수별로 다른 연산을 수행한다.symbol
필드: 각 상수에 대응되는 기호(+
,-
,*
,/
)를 저장하는symbol
필드를 사용하여toString
메서드가 해당 기호를 반환하도록 설정한다.fromString
메서드: 연산 기호(+
,-
,*
,/
)를 이용해 적절한Operation
상수를 찾을 수 있도록fromString
메서드를 구현한다.operation
메서드: 문자열 형태의 연산("3 * 5"
)을 받아 기호를 기준으로Operation
을 찾아 연산 결과를 반환하는 메서드이다.
열거 상수별
apply
메서드 구현: 각 상수가 고유한 동작을 갖도록apply
를 재정의한다. 예를 들어PLUS
는x + y
를 수행하고,TIMES
는x * y
를 수행한다.symbol
필드와toString
재정의: 기호를 담는symbol
필드를 통해toString
메서드를 재정의하여 기호가 출력되도록 한다.fromString
메서드: 문자열로 받은 기호를 통해 해당하는Operation
상수를 찾아주는 메서드로,Map
을 통해 기호와 상수를 빠르게 매핑하여 반환한다.
toString 메서드 재정의 시 고려해줘야 할 점 : fromString 메서드도 함께 제공
문자열로부터 열거 타입 상수를 안전하게 찾기 위해
fromString
메서드를 추가하여 기호에 따라 열거 타입 상수를 매핑한다.Map
을 사용하여 문자열과 열거 타입 상수를 매핑하고, 주어진 문자열을 통해 열거 상수를 찾는다.fromString()을 만들어두면 편리하게 다시 문자열을 enum으로 변경할 수 있다.
4. 열거타입 상수끼리 코드 공유해보기
각각 switch
문을 사용한 방법과 전략 패턴을 활용한 방법으로, 열거 타입 상수마다 다른 로직을 제공하면서도 공통된 코드를 효과적으로 공유할 수 있다. 각 접근 방식의 장단점을 비교하여 어떤 상황에서 더 적합한지 알아보자
1) switch
문을 이용한 열거 타입 상수 코드 공유
switch
문을 이용한 열거 타입 상수 코드 공유첫 번째 방식은 switch
문을 사용하여 주중과 주말을 구분하는 PayrollDay
열거 타입이다. switch
문을 통해 상수마다 다른 로직을 적용하면서도 기본적인 코드 흐름을 공유할 수 있다.
예제 코드
출력 결과
장점과 단점
장점:
switch
문을 통해 코드를 간결하게 유지하며, 주중과 주말에 따라 다른 임금을 계산하는 코드를 쉽게 구현할 수 있다.단점: 새로운 상수를 추가할 때
switch
문에서 새로운case
절을 추가하는 것을 잊을 위험이 있다. 이러한 코드 유지보수의 리스크가 있으며, 실수로 누락된 경우에는 런타임 오류가 발생할 수 있다.
2) 전략 상수 패턴을 사용한 열거 타입 상수 코드 공유
두 번째 방식은 전략 패턴을 활용하여 PayrollDay
열거 타입에 주중과 주말의 임금 계산 로직을 별도로 정의하는 방법이다. PayType
이라는 중첩 열거 타입을 통해 WEEKDAY
와 WEEKEND
를 정의하고, 각각 다른 초과 근무 임금 계산 방식을 제공한다.
동작 방식
주중과 주말의 임금 계산 로직을 분리: 주중 근무(
WEEKDAY
)와 주말 근무(WEEKEND
)의 초과 근무 계산 방식이 서로 다르며, 각 상수에 맞는PayType
을 할당하여 각각 다른 동작을 수행한다.코드 실행:
PayrollDay
의 각 상수는 주어진PayType
을 통해 적절한 초과 근무 임금을 계산한다.
장점과 단점
장점: 새로운 상수가 추가될 때 각 상수가
PayType
을 필수로 지정해야 하므로 실수로 빠뜨릴 위험이 적다. 더불어 각 상수에 따른 로직을 전략 패턴으로 분리함으로써 코드를 좀 더 유연하게 관리할 수 있다.단점:
switch
문에 비해 다소 복잡하지만, 코드의 안전성과 확장성을 높이는 데 유리
3) 전략 패턴과 switch
문 비교
switch
문 비교switch
문: 간단하게 작성할 수 있지만, 열거 타입 상수별 동작을 일괄적으로 수정할 때 불편하며, 유지보수가 어렵고 실수가 발생할 여지가 있다.전략 상수 패턴: 열거 타입 내부에서 전략 패턴을 활용하여 상수별로 다른 로직을 안전하게 관리할 수 있다. 이 방법은 특히 코드의 확장성과 안전성을 중시할 때 유용하다.
5. 열거 타입을 언제 쓰는데?
필요한 원소를 컴파일타임에 다 알 수 있는 상수 집합이라면 열거 타입을 사용하자.
ex) 태양계 행성, 한 주의 요일, 체스 말
ex) 메뉴 아이템, 연산 코드, 명령줄 플래그
정리하자면 ,
열거 타입은 확실히 정수 상수보다 효율이다. 읽기도 쉽고 강력하다. 물론 메서드도 쓸 수 있다. 필요한 원소를 컴파일 타임에 모두 다 알 수 있는 상수의 집합이라면 열거 타입을 강력히 추천한다. 바이너리 수준에서 호환되도록 설계되었기 때문에 열거 타입에 정의된 상수 개수가 영원히 고정 불변일 필요도 없다.
대다수 열거 타입은 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결짓거나 상수마다 다르게 동작할 때는 필요하다.
이 경우, 보통은 추상 메서드를 선언한 뒤,
switch
문 대신 상수별 메서드 구현이 낫다.열거 타입 상수가 같은 코드를 공유한다면, 전략 열거 타입 패턴을 사용하자.
출처
이펙티브 자바 책
Last updated