Enum – 타입 안전한 열거형

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을 사용하면

  • 컴파일 타임에 오류를 잡을 수 있다
  • 코드의 의도가 명확해진다
  • 유지보수가 쉬운 코드를 작성할 수 있다

출처 – 김영한 님의 강의 중 김영한의 실전 자바 – 중급 1편