공부/Java

자바 열거형-ENUM

Stair 2024. 9. 11. 15:39
반응형

자바가 제공하는 열거형(Enum Type)을 제대로 이해하려면 먼저 열거형이 생겨난 이유를 알아야 한다. 예제를 순서대로 따라가며 열거형이 만들어진 근본적인 이유를 알아보자

 

다음은 회원등급에 맞는 할인등급이 적용되는 예제이다.

public class ThisCountService {

    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 + "할인 없음");
        }

        //10000 * (20 / 100) -> 2000원
        return price*discountPercent/100;
    }
}
public static void main(String[] args) {
    int price = 10000;

    ThisCountService thisCountService = new ThisCountService();

    int basic = thisCountService.discount("BASIC", price);
    int gold = thisCountService.discount("GOLD", price);
    int diamond = thisCountService.discount("DIAMOND", price);

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

}

 

 

하지만 문자열을 받는 것이기 때문에 다음과 같은 결과들 또한 나올 수 있다.

public static void main(String[] args) {
    int price = 10000;

    ThisCountService thisCountService = new ThisCountService();

    //존재하지 않는 등급
    int vip = thisCountService.discount("VIP",price);

    System.out.println("vip등급의 할인 금액: " + vip);

    int dimond = thisCountService.discount("DIMOND", price);
    System.out.println("DIMOND등급의 할인 금액: " + dimond );

    //소문자 입력
    int gold = thisCountService.discount("gold", price);
    System.out.println("gold등급의 할인 금액: " + gold);

}

이번 예제에서는 다음과 같은 문제가 발생했다.

1. 존재하지 않는 VIP라는 등급을 입력했다.

2. 오타 : DIAMOND가 아닌 DIMOND라는 오타를 발생했다

3. GOLD가 아닌 gold라는 소문자를 입력하였다.

 

 

등급에 문자열을 사용하는 지금의 방식은 다음과 같은 문제가있다.

타입 안정성 부족 : 문자열을 오타가 발생하기 쉽고, 유효하지 않은 값이 입력 될 수 있다.

데이터 일관성 : "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 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 + "할인 없음");
    }

    //10000 * (20 / 100) -> 2000원
    return price*discountPercent/100;
}
public static void main(String[] args) {
    int price = 10000;

    ThisCountService thisCountService = new ThisCountService();

    int basic = thisCountService.discount(StringGrade.BASIC, price);
    int gold = thisCountService.discount(StringGrade.GOLD, price);
    int diamond = thisCountService.discount(StringGrade.DIAMOND, price);

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

}

 

문자열 상수를 사용한 덕분에 코드의 오류를 줄일 수 있고 전체적으로 코드가 명확해졌다.

하지만 문자열 상수를 사용해도, 지금까지 발생한 문제들을 근본적으로 해결할 수는 없다. 왜냐하면 String 타입은 어떤 문자열이든 입력할 수 있기 때문이다.

public static void main(String[] args) {
    int price = 10000;

    ThisCountService thisCountService = new ThisCountService();

    //존재하지 않는 등급
    int vip = thisCountService.discount("VIP",price);

    System.out.println("vip등급의 할인 금액: " + vip);

    int dimond = thisCountService.discount("DIMOND", price);
    System.out.println("DIMOND등급의 할인 금액: " + dimond );

    //소문자 입력
    int gold = thisCountService.discount("gold", price);
    System.out.println("gold등급의 할인 금액: " + gold);

}

 

public int discount(String grade, int price)

이유는 discount에서 받을 수 있는 매개변수가 String이기 때문이다.

 

그럼 String대신 BASIC,GOLD,DIAMOND외의 다른 문자가 못들어오게 막을 수 있을까?

이러한 문제를 어떻게 해결해야할까?

 

 

타입 안전 열거형 패턴을 통해 이 문제를 해결할 수 있다.

enum은 enumeration의 줄임말인데, 번역하면 열거라는 뜻이고, 어떤 항목을 나열하는 것을 뜻한다.

타입 안전 열거형 패턴을 사용하면 이렇게 나열한 항목만 사용할 수 있다는 것이 핵심이다. 나열한 항목이 아닌 것은 사용할 수 없다.

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, final을 사용한다.

 

 

public class ThisCountService {

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


