본문 바로가기
Course/Java

[java-mid1] 5. 열거형 - ENUM

by Lpromotion 2025. 3. 15.
"김영한의 실전 자바 - 중급편" 내용을 참고하여 정리함.

1. 문자열과 타입 안전성

자바에서 열거형(Enum Type)이 등장한 이유는 문자열을 이용한 값 표현의 한계 때문이다.
기본적으로 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;
    }
}

 

문제점

  • 오타 발생 가능: "GOLD" 대신 "gold"로 입력하면 정상적으로 동작하지 않는다.
  • 잘못된 값 입력 가능: "VIP" 같은 존재하지 않는 값도 전달할 수 있다.
  • 컴파일 시 오류 감지 불가: 문자열 값이 유효한지 여부는 런타임에서만 확인 가능하다.
  • 데이터 일관성 부족: "GOLD", "gold", "Gold" 등 서로 다른 표기법이 가능하다.

이러한 문제를 해결하기 위해 문자열 대신 상수(constant)를 사용하는 방법이 등장했다.

 

문자열 상수를 사용한 방식 & 문제점

 
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;
    }
}

 

장점

  • 코드 가독성 증가: 직접 "GOLD" 같은 문자열을 입력하지 않고, StringGrade.GOLD 와 같이 상수를 사용할 수 있다.
  • 컴파일 시 오류 감지 가능: 잘못된 상수명을 입력하면 컴파일 오류가 발생한다.

 

여전히 해결되지 않는 문제

  • String 타입을 사용하기 때문에 여전히 어떤 문자열이든 입력할 수 있다.
  • 개발자가 StringGrade 의 상수를 사용해야 한다는 것을 강제할 수 없다.
  • 문자열 비교(equals())를 해야 하는 불편함이 있다.

이 문제를 해결하기 위해 타입 안전 열거형 패턴이 등장했다.

 

 

2. 타입 안전 열거형 패턴 (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 ClassGrade() {} // 외부에서 인스턴스 생성 방지
}
  • 먼저 회원 등급을 다루는 클래스를 만들고, 각각의 회원 등급별로 상수를 선언한다.
  • 이때 각각의 상수마다 별도의 인스턴스를 생성하고, 생성한 인스턴스를 대입한다.
  • 각각을 상수로 선언하기 위해 static , final 을 사용한다.
    • static 을 사용해서 상수를 메서드 영역에 선언한다.
    • final 을 사용해서 인스턴스(참조값)를 변경할 수 없게 한다.

 

public class ClassRefMain {
    public static void main(String[] args) {
        System.out.println("class BASIC = " + ClassGrade.BASIC.getClass());
        System.out.println("class GOLD = " + ClassGrade.GOLD.getClass());
        System.out.println("class DIAMOND = " + ClassGrade.DIAMOND.getClass());

        System.out.println("ref BASIC = " + ClassGrade.BASIC);
        System.out.println("ref GOLD = " + ClassGrade.GOLD);
        System.out.println("ref DIAMOND = " + ClassGrade.DIAMOND);
    }
}
class BASIC = class enumeration.ex2.ClassGrade
class GOLD = class enumeration.ex2.ClassGrade
class DIAMOND = class enumeration.ex2.ClassGrade
ref BASIC = enumeration.ex2.ClassGrade@x001
ref GOLD = enumeration.ex2.ClassGrade@x002
ref DIAMOND = enumeration.ex2.ClassGrade@x003
  • 각각의 상수는 모두 ClassGrade 타입을 기반으로 인스턴스를 만들었기 때문에 getClass() 의 결과는 모두 ClassGrade 이다.
  • 각각의 상수는 모두 서로 각각 다른 ClassGrade 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.

 

 

개선된 DiscountService

public class DiscountService {
    public int discount(ClassGrade grade, int price) {
        int discountPercent = 0;
        if (grade == ClassGrade.BASIC) discountPercent = 10;
        else if (grade == ClassGrade.GOLD) discountPercent = 20;
        else if (grade == ClassGrade.DIAMOND) discountPercent = 30;
        else System.out.println("할인X");

        return price * discountPercent / 100;
    }
}

 

실행 코드

