열거형 - ENUM

박병욱·2025년 6월 22일

Java

목록 보기
17/38
post-thumbnail

🔡 문자열과 타입 안정성

먼저 아래 요구사항에 따라 클래스를 만들어보자.

고객은 3등급으로 나누고, 상품 구매 시 등급별로 할인을 적용한다. 할인 시 소수점 이하는 버린다.

  • BASIC: 10% 할인
  • GOLD: 20% 할인
  • DIAMOND: 30% 할인

 

package enumeration.ex0;

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

회원 등급 외 다른 값이 입력되면 “할인 X” 를 출력하도록 했다.

 

package enumeration.ex0;

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);
        System.out.println("GOLD 등급의 할인 금액: " + gold);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
    }
}

/*
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
*/

실행 결과를 보면, 각각의 회원 등급에 맞게 할인이 들어간 것을 볼 수 있다. 하지만 이런 식으로 단순한 문자열로 입력하면 오타 가능성도 있고, 유효하지 않는 값이 입력될 수도 있다. 아래 코드를 보자.

package enumeration.ex0;

public class StringGradeEx0_2 {
    public static void main(String[] args) {

        int price = 10000;

        DiscountService discountService = new DiscountService();

        // 존재하지 않는 등급(VIP)
        int vip = discountService.discount("VIP", price);
        System.out.println("VIP 등급의 할인 금액: " + vip);

        // 오타
        int diamondd = discountService.discount("DIAMONDD", price);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamondd);

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

/*
VIP: 할인 X
VIP 등급의 할인 금액: 0
DIAMONDD: 할인 X
DIAMOND 등급의 할인 금액: 0
gold: 할인 X
GOLD 등급의 할인 금액: 0
*/

위의 코드에서 VIP라는 존재하지 않는 등급이 입력되거나, DIAMONDD처럼 오타가 발생할 수도 있고, 대문자를 입력해야 하는데 소문자를 입력할 수도 있다. 이처럼 등급에 문자열을 사용하는 방식은 아래와 같은 문제점이 있다.

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

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

 

이러한 문제들은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에 오류를 찾기 힘들고 디버깅이 어려워질 수 있다. 이런 문제를 해결하기 위해 특정 범위로 값을 제한해야 할 필요가 있다. 예를 들어, BASIC, GOLD, DIAMOND라는 정확한 문자만 discount() 메서드에 전달되어야 한다. 하지만 String은 어떤 문자열이든 받을 수 있기 때문에 자바 문법 관점에서는 아무런 문제가 없다. 결론은 String같은 타입을 사용해서는 문제를 해결할 수 없다는 것이다.

 
그렇다면 문자열을 상수로 만든다면 괜찮지 않을까? 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 직접 사용하는 것보다는 안전할 것 같다. 아래 코드를 보자.

package enumeration.ex1;

public class StringGrade {

    public static final String BASIC = "BASIC";
    public static final String GOLD = "GOLD";
    public static final String DIAMOND = "DIAMOND";
}
package enumeration.ex1;

public class DiscountService {

    // StringGrade를 참고
    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;
    }
}
package enumeration.ex1;

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

/*
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
*/

StringGrade 클래스에 문자열 상수를 일목요연하게 작성하고, 다른 클래스에서 discount()에 인자를 전달할 때도 잘 작동하는 것처럼 보인다. 더 좋은 점은, 만약 실수로 상수의 이름을 잘못 입력해도 컴파일 시점에 오류를 찾아줘서 빠르게 문제를 찾을 수 있다는 점이다. 이러면 문제 없는거 아닌가...?

 

하지만 아쉽게도 문자열 상수를 사용해도 근본적으로 문제를 해결할 수는 없다. 왜냐하면 String 타입은 어떤 문자열이든 입력할 수 있기 때문이다. 아래처럼 StringGrade에 있는 문자열 상수를 사용하지 않고, 직접 문자열을 사용해도 막을 방도가 없다.

package enumeration.ex1;

