공부/Java

자바 열거형 ENUM 복습하기

Stair 2024. 9. 20. 13:29
반응형

ENUM이 왜 필요한지 다음의 예제로 확인을 해보자.

public class DiscountService {

    public int discount(String grade, int price){
        int dicountPercent = 0;

        if(grade.equals("BASIC")){
            dicountPercent = 10;
        } else if (grade.equals("GOLD")) {
            dicountPercent = 20;
        } else if (grade.equals("DIAMOND")) {
            dicountPercent = 30;
        } else {
            System.out.println(grade + ": 할인X");
        }


        return (price * dicountPercent) / 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);
        int outBound = discountService.discount("등외", price);

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

 

 

위의 코드에서 BASIC, GOLD, DIAMOND 등급에게 할인을 적용할 수 있으나 존재하지 않는 등급, 소문자, 오타 또한 입력이 가능하다. 물론 할인은 적용되지 않는다.

 

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

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

2. 데이터 일관성 : GOLD, gold Gold 등 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다.

 

 

String 사용 시 타입 안정성이 부족하다.

1. 값의 제한 부족 : 대소문자 구분 어려움, 잘못된 문자열을 실수로 입력할 가능성이 있다.

2. 컴파일 시 오류 감지 불가 : 이런 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임시에만 오류가 확인이 가능하다.

 

그렇다면 상수를 사용하면 어떻게 될까?

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 dicountPercent = 0;

        if(grade.equals(StringGrade.BASIC)){
            dicountPercent = 10;
        } else if (grade.equals(StringGrade.GOLD)) {
            dicountPercent = 20;
        } else if (grade.equals(StringGrade.DIAMOND)) {
            dicountPercent = 30;
        } else {
            System.out.println(grade + ": 할인X");
        }


        return (price * dicountPercent) / 100;
    }
}
public class StringGradeEx1_1 {

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

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

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


        int outBound = discountService.discount("등외", price);
        System.out.println("등급 없음:  " + outBound);
    }
}

상수를 사용해도 마찬가지이다. 상수는 String이기 때문에 상수가 아닌 문자를 집어넣어도 컴파일이 된다.

 

 

이런 문제를 해결하려면 값을 제한해야 한다. --> 이 때문에 ENUM이 필요한 것이다.

타입 안전 열거형 패턴 (Type-Safe Enum Pattern)

지금까지 설명한 문제를 해결하기 위해 많은 개발자들이 오랜기간 고민하고 나온 결과가 바로 타입 안전 열거형 패턴이다.

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

 

 

1. 회원 등급을 다루는 클래스를 만들고, 각각의 회원 등급별 상수를 선언한다.

2. 이때 각각의 상수마다 별도의 인스턴스를 생성하고, 생성한 인스턴스를 대입한다.

3. 상수로 선언하기 위해 static, final을 사용한다.(static이기에 상수가 메서드영역에 선언)

public class ClassRefMain {

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

        System.out.println("ref BASIC = " + ClassGrade.BASIC);
        System.out.println("ref GOLD = " + ClassGrade.GOLD);
        System.out.println("ref DIAMOND = " + ClassGrade.DIAMOND);
    }
}

 

1. 각각의 상수는 모두 ClassGrade 타입을 기반으로 인스턴스를 만들었기 때문에 getClass 결과가 모두 ClassGrade이다

2. 각각의 상수는 모두 서로 각각 다른 ClassGrade 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.

public class ClassGrade {

    //x001
    public static final ClassGrade BASIC = new ClassGrade();
    //x002
    public static final ClassGrade GOLD = new ClassGrade();
    //x003
    public static final ClassGrade DIAMOND = new ClassGrade();

}
public class DiscountService {

    public int discount(ClassGrade classGrade, int price){
        int dicountPercent = 0;


        if(classGrade == ClassGrade.BASIC){ //x001
            dicountPercent = 10;
        } else if (classGrade == ClassGrade.GOLD) { //x002
            dicountPercent = 20;
        } else if (classGrade == ClassGrade.DIAMOND) { //x003
            dicountPercent = 30;
        }else {
            System.out.println("할인 X");
        }


        return (price * dicountPercent) / 100;
    }
}
public class ClassGradeEx2_1 {

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

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

 

이 방식도 한가지 문제가 있는데 classGrade()를 생성해버리는 문제가 발생할 수 있다.

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

    DiscountService discountService = new DiscountService();

    int discount = discountService.discount(new ClassGrade(), price);

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

}

 

 

이문제를 해결하려면 외부에서 생성할 수 없게 private 생성자로 막으면 된다.

public class ClassGrade {

    //x001
    public static final ClassGrade BASIC = new ClassGrade();
    //x002
    public static final ClassGrade GOLD = new ClassGrade();
    //x003
    public static final ClassGrade DIAMOND = new ClassGrade();

    
    private ClassGrade() {
        //priavte 생성자 추가
    }
}

 

private 생성자를 사용해서 외부에 ClassGrade를 임의로 생성하지 못하게 막았다.

private 생성자 덕분에 ClassGrade의 인스턴스를 ClassGrade 내부에서만 생성할 수 있다.

