Programming Language/Java

[Java] 21. 열거형 - ENUM

lumana 2024. 11. 8. 18:53

 

 

열거형 - ENUM

#Java


열거형이 생겨난 이유

문자열로 처리

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

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

만약 if문을 통해서 구현한다면

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

이런식으로 구현할 것이다. 하지만 이 경우 파라미터로 값을 넘겨줄 때 오타가 있거나 실수를 해서 유효하지 않은 값을 넣어버리면 할인이 제대로 적용되지 않는다.


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

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

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

  • 값의 제한 부족: 오타나 잘못된 값이 입력될 위험이 있다.
  • 컴파일 시 오류 감지 불가 : 컴파일 시 감지되지 않고, 런타임에서만 문제가 발견되어 디버깅이 어려워진다.

이런 문제를 해결하려면 특정 범위로 값을 제한해야 한다. 하지만 String 타입에 어떤 문자열을 받는 문법상으로 문제가 없다. 결국 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 + ": 할인X"); 
	} 
	return price * discountPercent / 100;
}

코드가 훨씬 명확해지고, 인자를 전달할 때에도 StringGrade가 제공하는 문자열 상수를 사용하면 된다.
만약 실수로 이름을 잘못입력하면 컴파일 시점에 오류가 발생한다.


하지만, 이 방법에도 한계가 있다. 인자를 넘겨줄 때 StringGrade에 존재하는 문자열 상수를 사용하도록 강제할 수가 없다.


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

지금까지 설명한 문제를 해결하기 위해 많은 개발자들이 오랜기간 고민하고 나온 결과가 바로 타입 안전 열거형 패턴이다. 타입 안전 열거형 패턴을 사용하면 이렇게 나열한 항목만 사용할 수 있다. 나열한 항목이 아닌 것은 사용할 수 없다. 위 예시에서는 어떤 String처럼 아무런 문자열 다 사용할 수 있는게 아니라, 우리가 나열한 항목만 안전하게 사용할 수 있다.


타입 안전 열거형 패턴을 직접 구현해보자.

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

  • 먼저 회원 등급을 다루는 클래스를 만들고, 각각의 회원 등급별로 상수를 선언한다.
  • 이때 각각의 상수마다 별도의 인스턴스를 생성하고, 생성한 인스턴스를 대입한다.
  • 각각을 상수로 선언하기 위해 static , final 을 사용한다.
    • 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);
	} 
} 

  • 각각의 상수는 모두 ClassGrade 타입을 기반으로 인스턴스를 만들었기 때문에 getClass() 의 결과는 모두 ClassGrade 이다.
  • 각각의 상수는 모두 서로 각각 다른 ClassGrade 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.

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

  • ClassGrade BASIC : x001
  • ClassGrade GOLD : x002
  • ClassGrade DIAMOND : x003

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


  • 참조값 비교를 해주면 된다. 등급별로 딱 하나의 인스턴스가 있기 때문이다.
  • discount() 를 호출할 때 미리 정의한 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); 
	} 
} 


Private 생성자

이 방법도 완전히 안전한 방법은 아니다. ClassGrade의 인스턴스를 생성할 수 있어서, 매개변수에 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() {}
 }

이렇게 함으로써 ClassGrade 타입에 값을 전달할 때는 우리가 앞서 열거한 BASIC , GOLD , DIAMOND 상수만 사용할 수 있다.


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

  • 타입 안정성 향상: 정해진 객체만 사용할 수 있기 때문에, 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있다.
  • 데이터 일관성: 정해진 객체만 사용하므로 데이터의 일관성이 보장된다.

조금 더 자세히

  • 제한된 인스턴스 생성: 클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용 할 수 있도록 한다. 이를 통해 미리 정의된 값들만 사용하도록 보장한다.
  • 타입 안전성: 이 패턴을 사용하면, 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지할 수 있다. 예를 들어, 특정 메서드가 특정 열거형 타입의 값을 요구한다면, 오직 그 타입의 인스턴스만 전달할 수 있다. 여기서는 메 서드의 매개변수로 ClassGrade 를 사용하는 경우, 앞서 열거한 BASIC , GOLD , DIAMOND 만 사용할 수 있다.

간단히 말하자면
이미 만들어 놓은거만 사용할 수 있고, 그 외의 경우는 컴파일 에러로 막을 수 있다.


그런데, 매 번 이렇게 구현하려면 코드도 길어지고, 또 이 코드를 작성하다가 실수를 할 수도 있는 것이다.


열거형 - Enum Type