public class StringGradeEx1_2 {
    public static void main(String[] args) {

        int price = 10000;

        DiscountService discountService = new DiscountService();

        // 존재하지 않는 등급(VIP)
        int vip = discountService.discount("VIP", price);
        System.out.println("VIP 등급의 할인 금액: " + vip);

        // 오타
        int diamondd = discountService.discount("DIAMONDD", price);
        System.out.println("DIAMOND 등급의 할인 금액: " + diamondd);

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

/*
VIP: 할인 X
VIP 등급의 할인 금액: 0
DIAMONDD: 할인 X
DIAMOND 등급의 할인 금액: 0
gold: 할인 X
GOLD 등급의 할인 금액: 0
*/

⛓ 타입 안전 열거형 패턴

해결책은 Type-Safe Enum Pattern, 즉 타입 안전 열거형 패턴이다. 여기서 enumenumeration의 줄임말인데, 어떤 항목을 나열하는 것을 뜻한다. 우리의 경우, 회원 등급인 BASIC, GOLD, DIAMOND를 나열하는 것인데, 여기서 중요한 것은 타입 안전 열거형 패턴을 사용하면 "딱 저 3가지만" 안전하게 사용할 수 있다는 것이 핵심이다. 나열한 항목이 아닌 것은 사용할 수 없다.

package enumeration.ex2;

public class ClassGrade {

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

}

회원 등급을 다루는 클래스(ClassGrade)를 만들고, 각각의 회원 등급별로 상수(static final)를 선언한다. 이때 각각의 상수마다 별도의 인스턴스를 생성하고, 생성한 인스턴스를 대입한다.

 

해당 코드를 확실히 이해하기 위해 다음 코드를 실행해보자.

package enumeration.ex2;

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.ex02.ClassGrade
class GOLD = class enumeration.ex02.ClassGrade
class DIAMOND = class enumeration.ex02.ClassGrade

ref BASIC = enumeration.ex02.ClassGrade@a09ee92
ref GOLD = enumeration.ex02.ClassGrade@30f39991
ref DIAMOND = enumeration.ex02.ClassGrade@452b3a41
*/

각각의 상수는 모두 ClassGrade 타입을 기반으로 인스턴스를 만들었기 때문에 getClass() 메서드의 결과는 모두 ClassGrade다. 보다시피 각각의 상수는 모두 각각 다른 ClassGrade 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다. static이므로 애플리케이션 로딩 시점에 3개의 ClassGrade 인스턴스가 생성되고, 각각의 상수는 같은 ClassGrade 타입의 서로 다른 인스턴스의 참조값을 가진다.

 

여기서는 BASIC, GOLD, DIAMOND를 상수로 열거했다. 이제 ClassGrade 타입을 사용할 때는 열거한 상수들만 사용하면 되는 것이다.

package enumeration.ex2;

public class DiscountService {

    // discount 메서드는 이제 ClassGrade 타입을 받는다.
    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;
    }
}

discount() 메서드는 매개 변수로 ClassGrade 클래스를 사용한다. 값을 비교할 때는 == 참조값 비교를 사용하면 된다. 매개 변수로 넘어온 인수도 ClassGrade가 가진 상수 중에 하나를 사용한다.

package enumeration.ex2;

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

/*
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
*/

위처럼 discount() 메서드를 호출할 때 미리 정의한 ClassGrade의 상수를 전달한다. 그런데 지금의 코드는 외부에서 임의로 ClassGrade의 인스턴스를 생성할 수 있다는 문제가 있기에 private 생성자를 사용해야 한다.

package enumeration.ex2;

public class ClassGradeEx2_2 {
    public static void main(String[] args) {

        int price = 10000;

        DiscountService discountService = new DiscountService();

//        ClassGrade newClassGrade = new ClassGrade();  // 이런 코드가 있도록 해서는 안 된다.
//        int result = discountService.discount(newClassGrade, price);
//        System.out.println("newClassGrade 등급의 할인 금액: " + result);
    }
}

/*
할인 X
newClassGrade 등급의 할인 금액: 0
*/

 

아래처럼 ClassGrade의 기본 생성자를 private으로 변경하자.

package enumeration.ex2;

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() {}
}
package enumeration.ex2;

public class ClassGradeEx2_2 {
    public static void main(String[] args) {

        int price = 10000;

        DiscountService discountService = new DiscountService();

//        ClassGrade newClassGrade = new ClassGrade();
//        int result = discountService.discount(newClassGrade, price);
//        System.out.println("newClassGrade 등급의 할인 금액: " + result);
    }
}

/*
할인 X
newClassGrade 등급의 할인 금액: 0
*/

위의 코드처럼 private 생성자를 사용해서 외부에서 ClassGrade를 임의로 생성하지 못하게 막았다. private 생성자 덕분에 ClassGrade의 인스턴스를 생성하는 것은 ClassGrade 클래스 내부에서만 할 수 있다. 이제 ClassGrade 인스턴스를 사용할 때는 ClassGrade 내부에 정의한 상수(BASIC, GOLD, DIAMOND)를 사용해야 한다. 그렇지 않으면 컴파일 오류가 발생할 것이다. 이렇게 private 생성자까지 사용하면 타입 안전 열거형 패턴이 완성되는 것이다.

 

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

  • 타입 안정성 향상: 클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 해당 인스턴스들만 사용할 수 있도록 한다.
  • 데이터 일관성: 정해진 객체만 사용하므로 데이터의 일관성이 보장된다.

