Use enums instead of int constants
- 2판 제목 : int 상수 대신 enum을 사용하라
- 3판 제목 : int 상수 대신 열거 타입을 사용하라
1. 열거 자료형이란 무엇인가?
열거 자료형(enumerated type) 은 고정 개수의 상수들로 값이 구성되는 자료형을 뜻 한다.
열거형을 뜻하는 키워드 enum
이 자바 1.5에 도입되기 전엔 int
를 이용해 상수를 정의할 수 밖에 없었다.
흔히 int enum pattern
으로 알려진 이 방법은 아래와 같은 코드를 말한다.
1 | // The int enum pattern - severely deficient! |
enum
의 value
를 사용하는 경우라면 별 차이 없지만 타입의 안정성 (type safety) 측면에서 보았을 때 단점이 명백하다.
Orange를 기대하는 메서드에 Apple을 넘기더라도, ==
연산자를 통해 Orange와 Apple을 비교해도 컴파일러는 이를 잡아내지 못하기 때문이다.
또한 변수 앞에 APPLE_
과 ORANGE_
라는 접두어가 붙어있는데, 이는 상수값들에 대한 별도의 namespace를 구별하기 위해서이다.
이러한 int enum pattern을 사용하는 경우 상수의 특성상 컴파일 타임에 결정되기때문에 상수의 값을 변경하는 경우 재컴파일을 해야한다.
다른 방식으로 int
대신 String
을 사용하는 String enum pattern
이 있는데, 이는 컴파일러가 오타를 감지할 수 없으므로 더욱 안 좋은 방식이라고 볼 수 있다.
상술했듯 자바 1.5부턴 이를 개선한 타입인 enum
을 사용할 수 있다.
아래는 간단한 예시이다.
1 | public enum Apple { FUJI, PIPPIN, GRANNY_SMITH } |
자바의 enum
자료형은 완전한 기능을 갖춘 클래스도 C++나 C#의 enum보다 강력하다는 특징이 있다.
enum
자료형의 기본 아이디어는 열거 상수(enumeration constant) 별로 하나의 객체를 public static final
필드 형태로 제공하는 단순한 방식이다.
따라서 enum
타입은 실질적으로 final
로 선언된 것이나 마찬가지로 접근할 수 있는 생성자가 존재하지 않는다.
생성자가 없기에 새로운 객체를 만들수도, 상속을 통해 확장할 수도 없으므로 매우 엄격하게 통제된다고 볼 수 있다.
이런 enum
특유의 엄격함으로 인해 싱글턴을 enum
으로 선언해서 쓰는 방법도 존재하고 있다.
enum
자료형은 컴파일 타임에 이미 타입 안정성을 제공하며 이는 잘못된 값을 전달하는 경우 컴파일러가 오류로 표현한다는 것을 뜻하며 ==
연산자를 통해 다른 자료형의 enum
상수들을 비교할 때도 마찬가지이다.
마지막으로 enum
자료형은 별도의 namespace로 분리되기때문에 값의 추가나 순서의 변경이 있더라도 재컴파일할 필요가 없다.
이처럼 enum
은 int enum pattern
이 가진 단점을 완전히 해결한 자료형임을 알 수 있다.
물론 기존에 존재하는 단점을 해소하는 데 그치지않고 부가적인 기능을 제공한다.
enum
자료형은 임의의 메서드나 필드를 추가할 수도, 임의의 인터페이스를 구현할 수 도 있으며 그 자체로 객체 취급이기에 당연히 Object
클래스에 정의된 모든 메서드를 제공한다.
추가로 Comparable
, Serializable
인터페이스가 구현되어있어 직렬화 이슈도 회피한다.
2. 열거 자료형을 이용한 추상화
enum
에 임의의 메서드나 필드를 왜 추가할 수 있다면 뭘 할 수 있을까?
enum
은 객체이기에 상수 뿐만 아니라 상수에 데이터를 연계하여 효율적으로 코드를 작성할 수 있다.
이 과정을 통해 enum
은 단순한 상수의 모음에서 벗어나 완전한 기능을 갖춘 하나의 추상화 단위(full-featured abstraction) 로 발전시켜나갈 수 있다.
이번엔 태양계의 여덟 행성을 모델링하는 예제를 보자.
각 행성은 질량과 반지름 정보를 가지고 있으며, 이를 통해 행성 표면 중력이나 어떤 물체의 질량이 어떤 무게로 측정될지를 계산할 수 있다.
1 | // Enum type with data and behavior |
이 예제의 Planet
처럼 다양한 기능을 갖춘 enum
을 만들려면 어떻게 해야할까?
enum
상수에 데이터를 넣으려면 해당 객체의 필드를 선언하고, 생성자를 통해 받은 데이터를 저장하면 된다.
물론 enum
은 변경이 불가능하므로 모든 필드는 final
로 선언해야 한다.
이를 이용해 어떤 물체의 지표면상 무게를 입력받아서 모든 8개 행성 표현에서 측정한 무게를 출력하는 프로그램을 작성할 수 있다.
1 | public class WeightTable { |
모든 enum
자료형이 그럿듯 Planet
의 기본 정의된 values()
메서드를 이용해 모든 상수를 순회할 수 있다.
인자로 175를 넘겼을 때, 실행 결과는 아래와 같다.
1 | Weight on MERCURY is 66.133672 |
일반적으로 유용하게 쓰일 enum
이라면 최상위 클래스에 public
으로 선언해야 하며, 특정한 최상위 클래스에서만 쓰인다면 멤버 클래스로 선언하는 것이 좋다.
제일 좋은 예시다. java.math.RoundingMode
이다.
1 | package java.math; |
RoundingMode
는 십진수의 소수점 이하를 어떻게 올림처리할 것인지 표현한다.
RoundingMode
는 소스코스에서 보이는 BigDecimal
클래스가 주로 이용하지만, 다른 곳에서도 이용하도록 권장되므로 최상위 enum
으로 선언된 것이다.
위의 Planet
처럼 모든 상수들이 메서드 내에서 동일하게 동작하는 경우도 있지만, 값에 따라 다른 동작을 해야할 수도 있다.
이때 가장 기본적인 방법은 enum
이 표현하는 상수를 이용해 switch
문을 쓰는 것이다.
예를 들어 기본적인 사칙연산을 표현하기 위한 enum
은 아래와 같이 작성할 수 있다.
1 | // Enum type that switches on its own value - questionable |
3. 상수별 메서드 구현(constant-specific method implementation)
위의 Operation
은 동작엔 이상이 없겠지만, 제어 흐름의 범위상 throw
가 없으면 실행할 수 없다.
이를 좀 더 개선하면 아래와 같이 작성할 수 있다.
1 | // Enum type with constant-specific method implementations |
위 코드의 abstract
키워드가 쓰인 것을 볼 수 있는데, 이를 이용해 각 상수 별로 실제 메서드를 재정의한다.
이러한 방식을 상수별 메서드 구현(constant-specific method implementation) 이라고 부른다.
이 방법은 만약 새로운 상수를 추가하였을 때 메서드의 구현을 누락하더라도 컴파일러가 알려주므로 놓치지않고 작성할 수 있다.
4. 상수별 데이터(constant-specific data)
상수별 메서드 구현은 상수별 데이터(constant-specific data) 과 혼동될 수도 있다.
예제를 통해 비교해보자.
아래 예제는 toString()
을 재정의하여 연산을 나타내는 기호가 반환되도록 구현된 예제이다.
1 | // Enum type with constant-specific class bodies and data |
toString()
을 재정의한 것은 아래와 같이 활용할 수 있다.
1 | public static void main(String[] args) { |
x
와 y
에 각각 2와 4를 넘겼을 때의 출력 결과는 아래와 같다.
1 | 2.000000 + 4.000000 = 6.000000 |
5. valueOf()
enum
자료형은 valeOf()
메서드를 자동으로 생성해준다.
valeOf()
메서드를 이용하면 상수의 이름을 상수 그 자체로 변환할 수 있다.
만약 enum
에서 toString()
을 재정의한 경우 아래와 같이 별도의 메서드를 작성하여 toString()
으로 출력하는 값을 다시 enum
으로 변환해줄지도 검토해보아야 한다.
1 | public enum Operation { |
6. 마무리
본 포스팅에선 enum
에 대해서 알아보았다.
enum
은 고정된 상수의 집합이 필요할 때 사용할 수 있으며, 단순히 상수 그 이상으로 추상화할 수 있다는 장점이 있다.
추상화가 필요없는 값이라 단순히 int enum pattern
을 쓰는 것과 별 차이가 없다하더라도,
enum
을 쓰면서 얻을 수 있는 가독성과 타입 안정성 등을 생각하면 안 쓸 이유가 없을 것이다.
동일한 메서드에서 상수별로 다른 동작이 필요한 경우 상수별 메서드 구현을, 여러 상수가 공통 기능을 이용해야하는 경우 정책 enum 패턴을 사용하도록 하자.