열거형 - ENUM

황상익·2024년 5월 14일

Inflearn JAVA

목록 보기
28/61

문자열과 타입 안전성1

자바가 제공하는 열거형(Enum Type)을 제대로 이해하려면 먼저 열거형이 생겨난 이유를 알아야 한다.

public class DiscountService {

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

        if (grade.equals("BASIC")){
            discountPercent = 0;
        } 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 StringGradeEx_0 {
    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);
    }
}
public class StringGradeEx_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 vip = discountService.discount("VIP", price);

        //오타
        int diamonnd = discountService.discount("DIAMONDD", price);

        //대소문자
        int gold1 = discountService.discount("Gold" , price);

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

문제점

  • 존재하지 않는 VIP 라는 등급을 입력
  • 오타
  • 소문자 입력 -> 등급은 다 대문자인데 소문자를 입력

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

  • 타입 안정성 부족 : 문자열은 오타가 발생하기 쉽고, 유효하지 않은 값이 입력 가능
  • 데이터 일관성 : 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어짐

String 사용시 타입 안정성 문제

  • 값의 제한 부족 : String으로 상태나 카테고리를 표현하면 잘못된 문자열을 실수로 입력할 가능성

  • 컴파일 시 오류 감지 불가 : 잘못된 값에는 컴파일 시에는 감지 X. 런타임시 문제 발견

문자열과 타입 안정성 2

public class StringGrade {
    public static final String BASIC = "BASIC";
    public static final String GOLD = "BASIC";
    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 = 0;
        } 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;
    }
}
public class StringGradeEx_0 {
    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);
    }
}

문자열 상수를 사용한 덕분에 전체적으로 코드 명확, 인자를 전달할 때도 StringGrade가 제공하는 문자열 상수를 사용, 더 좋은 점은 만약 실수로 상수의 이름을 잘못 입력하면 컴파일 시점에 오류 발생

하지만 문자열 상수를 사용해도 지금까지 발생한 문제점들을 극복 X, String 타입은 어떤 문자열이든 입력 가능.

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

        DiscountService discountService = new DiscountService();

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

        //오타
        int diamonnd = discountService.discount("DIAMONDD", price);
        System.out.println("diamonnd = " + diamonnd);

        //대소문자
        int gold1 = discountService.discount("Gold" , price);
        System.out.println("gold1 = " + gold1);
    }
}

그리고 사용해야 하는 문자열 상수가 어디 있는지 discount() 호출하는 개발자가 알 수 있을까??

public int discount(String grade, int price) {}

주석을 남겨서 상수를 사용해달라고 해야 함

타입 안전 열겨형 패턴

enum은 enumeration의 줄임말, -> 번역하면 열거라는 의미. 어떤 항목을 나열하는 것을 뜻함.
BASIC, GOLD, DIAMOND를 나열. 여기서 중요한 것은 타입 안전 열거형 패턴을 사용하면 이렇게 나열한 항목만 사용할 수 있는 것이 핵심.
나열하지 않은 항목은 사용 X

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을 사용
    -> static을 사용하기 위해서 상수를 메서드 영역에 선언
    -> final을 사용해서 인스턴스를 변경 X

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

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

각 상수는 ClassGrade 타입을 기반으로 인스턴스를 만들었기 때문에 getClass의 결과는 모두 classGrade
각 상수는 모두 서로 각각 다른 ClassGrade 인스턴스를 참조 -> 참조값 다르게 출역

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;
    }
}
  • disocunt 메서드는 매개변수는 ClassGrade 클래스를 사용
  • 값을 비교할 때는 classGrade == ClassGrade.Basic과 같이 == 참조값을 비교
    -> 매개변수에 넘어오는 인수도 ClassGrade가 가진 상수중에 하나를 사용.
public class ClassGradeEx1 {
    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);
    }
}

private 생성자

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

        DiscountService discountService = new DiscountService();
        //private으로 생성 X 제한
        //ClassGrade classGrade = new ClassGrade();
        //int rst = discountService.discount(classGrade, price);
       // System.out.println("rst = " + rst);
    }
}
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를 임의로 생성 X
private 생성자 덕분에 ClassGrade 인스턴스를 생성하는 것은 ClassGrade 클래스 내부에서만 할 수 있음. 앞서 우리가 정의한 상수들은 ClassGrade 클래스 내부에서 ClassGrade 객체를 생성

ClassGrade 인스턴스를 사용할 때는 ClassGrade 내부에 정의한 상수를 사용해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.