public class ClassGradeEx2_1 {
    public static void main(String[] args) {
        int price = 10000;
        DiscountService discountService = new DiscountService();
        int basic = discountService.discount(ClassGrade.BASIC, price);
        int gold = discountService.discount(ClassGrade.GOLD, price);
        int diamond = discountService.discount(ClassGrade.DIAMOND, price);

        System.out.println("BASIC 등급의 할인 금액: " + basic);
        System.out.println("GOLD 등급의 할인 금액: " + gold);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
    }
}
  • discount() 를 호출할 때 미리 정의한 ClassGrade 의 상수를 전달한다.
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000

 

 

장점

  • 타입 안정성 보장: ClassGrade 타입만 전달할 수 있어 잘못된 값 입력을 컴파일 시점에 방지할 수 있다.
  • 제한된 인스턴스 생성: 사전에 정의된 몇 개의 인스턴스만 생성하여, 미리 정의된 값들만 사용하도록 보장한다.
  • 컴파일 타임 오류 감지 가능: 잘못된 타입이 전달되면 컴파일 오류 발생.
  • 일관성 유지: BASIC, GOLD, DIAMOND 외의 값은 사용할 수 없음.

 

단점

  • 구현이 번거로움: private 생성자와 인스턴스 상수를 직접 정의해야 한다.
  • 클래스 코드가 많아짐: 단순한 값 표현을 위해 많은 코드가 필요하다.

이 단점을 해결하기 위해 자바에서는 열거형(Enum Type)을 도입했다.

 

 

3. 열거형 (Enum Type)

자바는 타입 안전 열거형 패턴(Type-Safe Enum Pattern)을 간결하게 사용할 수 있도록 열거형(Enum Type)을 제공한다.

이는 특정 값들의 집합을 정의할 때, 기존의 final static 상수를 사용하는 방식보다 안전하고 가독성이 뛰어난 방식을 제공한다.

 

public enum Grade {
    BASIC, GOLD, DIAMOND
}
  • class 대신 enum 키워드를 사용하여 정의한다.
  • Grade.BASIC, Grade.GOLD, Grade.DIAMOND 처럼 사용 가능하다.

 

열거형 특징 및 내부 구조 분석

  • 자동으로 java.lang.Enum을 상속받는다.
  • 내부적으로 Enum<Grade>를 상속하며, 추가적인 클래스를 상속할 수 없다.
  • 각각의 상수는 독립적인 객체로 싱글톤 패턴이 적용된다.
  • 외부에서 new 를 사용하여 인스턴스를 생성할 수 없다.
    • 생성할 경우 컴파일 오류가 발생: enum classes may not be instantiated

 

자바의 열거형은 Enum을 자동 상속하는 특수한 형태의 클래스이다.
따라서, 다음 코드와 거의 같다.

public class Grade extends Enum {
    public static final ClassGrade BASIC = new ClassGrade();
    public static final ClassGrade GOLD = new ClassGrade();
    public static final ClassGrade DIAMOND = new ClassGrade();

    // private 생성자 추가
    private Grade() {}
}

 

실행 코드 - 열거형 값 출력

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 = " + refValue(Grade.BASIC));
        System.out.println("ref GOLD = " + refValue(Grade.GOLD));
        System.out.println("ref DIAMOND = " + refValue(Grade.DIAMOND));
    }

    private static String refValue(Object grade) {
        // System.identityHashCode(grade) : 자바가 관리하는 객체의 참조값을 숫자로 반환한다.
        // Integer.toHexString() : 숫자를 16진수로 변환, 우리가 일반적으로 확인하는 참조값은 16진수
        return Integer.toHexString(System.identityHashCode(grade));
    }
}
class BASIC = class enumeration.ex3.Grade
class GOLD = class enumeration.ex3.Grade
class DIAMOND = class enumeration.ex3.Grade
ref BASIC = 1d81eb93
ref GOLD = 7291c18f
ref DIAMOND = 34a245ab
  • 모든 열거형 상수는 Grade 타입을 사용하고, 각각의 인스턴스가 서로 다른 것을 확인할 수 있다.
  • 열거형은 toString()을 재정의하기 때문에 참조값을 직접 확인할 수 없다.
    따라서 refValue()를 사용했다.
  • 열거형도 클래스이다. 열거형을 제공하기 위해 제약이 추가된 클래스라 생각하면 된다.

 

열거형 활용 - 할인율 적용

public class DiscountService {

