타입 안전 열거 패턴과 enum
private 생성자로 선택 집합을 닫는 패턴을 구현하고 언어 enum의 생성 제한·동등성·values·valueOf 계약을 검증합니다.
문자열 대신 등급 전용 타입을 매개변수로 받으면 임의 문자열이 서비스 안으로 들어오지 못합니다.
클래스로도 미리 만든 인스턴스만 공개하는 타입 안전 열거 패턴을 구현할 수 있고, 자바 enum은 이 구조를 언어 차원에서 간결하고 안전하게 제공합니다.
enum 인스턴스 생성 제한
enum 생성자는 외부에서 호출할 수 없습니다.
허용 집합에 없는 네 번째 Grade를 임의로 만드는 다음 코드는 컴파일 단계에서 차단됩니다.
public final class EnumInstantiationFailure {
public static void main(String[] args) {
Grade unknown = new Grade();
System.out.println(unknown);
}
private enum Grade {
BASIC, GOLD, DIAMOND
}
}error: enum types may not be instantiated서비스 매개변수가 Grade라면 BASIC, GOLD, DIAMOND 또는 null만 들어올 수 있습니다.
오타 문자열과 대소문자 변형은 Grade 인스턴스가 될 수 없고, switch도 허용 상수를 기준으로 완전성을 검사할 수 있습니다.
타입 안전 열거 패턴
생성자를 private로 막고 public static final 인스턴스만 공개하면 외부가 새 값을 만들 수 없습니다.
각 인스턴스는 자기 이름과 할인율을 보관합니다.
public final class TypeSafeGradePattern {
public static void main(String[] args) {
System.out.println(discount(ClassGrade.BASIC, 10_000));
System.out.println(discount(ClassGrade.GOLD, 10_000));
System.out.println(discount(ClassGrade.DIAMOND, 10_000));
}
private static int discount(ClassGrade grade, int price) {
return price * grade.percent() / 100;
}
private static final class ClassGrade {
static final ClassGrade BASIC = new ClassGrade("BASIC", 10);
static final ClassGrade GOLD = new ClassGrade("GOLD", 20);
static final ClassGrade DIAMOND = new ClassGrade("DIAMOND", 30);
private final String name;
private final int percent;
private ClassGrade(String name, int percent) {
this.name = name;
this.percent = percent;
}
int percent() { return percent; }
@Override public String toString() { return name; }
}
}1000
2000
3000private 생성자가 빠지면 호출자가 같은 필드를 가진 임의 등급을 만들 수 있어 닫힌 집합이 깨집니다.
직접 패턴은 values, valueOf, 직렬화 동일성 같은 부가 기능을 스스로 구현해야 합니다.
언어 enum은 이 반복을 제거합니다.
필드와 메서드를 가진 enum
enum은 단순 정수 상수가 아니라 지정된 타입의 유일한 인스턴스들입니다.
생성자 인수로 데이터가 들어가고 일반 메서드가 그 데이터를 사용합니다.
public final class EnumGradeDiscount {
public static void main(String[] args) {
int price = 10_000;
for (Grade grade : Grade.values()) {
System.out.println(grade.name() + "=" + grade.discount(price));
}
}
private enum Grade {
BASIC(10), GOLD(20), DIAMOND(30);
private final int percent;
Grade(int percent) {
this.percent = percent;
}
int discount(int price) {
if (price < 0) throw new IllegalArgumentException("price");
return price * percent / 100;
}
}
}BASIC=1000
GOLD=2000
DIAMOND=3000할인 계산이 Grade 안에 있으므로 서비스의 종류별 if가 사라집니다.
새 등급의 데이터와 행동이 한 선언에 모입니다.
모든 등급이 같은 공식이라면 필드 기반 메서드가 적합하고 상수마다 완전히 다른 행동이 필요하면 상수별 메서드 구현도 가능하지만 복잡도가 커질 수 있습니다.
enum 기본 메서드의 용도
values()는 선언 순서의 모든 상수를 새 배열로 반환합니다.
valueOf("GOLD")는 정확한 이름을 찾아 상수를 반환하고 일치하지 않으면 IllegalArgumentException을 던집니다.
name()은 선언 이름이고 toString()은 재정의될 수 있습니다.
import java.util.Arrays;
public final class EnumCoreMethods {
public static void main(String[] args) {
Grade[] grades = Grade.values();
System.out.println("values=" + Arrays.toString(grades));
System.out.println("parsed=" + Grade.valueOf("GOLD"));
for (Grade grade : grades) {
System.out.println(grade.name() + ":" + grade.ordinal());
}
}
private enum Grade {
BASIC, GOLD, DIAMOND
}
}values=[BASIC, GOLD, DIAMOND]
parsed=GOLD
BASIC:0
GOLD:1
DIAMOND:2ordinal은 선언 순서가 바뀌면 달라집니다.
데이터베이스 저장, 네트워크 코드, 업무 우선순위에 사용하지 않고 별도 안정 코드나 level 필드를 둡니다.
화면 표시명도 name에 의존하지 않고 description 필드나 번역 계층에서 제공합니다.
enum 비교와 switch 완전성
enum 상수는 JVM 안에서 각 이름당 하나의 인스턴스이므로 == 비교가 안전하고 null도 equals 호출 없이 비교할 수 있습니다.
switch 식에서 모든 상수를 나열하고 default를 생략하면 새 상수 추가 시 컴파일러가 빠진 분기를 알려 줄 수 있습니다.
public final class EnumBoardPriority {
public static void main(String[] args) {
System.out.println(label(Priority.LOW));
System.out.println(label(Priority.NORMAL));
System.out.println(label(Priority.HIGH));
}
private static String label(Priority priority) {
return switch (priority) {
case LOW -> "later";
case NORMAL -> "today";
case HIGH -> "now";
};
}
private enum Priority { LOW, NORMAL, HIGH }
}later
today
nownull Priority는 switch 전에 실패합니다.
null이 업무 상태인지 입력 오류인지 정하고 별도 상수 UNKNOWN을 추가할지 경계에서 거부할지 선택합니다.
UNKNOWN은 실제 미정 상태일 때만 사용하고 오류를 숨기는 기본값으로 남용하지 않습니다.
enum 적용 기준
선택지가 배포 코드와 함께 바뀌고, 모든 값이 같은 타입 계약을 공유하며, 이름 외 데이터나 행동을 가질 때 enum이 적합합니다.
관리자가 운영 중 임의 값을 추가해야 하거나 각 항목 수명이 데이터베이스에서 관리되면 enum으로 고정하지 않습니다.
연습 문제
DRAFT, REVIEW, PUBLISHED 상태와 사용자 설명을 가진 PostStatus를 작성하세요.
values를 순회해 이름=설명을 출력하고 valueOf로 REVIEW를 조회합니다.
해설 보기
public final class PostStatusExercise {
public static void main(String[] args) {
for (PostStatus status : PostStatus.values()) {
System.out.println(status.name() + "=" + status.description());
}
System.out.println("parsed=" + PostStatus.valueOf("REVIEW"));
}
private enum PostStatus {
DRAFT("초안"), REVIEW("검토"), PUBLISHED("공개");
private final String description;
PostStatus(String description) {
this.description = description;
}
String description() { return description; }
}
}DRAFT=초안
REVIEW=검토
PUBLISHED=공개
parsed=REVIEWdescription은 화면 문구를 선언 이름과 분리합니다.
다국어가 필요하면 enum에 번역문을 직접 넣기보다 메시지 키를 보관합니다.