하지만 이 패턴을 사용하려면 많은 양의 코드가 필요하고, private 생성자를 추가해줘야 하는 등, 유의해야 하는 부분들도 있다.


🪜 열거형 - Enum Type

자바는 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을 제공한다. 자바의 enum은 타입 안전상을 제공하고, 코드의 가독성을 높이며, 예상 가능한 값들의 집합을 표현하는 데 사용된다.

package enumeration.ex3;

public enum Grade {
    BASIC, GOLD, DIAMOND
}

열거형을 정의할 때는 class 대신, enum을 사용한다. 원하는 상수의 이름을 나열하면 된다. 보다시피 ClassGrade를 구현한 것보다 훨씬 편리하다. 열거형은 자동으로 java.lang.Enum을 상속받는 클래스로, 외부에서 임의로 생성할 수 없다.

package enumeration.ex3;

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(Grade grade) {
		// 숫자로 반환된 객체의 참조값을 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 = a09ee92
ref GOLD = 30f39991
ref DIAMOND = 452b3a41
*/

실행 결과를 보면 상수들이 열거형으로 선언한 타입인 Grade 타입을 사용하는 것을 볼 수 있다. 따라서 각각의 인스턴스도 서로 다른 것을 확인할 수 있다. 참고로 열거형은 toString()을 재정의 하기 때문에 참조값을 직접 확인할 수 없어서 참조값을 구하기 위해 refValue()를 만들었다.

 

이제 자바의 열거형을 사용해보자.

package enumeration.ex3;

public class DiscountService {

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

        if (classGrade == Grade.BASIC) {
            discountPercent = 10;
        } else if (classGrade == Grade.GOLD) {
            discountPercent = 20;
        } else if (classGrade == Grade.DIAMOND) {
            discountPercent = 30;
        } else {
            System.out.println("할인 X");
        }

        return price * discountPercent / 100;
    }
}
package enumeration.ex3;

public class ClassGradeEx3_1 {
    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);
    }
}

/*
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
*/

앞서 살펴봤던 타입 안전 열거형 패턴을 직접 구현한 코드와 같은 것을 확인할 수 있다. 그리고 열거형은 아래와 같이 외부 생성이 불가하다. 생성할 경우, 컴파일 오류가 발생한다.

package enumeration.ex3;

public class ClassGradeEx3_2 {
    public static void main(String[] args) {

        int price = 10000;

        DiscountService discountService = new DiscountService();

//        Grade grade = new Grade();  // enum은 생성 불가
//        int result = discountService.discount(grade, price);
//        System.out.println("result price: " + result);
    }
}

 

열거형(ENUM)의 장점

  • 유효하지 않은 값은 입력될 가능성이 없다.
  • 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.
  • 새로운 타입을 추가하고 싶을 때는, ENUM에 새로운 상수를 추가하기만 하면 된다.

 

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


🎯 열거형의 주요 메서드

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

package enumeration.ex3;

import java.util.Arrays;

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

/*
values = [BASIC, GOLD, DIAMOND]
name=BASIC, ordinal=0
name=GOLD, ordinal=1
name=DIAMOND, ordinal=2
gold = GOLD
*/

 

<ENUM - 주요 메서드>

  • values(): 모든 ENUM 상수를 포함하는 배열을 반환
  • valueOf(String name): 주어진 이름과 일치하는 ENUM 상수를 반환
  • name(): ENUM 상수의 이름을 문자열로 반환
  • ordinal(): ENUM 상수의 선언 순서(0부터 시작)를 반환
  • toString(): ENUM 상수의 이름을 문자열로 반환, name() 메서드와 유사하지만 toString()은 직접 오버라이드 할 수 있다.

 

🔥 주의 사항 : ordinal()은 가급적 사용하지 않는 것이 좋다.

  • ordinal()의 값은 가급적 사용하지 않는 것이 좋다. 값을 사용하다가 중간에 상수를 선언하는 위치가 변경되면 전체 상수의 위치가 모두 변경될 수 있기 때문이다.
  • 예를 들어, 중간에 BASIC 다음에 SILVER 등급이 추가되는 경우, GOLD, DIAMOND의 값이 하나씩 추가된다. 쉽게 말해, ordinal()의 값을 사용하면 기존 GOLD 회원이 갑자기 SILVER가 되는 큰 버그가 발생할 수 있다는 소리다.

 

열거형에 대해 최종적으로 정리하자면,

  • 열거형은 java.lang.Enum를 강제로 상속 받기 때문에 추가로 다른 클래스를 상속 받을 수 없다.
  • 열거형은 인터페이스를 구현할 수 있다.
  • 열거형에 추상 메서드를 선언하고, 구현할 수 있다.

🔨 열거형 - 리팩토링1