    public int discount(Grade classGrade, int price) {
        int discountPercent = 0;

        // switch문 사용 가능
        if (classGrade == BASIC) {
            discountPercent = 10;
        } else if (classGrade == GOLD) {
            discountPercent = 20;
        } else if (classGrade == DIAMOND) {
            discountPercent = 30;
        } else {
            System.out.println("할인X");
        }

        return price * discountPercent / 100; // 가격 * 할인율 / 100
    }
}

 

실행 코드

public class ClassGradeEx3_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);
        System.out.println("GOLD 등급의 할인 금액: " + gold);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
    }
}
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
  • 열거형의 사용법이 앞서 타입 안전 열거형 패턴을 직접 구현한 코드와 같은 것을 확인 할 수 있다.

 

열거형의 장점

  • 타입 안정성 향상: 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성이 없다. 이런 경우 컴파일 오류가 발생한다.
  • 간결성 및 일관성: 열거형을 사용하면 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.
  • 확장성: 새로운 회원 등급을 타입을 추가하고 싶을 때, ENUM에 새로운 상수를 추가하기만 하면 된다.

 

참고: 열거형을 사용하는 경우 static import 를 적절하게 사용하면 더 읽기 좋은 코드를 만들 수 있다.

 

 

4. 열거형 - 주요 메서드

자바의 모든 열거형(Enum)은 java.lang.Enum을 자동으로 상속받는다. 따라서 Enum 클래스에서 제공하는 주요 메서드를 활용할 수 있다.

 

주요 메서드

1. values()

  • 열거형의 모든 상수를 배열로 반환한다.
Grade[] values = Grade.values();
System.out.println(Arrays.toString(values));

 

2. valueOf(String name)

  • 문자열을 해당하는 ENUM 상수로 변환한다.
  • 존재하지 않는 이름이면 IllegalArgumentException 예외가 발생한다.
Grade gold = Grade.valueOf("GOLD");

 

 
3. name()
  • ENUM 상수의 이름을 문자열로 반환한다.
System.out.println(Grade.GOLD.name()); // "GOLD"

 

 
4. ordinal()
  • ENUM 상수의 선언 순서를 0부터 반환한다.
  • 하지만 중간에 새로운 상수가 추가될 경우 기존 값들의 순서가 변경될 수 있으므로 ordinal() 사용은 지양하는 것이 좋다.

 

5. toString()

  • ENUM 상수의 이름을 문자열로 반환한다.
  • name() 과 유사하지만 toString() 은 직접 오버라이딩이 가능하다.

 

주의사항

  • ordinal() 은 열거형 상수의 순서를 기반으로 값이 변경될 가능성이 있기 때문에 데이터 저장 목적으로 사용하면 안 된다.
  • valueOf() 를 사용할 때는 반드시 존재하는 ENUM 상수인지 확인 후 호출해야 한다.

 

열거형 정리

  • 열거형은 java.lang.Enum 를 자동(강제)으로 상속 받는다.
  • 열거형은 이미 java.lang.Enum 을 상속 받았기 때문에 추가로 다른 클래스를 상속을 받을 수 없다.
  • 열거형은 인터페이스를 구현할 수 있다.
  • 열거형에 추상 메서드를 선언하고, 구현할 수 있다.
    • 이 경우 익명 클래스와 같은 방식을 사용한다.

 

 

5. 열거형 - 리팩토링1

기존 코드에서는 if 문을 사용하여 할인율을 계산하고 있었다. 이를 개선하기 위해 할인율 정보를 ENUM 내부에서 직접 관리하도록 리팩토링한다.

 

기존 코드 - DiscountService.discount()

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");
}
  • if 문을 사용하면 코드가 길어지고 유지보수가 어려워진다.
  • 위 코드에서 할인율(discountPercent)은 각각의 회원 등급별로 판단된다. 
    따라서 회원 등급 클래스가 할인율 (discountPercent)을 가지고 관리하도록 변경할 수 있다.

 

리팩토링: 할인율을 ENUM 내부에서 관리

public class ClassGrade {
    public static final ClassGrade BASIC = new ClassGrade(10);
    public static final ClassGrade GOLD = new ClassGrade(20);
    public static final ClassGrade DIAMOND = new ClassGrade(30);

    private final int discountPercent;

    private ClassGrade(int discountPercent) {
        this.discountPercent = discountPercent;
    }

