[Java] 자바 중급(5) 열거형ENUM

wony·2024년 3월 30일

Java

목록 보기
12/30

0.개요

주제 : 김영한님의 자바 중급 1편 총 정리
내용 : 열거형 ENUM에 대해 공부

1) 문자열과 타입 안정성1

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

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

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

ex) GOLD 유저가 10000원을 구매하면 할인 대상 금액은 2000원이다.

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;
    }
}
  • 지금과 같이 단순히 문자열을 입력하는 방식은 오타가 발생하기 쉽고, 유효하지 않는 값이 입력될 수 있다.
public class StringGradeEx0_2 {
    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 diamondd = discountService.discount("DIAMONDD", price);
		System.out.println("DIAMONDD 등급의 할인 금액: " + diamondd);
        // 소문자 입력
        int gold = discountService.discount("gold", price);
        System.out.println("gold 등급의 할인 금액: " + gold);
    }
}
  • 등급에 문자열을 사용하는 지금의 방식은 다음과 같은 문제가 있다.
    • 타입 안정성 부족 : 문자열은 오타가 발생하기 쉽고, 유효하지 않은 값이 입력될 수 있다.
    • 데이터 일관성 : GOLD, gold, Gold 등 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다.

String 사용 시 타입 안정성 부족 문제

  • 값의 제한 부족 : String으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다. 예를들어, Monday, Tuesday 등을 나타내는 데 String 을 사용한다면, 오타(Munday)나 잘못된 값 (Funday)이 입력될 위험이 있다.
  • 컴파일 시 오류 감지 불가: 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다.

이런 문제를 해결하려면 특정 범위로 값을 제한해야 한다. 예를 들어 BASIC , GOLD , DIAMOND 라는 정확한 문자만 discount() 메서드에 전달되어야 한다. 하지만 String 은 어떤 문자열이든 받을 수 있기 때문에 자바 문법 관점에 서는 아무런 문제가 없다. 결국 String 타입을 사용해서는 문제를 해결할 수 없다.

2) 문자열과 타입 안전성2

  • 대안으로 문자열 상수를 사용해보자. 상수는 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 직접 사용하는 것 보다는 더 안전하다.
public class ClassGrade {
	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;
    }
}
  • 문자열 상수를 사용한 덕분에 전체적으로 코드가 더 명확해졌다. 그리고 discount()에 인자를 전달할 때도 StringGrade가 제공하는 문자열 상수를 사용하면 된다. 좋은 점은 만약 실수로 상수의 이름을 잘못 입력하면 컴파일 시점에 오류가 발생한다는 점이다.
  • 하지만, 문자열 상수를 사용해도, 지금까지 발생한 문제들을 근본적으로 해결할 수는 없다. 왜냐하면 String 타입은 어떤 문자열이든 입력할 수 있기 때문이다. 어떤 개발자가 실수로 StringGrade에 있는 문자열 상수를 사용하지 않고, 다음과 같이 직접 문자열을 사용해도 막을 수 있는 방법이 없다.
public class StringGradeEx1_2 {
    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 diamondd = discountService.discount("DIAMONDD", price);
        System.out.println("DIAMONDD 등급의 할인 금액: " + diamondd);

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

3) 타입 안전 열거형 패턴

  • 쉽게 이야기해서 앞서본 String처럼 아무런 문자열이나 다 사용할 수 있는 것이 아니라, 우리가 나열한 항목인 BASIC,GOLD,DIAMOND만 안전하게 사용할 수 있다는 것이다.
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 을 사용해서 인스턴스(참조값)를 변경할 수 없게 한다.
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);
	}
}
  • 각각의 상수는 모두 ClassGrade 타입을 기반으로 인스턴스를 만들었기 때문에 getClass() 의 결과는 모두 ClassGrade 이다.

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

  • static 이므로 애플리케이션 로딩 시점에 다음과 같이 3개 ClassGrade 인스턴스가 생성되고, 각각의 상수는 같은 ClassGrade 타입의 서로 다른 인스턴스의 참조값을 가진다.

    • ClassGrade BASIC : x001
    • ClassGrade GOLD : x002
    • ClassGrade DIAMOND : x003
  • 이제 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;
	}
}
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);
	}
}
  • 그런데 이 방식은 외부에서 임의로 ClassGrade 의 인스턴스를 생성할 수 있다는 문제가 있다.
  • 이 문제를 해결하려면 외부에서 ClassGrade 를 생성할 수 없도록 막으면 된다. 기본 생성자를 private 으로 변경하자.
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();
    }
}
  • 이렇게 private 생성자를 사용해서 외부에서 ClassGrade를 임의로 생성하지 못하게 막고 타입 안전 열거형 패턴을 완성할 수 있었다.
  • 하지만, 이 패턴을 구현하려면 다음과 같이 많은 코드를 작성해야 하며, private생성자를 추가하는 등 유의해야 한는 부분들이 많다.