        //10000 * (20 / 100) -> 2000원
        return price*discountPercent/100;
    }
}
public static void main(String[] args) {
    int price = 10000;
    ThisCountService thisCountService = new ThisCountService();

    int basic = thisCountService.discount(ClassGrade.BASIC, price);
    int gold = thisCountService.discount(ClassGrade.GOLD, price);
    int diamond = thisCountService.discount(ClassGrade.DIAMOND, price);

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

}

 

    public static final ClassGrade BASIC = new ClassGrade();
    public static final ClassGrade GOLD = new ClassGrade();
    public static final ClassGrade DIAMOND = new ClassGrade();

 

각각의 코드가 x001, x002, x003이라는 참조 값을 가질 때 

discount 매개변수에 x001이 들어가면 

x001 == ClassGradeBASIC -> x001 == x001이 되기 때문에

값이 제대로 나올 수 있게 되었다.

 

(근데 나 왜 여지껏 discount를 thiscount라고 쓴 걸까?)

 

근데 이 방식도 한가지 단점이 있다.

public static void main(String[] args) {
    int price = 10000;
    ThisCountService thisCountService = new ThisCountService();
    ClassGrade newClassGrade = new ClassGrade();
    int result = thisCountService.discount(newClassGrade, price);

    System.out.println("result = " + result);


}

위처럼 ClassGrade의 인스턴스를 생성해서 thisCountService.discount()의 매개변수에 집어넣는 것이다.

 

이런 문제를 해결하려면 외부에서 ClassGrade를 생성할 수 없도록 막으면 된다.

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 내부에 정의한 상수를 사용해야 한다.

 

 

정리

제한된 인스턴스 생성 : 클래스는 사전에 정의된 몇개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용할 수 있도록 하여 정의된 값을만 사용하도록 보장한다.

타입 안정성 : 이 패턴을 사용하면, 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지할 수 있다.

 

단점: 이 패턴을 구현하려면 많은 코드를 작성해야하고 private 생성자를 추가하는 등 유의해야 하는 부분들도 있다.

 

 

 

이 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을 제공한다.

자바에서 열거형은 앞서 배운 타입 안전 열거형 패턴을 쉽게 사용할 수 있도록 프로그래밍 언어에서 지원하는 것이다.

 

 

 

클래스 대신 public enum을 사용하고, 원하는 상수의 이름을 나열하기만 하면 된다.

자바의 열거형으로 작성한 Grade는 이전 클래스로 구현한 코드와 거의 비슷하다.

 

열거형의 특징은 다음과 같다.

1. 열거형도 클래스이다.

2. 열거형은 자동으로 java.lang.Enum을 상속받는다.

3. 외부에서 임의로 생성할 수 없다.

 

 

 

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){
    //참조값 확인 후 리턴
    return Integer.toHexString(System.identityHashCode(grade));
}

 

이 코드의 실행 결과를 보면 상수들이 열거형으로 선언한 타입인 Grade 타입을 사용하는 것을 확인할 수 있다.

그리고 각각의 인스턴스도 서로 다르다.

또한 Enum타입은 인스턴스화 할 수 없기 때문에

Grade grade = new Grade();

처럼 인스턴스를 생성할 수 없다.

 

 

위와 같은 모든 열거형은 java.lang.Enum 클래스를 자동으로 상속 받는다. 따라서 해당 클래스가 제공하는 기능들을 사용할 수 있다.

public static void main(String[] args) {
    Grade[] values = Grade.values();
    System.out.println("Arrays.toString(values) = " + Arrays.toString(values));
    for (Grade value : values) {
        System.out.println("value = " + value);
    }
    //Arrays.toString() == 배열 내부의 값을 출력할 때 사용

    //String -> ENUM 변환
    String input = "GOLD";
    Grade gold = Grade.valueOf(input);
    System.out.println("gold = " + gold);

}

 

values() : 모든 ENUM 상수를 포함하는 배열을 반환

valueOf(String name) : 주어진 이름과 일치하는 ENUM 상수를 반환한다.

name() : ENUM 상수의 이름을 문자열로 반환한다.

ordinal() : ENUM 상수의 선언 순서(0부터 시작)를 반환한다.

 

 

ordinal()은 가급적 사용하지 않는 것이 좋다.

이 값을 사용하다가 중간에 상수를 선언하는 위치가 변경되면 전체 상수의 위치가 모두 변경될 수 있기 때문이다.

 

 

반응형

'공부 > Java' 카테고리의 다른 글

자바 중첩 클래스, 내부 클래스  (1) 2024.09.13
자바 날짜와 시간  (1) 2024.09.12
자바 래퍼 클래스(wrapper class)  (0) 2024.09.10
자바 String 클래스  (1) 2024.09.10
자바 불변 객체  (0) 2024.09.09