    public int getDiscountPercent() {
        return discountPercent;
    }
}
  • discountPercent 필드를 ClassGrade 내부에서 관리하여 if 문을 제거한다.
  • 생성자를 통해서만 discountPercent 를 설정하도록 했고, 중간에 이 값이 변하지 않도록 불변으로 설계했다.
  • 상수를 정의할 때 각각의 등급에 따른 할인율(discountPercent)이 정해진다.

 

리팩토링 후 DiscountService

public class DiscountService {
    public int discount(ClassGrade classGrade, int price) {
        return price * classGrade.getDiscountPercent() / 100;
    }
}
  • if 문이 사라지고, classGrade.getDiscountPercent() 를 직접 호출하는 방식으로 개선되었다.
  • 단순히 회원 등급안에 있는 getDiscountPercent() 메서드를 호출하면 인수로 넘어온 회원 등급의 할인율을 바로 구할 수 있다.

 

실행 코드

public class ClassGradeRefMain1 {
    public static void main(String[] args) {
        int price = 10000;
        DiscountService discountService = new DiscountService();
        int basic = discountService.discount(ClassGrade.BASIC, price);
        int gold = discountService.discount(ClassGrade.GOLD, price);
        int diamond = discountService.discount(ClassGrade.DIAMOND, price);

        System.out.println("BASIC 등급의 할인 금액: " + basic);
        System.out.println("GOLD 등급의 할인 금액: " + gold);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
    }
}
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000

실행 결과는 기존 코드와 같다.

 

 

6. 열거형 - 리팩토링2

열거형도 클래스이다. 자바의 enum을 활용하면 더 간결한 코드로 개선할 수 있다.

 

리팩토링: 열거형 사용 - 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;
    }
}
  • 열거형을 사용하여 ClassGrade 대신 Grade 로 대체하였다.
  • 열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하다.
    따라서 생성자에 접근제어자를 선언할 수 없게 막혀있다. private 이라고 생각하면 된다.
  • BASIC(10) 과 같이 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출된다.
  • 상수별로 할인율을 직접 지정하여 관리할 수 있다.

 

리팩토링 후 DiscountService

public class DiscountService {
    public int discount(Grade grade, int price) {
        return price * grade.getDiscountPercent() / 100;
    }
}

 

실행 코드

public class EnumRefMain2 {
    public static void main(String[] args) {
        int price = 10000;
        DiscountService discountService = new DiscountService();
        int basic = discountService.discount(Grade.BASIC, price);
        int gold = discountService.discount(Grade.GOLD, price);
        int diamond = discountService.discount(Grade.DIAMOND, price);

        System.out.println("BASIC 등급의 할인 금액: " + basic);
        System.out.println("GOLD 등급의 할인 금액: " + gold);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
    }
}

실행 결과는 기존과 같다.

 

 

7. 열거형 - 리팩토링3

할인율 계산을 담당하는 DiscountService 를 제거하고, 할인율 계산을 ENUM 내부로 이동시켜 더욱 객체지향적인 설계를 만든다.

 

리팩토링: ENUM 내부에서 할인율 계산

public enum Grade {
    BASIC(10), GOLD(20), DIAMOND(30);

    private final int discountPercent;

    Grade(int discountPercent) {
        this.discountPercent = discountPercent;
    }

    public int discount(int price) {
        return price * discountPercent / 100;
    }
}
  • 할인율을 Grade 내부에서 직접 계산하도록 discount(int price) 메서드를 추가하였다.
  • 객체지향 관점에서 자신의 데이터를 외부에 노출하는 것 보다는, Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.

 

DiscountService를 제거하고 ENUM 사용

public class EnumRefMain3_2 {
    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));
    }
}
  • DiscountService 를 제거하고, Grade.BASIC.discount(price) 와 같이 ENUM에서 직접 메서드를 호출하도록 변경하였다.

 

실행 코드

public class EnumRefMain3_4 {
    public static void main(String[] args) {
        int price = 10000;
        Grade[] values = Grade.values();
        for (Grade grade : values) {
            printDiscount(grade, price);
        }
    }

    private static void printDiscount(Grade grade, int price) {
        System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
    }
}
  • 출력 부분의 중복을 제거했다.
  • 새로운 등급이 추가되더라도 main() 코드의 변경없이 모든 등급의 할인을 출력할 수 있다.
  • 기존과 동일한 결과를 얻으면서도 코드가 더 깔끔해지고 유지보수가 쉬워졌다.
반응형

댓글