1. 열거형 - Enum Type

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

  • Enumeration은 일련의 명명된 상수들의 집합을 정의하는 것을 의미하며, 프로그래밍에서는 이러한 상수들을 사용하여 코드 내에서 미리 정의된 값들의 집합을 나타낸다.
  • 쉽게 이야기해서 회원의 등급은 상수로 정의한다. 즉,BASIC,GOLD,DIAMOND만 사용할 수 있다는 뜻이다.
  • 자바의 enum 은 타입 안전성을 제공하고, 코드의 가독성을 높이며, 예상 가능한 값들의 집합을 표현하는 데 사용된다.
public enum Grade {
    BASIC, GOLD, DIAMOND
}
  • 열거형을 정의할 때는 class 대신에 enum을 사용한다.
  • 원하는 상수의 이름을 나열하면 된다.

앞서 직접 ClassGrade를 구현할 때와는 비교가 되지 않을 정도로 편리하다.
자바의 열거형으로 작성한 Grade는 다음 코드와 거의 같다.

public class Grade extends Enum {
	public static final Grade BASIC = new Grade();
	public static final Grade GOLD = new Grade();
	public static final Grade DIAMOND = new Grade();
	//private 생성자 추가
	private Grade( ) { }
}
  • 열거형도 클래스이다.
  • 열거형은 자동으로 java.lang.Enum을 상속 받는다.
  • 외부에서 임의로 생성할 수 없다.

자바의 열거형을 사용해서 코드를 작성해보자.

public class DiscountService {
    public int discount(Grade grade, int price) {
        int discountPercent = 0;
    	//enum switch 변경 가능
        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 EnumEx3_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

  • 열거형의 사용법이 앞서 타입 안전 열거형 패턴을 직접 구현한 코드와 같은 것을 확인할 수 있다.
  • 참고로 열거형은 switch 문에 사용할 수 있는 장점도 있다.

열거형은 외부 생성 불가

public class EnumEx3_2 {
    public static void main(String[] args) {
        int price = 10000;
        DiscountService discountService = new DiscountService();
		/*
        Grade myGrade = new Grade(); //enum 생성 불가
        double result = discountService.discount(myGrade, price);
        System.out.println("result price: " + result);
		*/
    }
}
  • enum은 열거형 내부에서 상수로 지정하는 것 외에 직접 생성이 불가능하다. 생성할 경우 컴파일 오류가 발생한다.
  • 오류 메시지 : enum classes may not be instantiated

열거형(ENUM)의 장점

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

열거형 - 리팩토링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;
    }
}
  • discountPercent 필드를 추가하고, 생성자를 통해서 필드에 값을 저장한다.
    열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하다. 따라서 생성자에 접근제어자를 선언할 수 없게 막혀있다. private 이라고 생각하면 된다.
  • BASIC(10) 과 같이 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출된다.
  • 값을 조회하기 위해 getDiscountPercent() 메서드를 추가했다. 열거형도 클래스이므로 메서드를 추가할 수 있다.
public class DiscountService {
    public int discount(Grade grade, int price) {
        return price * grade.getDiscountPercent() / 100;
    }
}
  • 기존에 있던 if 문이 완전히 제거되고, 단순한 할인율 계산 로직만 남았다.
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 class DiscountService {
	public int discount(Grade grade, int price) {
		return price * grade.getDiscountPercent() / 100;
	}
}
  • 이 코드를 보면 할인율 계산을 위해 Grade가 가지고 있는 데이터인 discountPercent의 값을 꺼내서 사용한다.
  • 결국 Grade 의 데이터인 discountPercent 를 할인율 계산에 사용한다.
  • 객체지향 관점에서 이렇게 자신의 데이터를 외부에 노출하는 것 보다는, Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.
  • 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;
    }
}
  • Grade 내부에 discount() 메서드를 만들어서, 이제 할인율을 스스로 계산한다.
public class DiscountService {
    public int discount(Grade grade, int price) {
        return grade.discount(price);
    }
}
  • 할인율 계산은 이제 Grade 가 스스로 처리한다. 따라서DiscountService.discount() 메서드는 단순히 Grade.discount() 를 호출하기만 하면 된다.
public class EnumRefMain3 {
    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

DiscountService 제거

Grade 가 스스로 할인율을 계산하면서 DiscountService 클래스가 더는 필요하지 않게 되었다.

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));
    }
}
  • 각각의 등급별로 자신의 discount() 를 직접 호출하면 할인율을 구할 수 있다.

중복 제거

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));
    }
}
  • grade.name() 을 통해서 ENUM의 상수 이름을 사용할 수 있다.

ENUM 목록

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

  • Grade.values() 를 사용하면 Grade 열거형의 모든 상수를 배열로 구할 수 있다.
profile
안녕하세요. wony입니다.

0개의 댓글