Java의 열거형(Enum Type)을 제대로 이해하려면 열거형이 생겨난 배경을 알아야 한다. 실무 예제를 통해 문제 상황부터 살펴보자
비즈니스 요구사항
다음과 같은 회원 등급 시스템을 구현한다고 가정해보자
- BASIC 등급: 10% 할인
- GOLD 등급: 20% 할인
- DIAMOND 등급: 30% 할인
예를 들어, GOLD 등급 회원이 10,000원을 구매하면 2,000원 할인 받는다
문자열 사용의 문제점
첫 번째 구현 – String
가장 직관적인 방법은 문자열로 등급을 표현하는 것이다
public class DiscountService {
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;
}
}
사용 예시
public class StringGradeEx0_1 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
int basic = discountService.discount("BASIC", price);
int gold = discountService.discount("GOLD", price);
int diamond = discountService.discount("DIAMOND", price);
System.out.println("BASIC 등급의 할인 금액: " + basic); // 1000
System.out.println("GOLD 등급의 할인 금액: " + gold); // 2000
System.out.println("DIAMOND 등급의 할인 금액: " + diamond); // 3000
}
}
문제 발생
문자열을 직접 입력하는 방식은 다음과 같은 심각한 문제를 야기한다
public class StringGradeEx0_2 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
// 문제 1: 존재하지 않는 등급
int vip = discountService.discount("VIP", price);
System.out.println("VIP 등급의 할인 금액: " + vip); // 0 (할인X)
// 문제 2: 오타
int diamondd = discountService.discount("DIAMONDD", price);
System.out.println("DIAMONDD 등급의 할인 금액: " + diamondd); // 0 (할인X)
// 문제 3: 대소문자 불일치
int gold = discountService.discount("gold", price);
System.out.println("gold 등급의 할인 금액: " + gold); // 0 (할인X)
}
}
String 사용의 근본적 한계
타입 안전성 부족
- 오타가 발생하기 쉽다
- 유효하지 않은 값 입력이 가능하다
- 컴파일 시점에 오류를 감지할 수 없다 (가장 치명적)
데이터 일관성 부족
- “GOLD”, “gold”, “Gold” 등 다양한 형식으로 입력 가능
- 데이터 일관성 보장 불가
핵심 문제: String 타입은 어떤 문자열이든 받을 수 있기 때문에, 특정 값으로 제한하는 것이 불가능하다
문자열 상수의 한계
개선 시도 – 문자열 상수 사용
문자열을 직접 사용하는 대신 상수로 정의해보자
public class StringGrade {
public static final String BASIC = "BASIC";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}
사용 코드
public class DiscountService {
public int discount(String grade, int price) {
int discountPercent = 0;
if (grade.equals(StringGrade.BASIC)) {
discountPercent = 10;
} else if (grade.equals(StringGrade.GOLD)) {
discountPercent = 20;
} else if (grade.equals(StringGrade.DIAMOND)) {
discountPercent = 30;
} else {
System.out.println(grade + ": 할인X");
}
return price * discountPercent / 100;
}
}
int basic = discountService.discount(StringGrade.BASIC, price); int gold = discountService.discount(StringGrade.GOLD, price); int diamond = discountService.discount(StringGrade.DIAMOND, price);
여전히 남은 문제
문자열 상수를 사용하면 오타를 줄일 수 있지만, 근본적인 문제는 해결되지 않는다
// 여전히 가능한 잘못된 사용
int vip = discountService.discount("VIP", price); // 컴파일 오류 없음
int gold = discountService.discount("gold", price); // 컴파일 오류 없음
메서드 시그니처의 문제
public int discount(String grade, int price) {}
이 시그니처만 보고는 StringGrade의 상수를 사용해야 한다는 것을 알 수 없다. 주석이나 문서에 의존해야 하며, 이는 휴먼 에러를 방지할 수 없다
타입 안전 열거형 패턴
타입 안전 열거형 패턴 (Type-Safe Enum Pattern)이란?
많은 개발자들이 오랜 기간 고민한 끝에 나온 해결책이 타입 안전 열거형 패턴이다. 핵심 아이디어는 아래와 같다
- 타입 자체를 제한하여 특정 값만 사용 가능하게 한다
- 나열한 항목만 사용할 수 있도록 강제한다
구현
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() {}
}
메모리 구조
┌─────────────────────────────────┐ │ ClassGrade 클래스(Method 영역) │ │ │ │ BASIC ──────────────────────┼─→ ClassGrade 인스턴스 (x001) │ GOLD ──────────────────────┼─→ ClassGrade 인스턴스 (x002) │ DIAMOND ──────────────────────┼─→ ClassGrade 인스턴스 (x003) │ │ └─────────────────────────────────┘
주요 특징
- static final로 상수 선언 → 참조값 변경 불가
- 각 상수는 별도의 인스턴스 참조
- private 생성자 → 외부 생성 차단
사용 예시
public class DiscountService {
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;
}
}
int basic = discountService.discount(ClassGrade.BASIC, price); int gold = discountService.discount(ClassGrade.GOLD, price); int diamond = discountService.discount(ClassGrade.DIAMOND, price);
안전한 이유
// 컴파일 오류 발생 ClassGrade newGrade = new ClassGrade(); // private 생성자로 차단됨
타입 시그니처가 강제
public int discount(ClassGrade classGrade, int price)
- ClassGrade 타입만 받을 수 있다
- IDE 자동완성으로 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();
private ClassGrade() {}
}
이런 불편함을 해소하기 위해 Java는 enum 키워드를 제공한다
Java Enum 타입
Enum 선언
Java의 enum을 사용하면 타입 안전 열거형 패턴을 매우 간결하게 구현할 수 있다
public enum Grade {
BASIC, GOLD, DIAMOND
}
이 한 줄의 코드가 앞서 작성한 복잡한 ClassGrade 코드와 동일한 기능을 제공한다
Enum의 내부 동작
Java enum은 다음과 같이 동작한다
// 실제로는 이렇게 변환됨 (개념적)
public class Grade extends Enum {
public static final Grade BASIC = new Grade();
public static final Grade GOLD = new Grade();
public static final Grade DIAMOND = new Grade();
private Grade() {} // 자동으로 private
}
핵심 특징
- java.lang.Enum 자동 상속
- private 생성자 자동 생성
- 외부 생성 불가
메모리 구조 확인
public class EnumRefMain {
public static void main(String[] args) {
System.out.println("class BASIC = " + Grade.BASIC.getClass());
System.out.println("class GOLD = " + Grade.GOLD.getClass());
System.out.println("class DIAMOND = " + Grade.DIAMOND.getClass());
System.out.println("ref BASIC = " + Grade.BASIC);
System.out.println("ref GOLD = " + Grade.GOLD);
System.out.println("ref DIAMOND = " + Grade.DIAMOND);
}
}
실행 결과
class BASIC = class enumeration.ex3.Grade
class GOLD = class enumeration.ex3.Grade
class DIAMOND = class enumeration.ex3.Grade
ref BASIC = enumeration.ex3.Grade@x001
ref GOLD = enumeration.ex3.Grade@x002
ref DIAMOND = enumeration.ex3.Grade@x003
- 모두 같은 Grade 타입
- 각각 다른 인스턴스 (다른 참조값)
Enum 사용 예시
public class DiscountService {
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;
}
}
int basic = discountService.discount(Grade.BASIC, price); // 1000 int gold = discountService.discount(Grade.GOLD, price); // 2000 int diamond = discountService.discount(Grade.DIAMOND, price); // 3000
외부 생성 불가
// 컴파일 오류 Grade myGrade = new Grade(); // enum classes may not be instantiated
Enum의 장점
- 타입 안정성: 정의된 상수만 사용 가능, 컴파일 시점 검증
- 간결성: 복잡한 패턴을 간단한 문법으로 구현
- 일관성: 데이터 일관성 자동 보장
- 확장성: 새로운 상수 추가가 용이
- switch문 지원: enum은 switch문에서 사용 가능
Enum의 주요 메서드
모든 열거형은 java.lang.Enum 클래스를 자동으로 상속받아 다양하고 유용한 메서드를 제공한다
주요 메서드
public class EnumMethodMain {
public static void main(String[] args) {
// 1. values(): 모든 ENUM 상수를 배열로 반환
Grade[] values = Grade.values();
System.out.println("values = " + Arrays.toString(values));
// 출력: values = [BASIC, GOLD, DIAMOND]
// 2. name(), ordinal(): 상수의 이름과 순서
for (Grade value : values) {
System.out.println("name=" + value.name() + ", ordinal=" + value.ordinal());
}
// 출력:
// name=BASIC, ordinal=0
// name=GOLD, ordinal=1
// name=DIAMOND, ordinal=2
// 3. valueOf(): 문자열을 ENUM으로 변환
String input = "GOLD";
Grade gold = Grade.valueOf(input);
System.out.println("gold = " + gold); // 출력: gold = GOLD
// 잘못된 문자열 입력 시 IllegalArgumentException 발생
}
}
메서드 상세
| 메서드 | 설명 | 예시 |
| values() | 모든 enum 상수를 배열로 반환 | Grade.values() → [BASIC, GOLD, DIAMOND] |
| valueOf(String) | 문자열에 해당하는 enum 상수 반환 | Grade.valueOf(“GOLD”) → Grade.GOLD |
| name() | enum 상수의 이름을 문자열로 반환 | Grade.BASIC.name() → “BASIC” |
| ordinal() | enum 상수의 선언 순서 반환(0부터 시작) | Grade.GOLD.ordinal() → 1 |
| toString() | enum 상수의 이름 반환(오버라이드 가능) | Grade.BASIC.toString → “BASIC” |
ordinal() 사용 주의 사항
ordinal()은 가급적 사용하지 않는 것이 권장된다
// 초기 상태 BASIC: 0 GOLD: 1 DIAMOND: 2 // SILVER 등급이 중간에 추가되면? BASIC: 0 SILVER: 1 // 새로 추가 GOLD: 2 // 순서 변경 DIAMOND: 3 // 순서 변경
위험한 시나리오
- 데이터베이스 ordinal 값 저장: GOLD = 1
- 코드에 SILVER 추가
- GOLD의 ordinal이 2로 변경
- 기존 데이터베이스의 1은 이제 SILVER를 의미
- GOLD 회원이 갑자기 SILVER로 강등
Enum 추가 특징
- java.lang.Enum 자동 상속 (다른 클래스 상속 불가)
- 인터페이스 구현 가능
- 추상 메서드 선언 및 구현 가능 (익명 클래스 방식)
실전 리팩토링 – Enum 활용
실무에서 Enum을 효과적으로 활용하는 방법을 단계별로 알아보자
리팩토링 1단계 – 필드 추가
문제점 – if-else 중복 코드
if (grade == Grade.BASIC) {
discountPercent = 10;
} else if (grade == Grade.GOLD) {
discountPercent = 20;
} else if (grade == Grade.DIAMOND) {
discountPercent = 30;
}
해결책 – Enum에 할인율을 필드로 추가
public enum Grade {
BASIC(10),
GOLD(20),
DIAMOND(30);
private final int discountPercent;
// 생성자 (접근 제어자 생략 가능, 자동으로 private)
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
}
개선된 DiscountService
public class DiscountService {
public int discount(Grade grade, int price) {
// if-else 완전 제거
return price * grade.getDiscountPercent() / 100;
}
}
리팩토링 2단계 – 메서드 추가 (캡슐화)
더 나은 설계 – 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;
}
// 할인 계산 로직을 Enum 내부로 이동
public int discount(int price) {
return price * discountPercent / 100;
}
}
완전히 간소화된 DiscountService
public class DiscountService {
public int discount(Grade grade, int price) {
return grade.discount(price); // 단순 위임
}
}
리팩토링 3단계 – DiscountService 제거
Grade가 모든 로직을 처리하므로 DiscountService가 불필요하다
public class EnumRefMain {
public static void main(String[] args) {
int price = 10000;
// 직접 호출
System.out.println("BASIC 등급의 할인 금액: " + Grade.BASIC.discount(price));
System.out.println("GOLD 등급의 할인 금액: " + Grade.GOLD.discount(price));
System.out.println("DIAMOND 등급의 할인 금액: " + Grade.DIAMOND.discount(price));
}
}
리팩토링 4단계 – 중복 제거
public class EnumRefMain {
public static void main(String[] args) {
int price = 10000;
printDiscount(Grade.BASIC, price);
printDiscount(Grade.GOLD, price);
printDiscount(Grade.DIAMOND, price);
}
private static void printDiscount(Grade grade, int price) {
System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
}
}
리팩토링 5단계 – 모든 등급 자동 처리
public class EnumRefMain {
public static void main(String[] args) {
int price = 10000;
// values()로 모든 enum 상수 조회
for (Grade grade : Grade.values()) {
printDiscount(grade, price);
}
}
private static void printDiscount(Grade grade, int price) {
System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
}
}
실행 결과
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
장점
- 새로운 등급 추가 시 Grade enum만 수정
- main() 코드는 변경 불필요
- 확장에 열려 있고 수정에 닫혀 있는 설계 (OCP 원칙)
Java Enum은 단순한 상수 집합을 넘어 타입 안전성과 객체지향 설계를 동시에 제공하는 강력한 기능이다. String이나 int 상수 대신 Enum을 사용하면
- 컴파일 타임에 오류를 잡을 수 있다
- 코드의 의도가 명확해진다
- 유지보수가 쉬운 코드를 작성할 수 있다