'김영한의 실전 자바 - 중급 1편' 강의를 들으면서 복습할만한 내용을 정리하였다.

5. 열거형 - ENUM

5.1 문자열과 타입 안전성

비즈니스 요구사항

고객은 3등급으로 나누고, 상품 구매시 등급별로 할인을 적용한다.

  • BASIC -> 10% 할인

  • GOLD -> 20% 할인

  • DIAMOND -> 30% 할인

회원 등급과 가격을 입력하면 할인 금액을 계산해주는 클래스를 만들어보자.

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

위의 discount 메서드는 문자열을 입력받아 각각의 회원 등급에 맞는 할인을 적용한다.

지금과 같이 단순히 문자열을 입력하는 방식은, 오타가 발생하기 쉽고, 유효하지 않는 값이 입력될 수 있다.

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

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

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

  • 값의 제한 부족 : String 으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다. (예 오타)

  • 컴파일 시 오류 감지 불가 : 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다.

문자열 상수 사용

public class StringGrade {
	public static final String BASIC = "BASIC";
    public static final String GOLD = "GOLD";
 	public static final String DIAMOND = "DIAMOND";
}

상수는 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 직접 사용하는 것 보다는 더 안전하다.

이제 discount 메서드에 회원 등급을 보낼때 StringGrade.BASIC 처럼 상수로 보내면 된다.

하지만 문자열 상수를 사용해도, 지금까지 발생한 문제들을 근본적으로 해결할 수는 없다. 왜냐하면 String 타입은 어떤 문자열이든 입력할 수 있기 때문이다. 즉 StringGrade 에 정의된 문자열 상수를 사용하지 않고 직접 문자열을 사용해도 막을 수 있는 방법은 없다.

5.2 타입 안전 열거형 패턴

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

타입 안전 열거형 패턴을 사용하여 회원 등급 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 이므로 애플리케이션 로딩 시점에 ClassGrade 인스턴스가 3개 생성되고, 각각의 상수는 같은 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;
}

private 생성자

그런데 이 방식은 외부에서 임의로 ClassGrade 의 인스턴스를 생성할 수 있다는 문제가 있다.

ClassGrade newClassGrade = new ClassGrade(); // 생성자 private을 막아야 함
discount(newClassGrade, price);
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 를 임의로 생성하지 못하게 막았다.

  • private 생성자 덕분에 ClassGrade 의 인스턴스를 생성하는 것은 ClassGrade 클래스 내부에서만 할 수 있다. 앞서 우리가 정의한 상수들은 ClassGrade 클래스 내부에서 ClassGrade 객체를 생성한다.

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

  • 쉽게 이야기해서 ClassGrade 타입에 값을 전달할 때는 우리가 앞서 열거한 BASIC, GOLD, DIAMOND 만 사용할 수 있다.

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

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

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

  • 제한된 인스턴스 생성 : 클래스 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용할 수 있도록 한다. 이를 통해 미리 정의된 값들만 사용하도록 보장한다.

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

단점

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

5.3 열거형 - Enum Type

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

public enum Grade {
	BASIC, GOLD, DIAMOND
}
  • 열거형을 정의할 때는 class 대신에 enum 을 사용한다.
  • 원하는 상수의 이름을 나열하면 된다.
  • 열거형도 클래스이다.
  • 열거형은 자동으로 java.lang.Enum을 상속 받는다.
  • 외부에서 임의로 생성할 수 없다.
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;
}

열거형(ENUM)의 장점

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

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

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

5.4 열거형 - 주요 메서드

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

//모든 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 상수의 이름을 문자열로 반환한다.

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

5.5 열거형 - 리팩토링

discount 메서드에서 할인율(discountPercent)은 각각의 회원 등급별로 판단된다. 할인율은 결국 회원 등급을 따라간다. 따라서 회원 등급 클래스가 할인율을 가지고 관리하도록 변경하자.

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

이제 단순히 할인율 계산만 남았다.

객체 지향 관점에서 이렇게 Grade 의 데이터인 discountPercent를 외부에 노출하는 것보다는, Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.

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;
    }
}
profile
가오리의 개발 이야기

0개의 댓글