5. 열거형 - ENUM
5.1 문자열과 타입 안전성
비즈니스 요구사항
고객은 3등급으로 나누고, 상품 구매시 등급별로 할인을 적용한다.
- BASIC -> 10% 할인
- GOLD -> 20% 할인
- DIAMOND -> 30% 할인
회원 등급과 가격을 입력하면 할인 금액을 계산해주는 클래스를 만들어보자.
public int discount(String grade, int price) {
int discountPercent = 0;
if (grade.equals("BASIC")) {
discountPercent = 10;
} else if (grade.equals("GOLD")) {
discountPercent = 20;
} else if (grade.equals("DIAMOND")) {
discountPercent = 30;
} else {
System.out.println(grade + ": 할인X");
}
return price * discountPercent / 100;
}
}
위의 discount 메서드는 문자열을 입력받아 각각의 회원 등급에 맞는 할인을 적용한다.
지금과 같이 단순히 문자열을 입력하는 방식은, 오타가 발생하기 쉽고, 유효하지 않는 값이 입력될 수 있다.
- 타입 안정성 부족 : 문자열은 오타가 발생하기 쉽고, 유요하지 않은 값이 입력될 수 있다.
- 데이터 일관성 : "GOLD", "gold", "Gold" 등 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다.
String 사용 시 타입 안정성 부족 문제
- 값의 제한 부족 : String 으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다. (예 오타)
- 컴파일 시 오류 감지 불가 : 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다.
문자열 상수 사용
public class StringGrade {
public static final String BASIC = "BASIC";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}
상수는 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 직접 사용하는 것 보다는 더 안전하다.
이제 discount 메서드에 회원 등급을 보낼때 StringGrade.BASIC 처럼 상수로 보내면 된다.
하지만 문자열 상수를 사용해도, 지금까지 발생한 문제들을 근본적으로 해결할 수는 없다. 왜냐하면 String 타입은 어떤 문자열이든 입력할 수 있기 때문이다. 즉 StringGrade 에 정의된 문자열 상수를 사용하지 않고 직접 문자열을 사용해도 막을 수 있는 방법은 없다.
5.2 타입 안전 열거형 패턴
타입 안전 열거형 패턴 - Type-Safe Enum Pattern
타입 안전 열거형 패턴을 사용하여 회원 등급 BASIC, GOLD, DIAMOND 를 나열하면 이렇게 나열한 항목만 사용할 수 있다.
public class ClassGrade {
public static final ClassGrade BASIC = new ClassGrade();
public static final ClassGrade GOLD = new ClassGrade();
public static final ClassGrade DIAMOND = new ClassGrade();
}
- 타입 안전 열거형 패턴의 메모리 구조