지금까지 구현한 코드, 일단 아래 코드부터 리팩토링 해보자.

package enumeration.ex2;

public class DiscountService {

    // discount 메서드는 이제 ClassGrade 타입을 받는다.
    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;
    }
}

이 코드는 결국 각각의 회원 등급별로 할인율이 결정된다. 할인율은 결국 회원 등급을 따라간다. 따라서 회원 등급 클래스가 할인율을 가지고 관리하도록 변경하도록 하자.

package enumeration.ref1;

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) 필드를 추가했다. getDiscountPercent() 조회 메서드도 추가하고, 생성자를 통해서만 discountPercent를 설정하도록 했다. 이제 상수를 정의할 때 각각의 등급에 따른 할인율이 정해진다.

package enumeration.ref1;

public class DiscountService {

    public int discount(ClassGrade classGrade, int price) {
        return price * classGrade.getDiscountPercent() / 100;
    }
}

 

기존의 if문이 완전히 제거되고, 계산된 할인 금액만 간단히 반환하도록 했다. 단순히 회원 등급 안에 있는 getDiscountPercent() 메서드를 호출하면 인수로 넘어온 회원 등급의 할인율을 바로 구할 수 있다.

package enumeration.ref1;

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
*/

⛏️ 열거형 - 리팩토링2

앞서 학습한대로 열거형도 클래스라고 했다. 열거형인 Grade에 동일하게 적용해서 리팩토링을 해보자.

package enumeration.ref2;

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

    private final int discountPercent;

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

    public int getDiscountPercent() {
        return discountPercent;
    }
}

discountPercent 필드를 불변으로 추가하고, 생성자를 통해서 필드에 값을 저장한다. 열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하다. 따라서 생성자에 접근 제어자를 선언할 수 없게 막혀있다. private이라고 생각하면 된다. BASIC(10)과 같이 상수 마지막의 괄호 안에 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출된다. 값을 조회하기 위한 getDiscountPercent() 메서드도 추가했다.

 

package enumeration.ref2;

public class DiscountService {

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

마찬가지로, 단순한 할인율 계산 로직만 남았다.

 

package enumeration.ref2;

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

/*
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
*/

🛠️ 열거형 - 리팩토링3

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 class DiscountService {

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

해당 코드를 보면, Grade가 가지고 있는 데이터인 discountPercent의 값을 꺼내서 사용하고 있다. 객체 지향 관점에서는 Grade처럼 자신의 데이터를 외부에 노출하는 것 보다는, Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다. 그러므로 Grade 클래스 안으로 discount() 메서드를 이동시키자.

package enumeration.ref3;

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

    private final int discountPercent;

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

    public int getDiscountPercent() {
        return discountPercent;
    }

    // 추가
    public int discount(int price) {
        return price * discountPercent / 100;
    }
}

이제 클래스 내부에 discount() 메서드를 만들었기 때문에 할인율을 스스로 계산할 수 있다.

package enumeration.ref3;

public class DiscountService {

    public int discount(Grade grade, int price) {
        return grade.discount(price);
    }
}

 

할인율 계산은 이제 클래스가 담당하고, DiscountService.discount() 메서드는 그저 Grade.discount()를 호출하기만 하면 된다.

package enumeration.ref3;

public class EnumRefMain3_1 {
    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);
    }
}

/*
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
VIP 등급의 할인 금액: 4000
*/

보다시피, 실행 결과는 전과 동일하다.

 

❌ DiscountService 제거

이제 클래스 내부에서 스스로 할인율을 계산하기 때문에 DiscountService 클래스는 더 필요하지 않다.

package enumeration.ref3;

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

/*
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
VIP 등급의 할인 금액: 4000
*/

이제 각각의 등급별로 discount() 메서드를 호출하면 할인율을 구할 수 있다. 이제 출력 부분의 중복을 제거하자.

package enumeration.ref3;

public class EnumRefMain3_3 {
    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));
    }
}

/*
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
VIP 등급의 할인 금액: 4000
*/

grade.name()을 통해 ENUM의 상수 이름을 사용할 수 있게 되었다.

 

🗂️ ENUM 목록

이제 새로운 등급이 추가돼도 main() 메서드의 코드 변경이 없도록 모든 등급의 할인 금액을 출력해보자.

package enumeration.ref3;

public class EnumRefMain3_4 {
    public static void main(String[] args) {

        int price = 10000;
        Grade[] grades = Grade.values();
        for (Grade grade : grades) {
            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
VIP 등급의 할인 금액: 4000
*/

이처럼 Grade.values()를 사용하면 Grade 열거형의 모든 상수를 배열로 구할 수 있다.

profile
도메인을 이해하는 백엔드 개발자(feat. OOP)

0개의 댓글