자바에서는 타입 안전 열거형 패턴을 쉽게 사용할 수 있도록 Enum Type을 지원해준다.
자바의 enum은 타입 안전성을 제공하고, 코드의 가독성을 높이며, 예상 가능한 값들의 집합을 표현하는 데 사용된다.


package enumeration.ex3;
public enum Grade {
     BASIC, GOLD, DIAMOND
} 

  • 문법
    • class 대신 enum을 사용하고
    • 원하는 상수 이름을 단순히 나열하면 된다.

자바의 열거형으로 작성한 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을 상속 받는다.
  • 외부에서 임의로 생성할 수 없다.


직접 코드로 확인해보면

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(Object grade) {
         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 = x001
ref GOLD = x002
ref DIAMOND = x003

같은 Grade 타입이고, 인스턴스도 각각 다른 것을 확인할 수 있다. 열거형도 클래스이다.


참고로 열거형은 toString() 을 재정의 하기 때문에 참조값을 직접 확인할 수 없다. 참조값을 구하기 위해 refValue() 를 만들었다.


열거형을 이용해서 discount 메서드를 작성하면

package enumeration.ex3;
 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;
     }
} 

타입 안전 열거형 패턴으로 직접 만든 코드와 거의 유사하다.
또한 열거형은 switch 문에 사용할 수 있는 장점도 있다. 위 코드 또한 switch-case로 개선이 가능하다.


타입 안전 열거형 패턴과 마찬가지로 enum 은 열거형 내부에서 상수로 지정하는 것 외에 직접 생성이 불가능하다. 생성할 경우 컴파일 오류가 발생한다.


열거형(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 변환, 잘못된 문자면 IllegalArgumentException 발생 
	String input = "GOLD";
	Grade gold = Grade.valueOf(input);
	System.out.println("gold = " + gold); //toString() 오버라이딩 가능 
	} 
} 


실행 결과

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 의 값이 하나씩 추가된다.

열거형 정리

  • 열거형은 java.lang.Enum 를 자동(강제)으로 상속 받는다.
  • 열거형은 이미 java.lang.Enum 을 상속 받았기 때문에 추가로 다른 클래스를 상속을 받을 수 없다.
    • 자바는 다중 상속이 안되니까
  • 열거형은 인터페이스를 구현할 수 있다.
  • 열거형에 추상 메서드를 선언하고, 구현할 수 있다.
    • 이 경우 익명 클래스와 같은 방식을 사용한다. 익명 클래스는 뒤에서 다룬다.

열거형 - 리팩토링 1

일단 클래스를 이용해 열거형 패턴을 구현했던 것을 이용해서 리팩토링 해보자

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

if문을 사용하지 않고 객체지향적으로 설계해보자. BASIC, GOLD… 등의 등급 객체 각각이 discountPercent라는 필드를 가지고 있으면 객체지향적으로 처리가 가능하다.


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

  • ClassGrade 에 할인율( discountPercent ) 필드를 추가했다. 조회 메서드도 추가한다. 값 설정은 초기화 시점 이후 불가능하다.
  • 상수를 정의할 때 각각의 등급에 따른 할인율이 정해진다.

package enumeration.ref1;
public class DiscountService {
    public int discount(ClassGrade classGrade, int price) {
        return price * classGrade.getDiscountPercent() / 100;
	} 
} 

기존에 있던 if문을 다 지울 수 있다.


열거형 - 리팩토링 2

열거형을 직접 사용해보자. 열거형도 클래스이므로, 동일하게 적용해보자.


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

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

열거형 - 리팩토링 3

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


여기서 grade의 discountpercent를 게터를 통해서 꺼내서 사용하고 있다. 이는 객체지향적으로 객체의 데이터를 직접 노출한다는 점에서 좋지 않다. Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.


 package enumeration.ref3;
 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;
     }
} 

  • 이제 할인율을 스스로 계산할 수 있다.

package enumeration.ref3;
public class DiscountService {
    public int discount(Grade grade, int price) {
        return grade.discount(price);
	} 
} 

discount 메서드 역할을 객체가 스스로 하므로, DiscountService라는 클래스는 더 이상 필요 없다.


중복 제거

출력하는 부분에서 공통된 부분을 하나의 메서드로 뽑아내자.

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

ENUM 목록

새로운 등급이 추가되더라도 main() 코드의 변경 없이 모든 등급의 할인을 출력해보자.


ENUM의 상수도 결국 객체이기 때문에 배열에 담을 수 있다.


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

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

Ref) 김영한의 실전 자바 - 중급 1편