이제 ClassGrade 인스턴스를 사용할때는 내부에 정의한 상수만을 사용해야 한다.

 

이렇게 private 생성자까지 사용하면 타입 안전 열거형 패턴을 완성할 수 있다.

 

타입 안전 열거형 패턴의 장점

타입 안정성 향상 : 정해진 객체만 사용할 수 있기 때문에 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있다.

데이터 일관성 : 정해진 객체만 사용하므로 데이터의 일관성이 보장된다.

 

단점

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

 

 

 

자바는 앞서 본 타입 안전 열거형 패턴을 아주 편리하게 사용할 수 있는 열거형(Enum Type)을 제공한다.

영어인 enum은 enumeration의 줄임말이며, 일련의 명명된 상수들의 집합을 정의하는 것을 의미하며, 프로그래밍에서는 이러한 상수들을 사용하여 코드 내에서 미리 정의된 값들의 집합을 나타낸다.

쉽게 이야기해서 회원의 등급은 상수로 정의한 BASIC, GOLD, DIAMOND만 사용할 수 있다는 뜻이다.

자바의 enum은 타입 안전성을 제공하고, 코드의 가독성을 높이며, 예상 가능한 값들의 집합을 표현하는 데 사용된다.

public enum Grade {
    BASIC, GOLD, DIAMOND
}

 

public class ClassGrade {

    //x001
    public static final ClassGrade BASIC = new ClassGrade();
    //x002
    public static final ClassGrade GOLD = new ClassGrade();
    //x003
    public static final ClassGrade DIAMOND = new ClassGrade();


    private ClassGrade() {

    }
}

위 두 코드는 차이가 거의 없다.

 

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

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

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

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){
        return Integer.toHexString(System.identityHashCode(grade));
    }
}

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

그리고 각각의 인스턴스도 서로 다른 것을 확인할 수 있다.

(참고로 열거형은 toString을 오버라이딩해서 재정의했기때문에 그냥 찍으면 참조값을 확인할 수 없다. 그래서 refValue를 만들어서 참조값을 확인할 수 있도록 하였다)

 

**열거형도 클래스이다. 열거형을 제공하기 위해 제약이 추가되었다고 생각하자.**

 

 

열거형(ENUM)의 장점

타입 안정성 향상 : 열거형은 사전에 정의된 상수들로만 구성된다.

간결성 및 일관성 : 열거형을 사용하면 코드가 더 간결하고 명확해진다.

확장성 : 새로운 회원 등급 타입을 추가하고 싶을 때, ENUM에 새로운 상수를 추가하기만 하면 된다.

 

 

열거형은 java.lang.Enum 클래스를 상속받기 때문에 여러 기능이 있는 메서드들을 사용할 수 있다.

public class EnumMethodMain {

    public static void main(String[] args) {


        //모든 ENUM 반환
        Grade[] values = Grade.values();
        System.out.println("values = " + Arrays.toString(values));

        for (Grade value : values) {
            System.out.println("name= " + value.name() + "  ordinal= " + value.ordinal());
        }

        //String -> ENUM 변환
        String input ="GOLD";
        Grade gold = Grade.valueOf(input);
        System.out.println("gold= " + gold); //toString()오버라이딩 가능


    }

}

 

 

 

 

앞서 만들었던 열거형을 효율적으로 리팩토링 하는 방법을 알아보자.

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

    private final int discountPercent;

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

    public int getDiscountPercent() {
        return discountPercent;
    }
}

 

열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하다. BASIC(10)과 같이 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하여 적절한 생성자를 호출시킨다.

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 = 10_000;

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

 

DiscountService는 이제 단순한 할인율 계산만 남았다.

 

어짜피 DiscountService는 할인율 계산만 남았으니 이 메서드를 Grade에 생성 후 DiscountService를 제거하여 캡슐화를 높여보자.

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

    private final int discountPercent;

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

    public int getDiscountPercent() {
        return discountPercent;
    }

    public int discount(int price){
        return price * getDiscountPercent()/100;
    }
}
public class EnumRefMain2 {

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

        int basic = Grade.BASIC.discount(price);
        int gold = Grade.GOLD.discount(price);
        int diamond = Grade.DIAMOND.discount(price);

        System.out.println("basic = " + basic);
        System.out.println("gold = " + gold);
        System.out.println("diamond = " + diamond);
    }
}

 

결국 inline 하면

public class EnumRefMain2 {

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

        System.out.println("basic = " + Grade.BASIC.discount(price));
        System.out.println("gold = " + Grade.GOLD.discount(price));
        System.out.println("diamond = " + Grade.DIAMOND.discount(price));
    }
}

 

훨씬 간결한 main문을 만들 수 있다.

public class EnumRefMain2 {

    public static void main(String[] args) {
        int price = 10_000;
        
        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));
    }
}
public class EnumRefMain2 {

    public static void main(String[] args) {
        int price = 10_000;
        Grade[] values = Grade.values();
        for (Grade value : values) {
            printDiscount(value, price);
        }
    }

    private static void printDiscount(Grade grade, int price){
        System.out.println(grade.name()+"등급의 할인 금액: " + grade.discount(price));
    }
}

 

 

 

반응형