타입 안전 열거형 패턴

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

  • 데이터 일관성 : 정해진 객체만 사용, 데이터 일관성이 보장

  • 제한된 인스턴스 : 클래스는 사전에 정의된 몇개의 인스턴스만 생성, 외부에서는 이 인스턴스들만 사용할 수 있도록 함

  • 타입 안정성 : 이 패턴을 사용, 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지 가능

열거형 EnumType

public enum Grade {
    BASIC, GOLD, DIAMOND
}

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

        System.out.println("BASIC = " + refVal(Grade.BASIC));
        System.out.println("BASIC = " + refVal(Grade.GOLD));
        System.out.println("BASIC = " + refVal(Grade.DIAMOND));
    }

    public static String refVal(Grade grade){
        return Integer.toHexString(System.identityHashCode(grade));
    }
}
  • 실행된 결과를 보면 상수들이 열거형으로 선언한 타입인 Grade 타입을 사용하는 것을 확인
    각각의 인스턴스들도 서로 다른 것을 확인

  • 참고로 열거형은 toString() 을 재정의 하기 때문에, 참조값을 직접 확인 X -> 참조값 구하기 위해ㅑ refValue를 만들었음

System.identityHashCode(grade): 자바가 관리하는 객체의 참조값을 숫자로 반환한다.

Integer.toHexString(): 숫자를 16진수로 변환, 우리가 일반적으로 확인하는 참조값은 16진수

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

열거형은 외부 생성 불가

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

        DiscountService discountService = new DiscountService();
        //private으로 생성 X 제한
        //ClassGrade classGrade = new ClassGrade();
        //int rst = discountService.discount(classGrade, price);
       // System.out.println("rst = " + rst);
    }
}

타입 안정성 향상: 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성이 없다. 이런 경우 컴파일 오류가 발생한다.

간결성 및 일관성: 열거형을 사용하면 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.

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

열거형 - 주요 메서드

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

        //모든 Enum 변환
        Grade[] grades = Grade.values();
        System.out.println("grades = " + Arrays.toString(grades));
        for (Grade grade : grades) {
            System.out.println("name= " + grade.name() + " ordinal= " + grade.ordinal());
        }

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

Arrays.toString 배열의 참조값이 아니라 배열의 내부 값을 출력할때 사용

ENUM - 주요 메서드
values() : 모든 ENUM 상수를 포함하는 배열을 반환한다.

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

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

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

toString(): ENUM 상수의 이름을 문자열로 반환한다. name() 메서드와 유사하지만, toString()은 직접 오버라이드 할 수 있다.

oridianl은 가급적 사용 X
이 값을 사용하다가 중간에 상수 위치 변경시 전체 상수의 위치가 변경

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

열거형 - 리펙토링

불필요한 if문 제거. disocuntPercent는 각각 회원 등급별로 판단. 할인율은 결구 회원 등급에 따라 간다.

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

ClassGrade에 할인률 disocuntPercent만 필드 추가. 조회 메서드도 추가
생성자를 통해서만 disocuntPercent를 설정. 값이 변하지 않도록 불변으로 설계

public class DiscountService {

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

기존에 있던 if문이 완전히 제거, 단순한 할인율 계산 로직만 남았다.
기존 if문을 통해서 회원의 등급을 찾고, 각 등급별로 discountPercent의 값 지정
if문 사용 이유 X

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

열거형 리펙토링 2

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

    private final int discountPercent;

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

    public int getDiscountPercent(){
        return discountPercent;
    }
}

discountPercnet 필드를 추가, 생성자를 통해서 필드에 값 지정
열거형은 상수로 지정하는 것 외 일반적인 방법으로 생성 불가능 -> 접근제어자를 선언할 수 X

public class DiscountService {
    public int discount(Grade grade, int price){
        return price * grade.getDiscountPercent() / 100;
    }
}
public class EnumRefMain {
    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);
    }
}

열거형 - 리펙토링 3

Grade 클래스 안으로 discount 메서드 이동

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 * discountPercent / 100;
    }
}
public class DiscountService {
    public int discount(Grade grade, int price){
        return grade.discount(price);
    }
}

할인율 계산은 이제 Grade가 스스로 처리, 따라서 DiscountService.discount 메서드는 단순히 Grade.disocunt를 호출

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

    public static void printDiscount(Grade grade, int price){
        System.out.println(grade.name() + " " + grade.discount(price));
    }
}

DiscountService 더이상 필요 X
중복 부분 또한 제거

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

    public static void printDiscount(Grade grade, int price){
        System.out.println(grade.name() + " " + grade.discount(price));
    }
}
profile
개발자를 향해 가는 중입니다~! 항상 겸손

0개의 댓글