static 이므로 애플리케이션 로딩 시점에 ClassGrade 인스턴스가 3개 생성되고, 각각의 상수는 같은 ClassGrade 타입의 서로 다른 인스턴스의 참조값을 가진다.
public int discount(ClassGrade classGrade, int price) {
int discountPercent = 0;
if (classGrade == ClassGrade.BASIC) {
discountPercent = 10;
}
else if (classGrade == ClassGrade.GOLD) {
discountPercent = 20;
}
else if (classGrade == ClassGrade.DIAMOND) {
discountPercent = 30;
}
else {
System.out.println("할인X");
}
return price * discountPercent / 100;
}
private 생성자
그런데 이 방식은 외부에서 임의로 ClassGrade 의 인스턴스를 생성할 수 있다는 문제가 있다.
ClassGrade newClassGrade = new ClassGrade(); // 생성자 private을 막아야 함
discount(newClassGrade, price);
public class ClassGrade {
public static final ClassGrade BASIC = new ClassGrade();
public static final ClassGrade GOLD = new ClassGrade();
public static final ClassGrade DIAMOND = new ClassGrade();
// private 생성자 추가
private ClassGrade() {}
}
- private 생성자 사용해서 외부에서 ClassGrade 를 임의로 생성하지 못하게 막았다.
- private 생성자 덕분에 ClassGrade 의 인스턴스를 생성하는 것은 ClassGrade 클래스 내부에서만 할 수 있다. 앞서 우리가 정의한 상수들은 ClassGrade 클래스 내부에서 ClassGrade 객체를 생성한다.
- 이제 ClassGrade 인스턴스를 사용할 때는 ClassGrade 내부에 정의한 상수를 사용해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.
- 쉽게 이야기해서 ClassGrade 타입에 값을 전달할 때는 우리가 앞서 열거한 BASIC, GOLD, DIAMOND 만 사용할 수 있다.
타입 안전 열거형 패턴의 장점
- 타입 안정성 향상 : 정해진 객체만 사용할 수 있기 때문에, 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있다.
- 데이터 일관성 : 정해진 객체만 사용하므로 데이터의 일관성이 보장된다.
- 제한된 인스턴스 생성 : 클래스 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용할 수 있도록 한다. 이를 통해 미리 정의된 값들만 사용하도록 보장한다.
- 타입 안정성 : 이 패턴을 사용하면, 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지할 수 있다.
단점
이 패턴을 구현하려면 많은 코드를 작성해야 한다. 그리고 private 생성자를 추가하는 등 유의해야 하는 부분들도 있다.
5.3 열거형 - Enum Type
자바는 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을 제공한다.
public enum Grade {
BASIC, GOLD, DIAMOND
}
- 열거형을 정의할 때는 class 대신에 enum 을 사용한다.
- 원하는 상수의 이름을 나열하면 된다.
- 열거형도 클래스이다.
- 열거형은 자동으로 java.lang.Enum을 상속 받는다.
- 외부에서 임의로 생성할 수 없다.
public int discount(Grade grade, int price) {
int discountPercent = 0;
if (grade == Grade.BASIC) {
discountPercent = 10;
}
else if (grade == Grade.GOLD) {
discountPercent = 20;
}
else if (grade == Grade.DIAMOND) {
discountPercent = 30;
}
else {
System.out.println("할인X");
}
return price * discountPercent / 100;
}
열거형(ENUM)의 장점
- 타입 안정성 향상 : 열거형은 사전에 정의된 상수들로만 구성되므로, 유요하지 않은 값이 입력될 가능성이 없다. 이런 경우 컴파일 오류가 발생한다.
- 간결성 및 일관성 : 열거형을 사용하면 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.
- 확장성 : 새로운 회원 등급을 타입을 추가하고 싶을 때, ENUM에 새로운 상수를 추가하지만 하면 된다.
5.4 열거형 - 주요 메서드
모든 열거형은 java.lang.Enum 클래스를 자동으로 상속 받는다. 따라서 해당 클래스가 제공하는 기능들을 사용할 수 있다.
//모든 ENUM 반환
Grade[] values = Grade.values();
System.out.println("values = " + Arrays.toString(values));
for (Grade value : values) {
System.out.println("name=" + value.name() + ", ordinal=" + value.ordinal());
}
//String -> ENUM 변환, 잘못된 문자면 IllegalArgumentException 발생
String input = "GOLD";
Grade gold = Grade.valueOf(input);
System.out.println("gold = " + gold); //toString() 오버라이딩 가능
실행 결과
values = [BASIC, GOLD, DIAMOND]
name=BASIC, ordinal=0
name=GOLD, ordinal=1
name=DIAMOND, ordinal=2
gold = GOLD
ENUM - 주요 메서드
- values() : 모든 ENUM 상수를 포함하는 배열을 반환한다.
- valueOf(String name) : 주어진 이름과 일치하는 ENUM 상수를 반환한다.
- name() : ENUM 상수의 이름을 문자열로 반환한다.
- toString() : ENUM 상수의 이름을 문자열로 반환한다. name() 메서드와 유사하지만, toString() 은 직접 오버라이드 할 수 있다.
5.5 열거형 - 리팩토링
discount 메서드에서 할인율(discountPercent)은 각각의 회원 등급별로 판단된다. 할인율은 결국 회원 등급을 따라간다. 따라서 회원 등급 클래스가 할인율을 가지고 관리하도록 변경하자.
public enum Grade {
BASIC(10), GOLD(20), DIAMOND(30);
private final int discountPercent;
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
}
- discountPercent 필드를 추가하고, 생성자를 통해서 필드에 값을 저장한다.
- 열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하다. 따라서 생성자에 접근제어자를 선언할 수 없게 막혀있다. private 이라고 생각하면 된다.
- BASIC(10) 과 같이 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출된다.
- 값을 조회하기 위해 getDiscountPercent() 메서드를 추가했다. 열거형도 클래스이므로 메서드를 추가할 수 있다.
public int discount(Grade grade, int price) {
return price * grade.getDiscountPercent() / 100;
}
이제 단순히 할인율 계산만 남았다.
객체 지향 관점에서 이렇게 Grade 의 데이터인 discountPercent를 외부에 노출하는 것보다는, Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.
public enum Grade {
BASIC(10), GOLD(20), DIAMOND(30);
private final int discountPercent;
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
//추가
public int discount(int price) {
return price * discountPercent / 100;
}
}