[김영한의 실전 자바 - 중급 1편] 05. 열거형 - ENUM

Turtle·2024년 7월 6일
0
post-thumbnail

🙄문자열과 타입 안전성1

  • ✔️비즈니스 요구사항
    • 고객을 BASIC, GOLD, DIAMOND의 3등급으로 나누고, 상품 구매시 등급별로 할인을 적용한다. 이 때, 할인시 소수점 이하는 버린다.
    • BASIC : 10% 할인
    • GOLD : 20% 할인
    • DIAMOND : 30% 할인
public class DiscountServiceV0 {
	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;
		DiscountServiceV0 discountService = new DiscountServiceV0();
		// 존재하지 않는 등급
		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);
	}
}
  • ✔️문제
    • 타입 안전성 부족 : 문자열은 오타가 발생하기 쉽고, 유효하지 않은 값이 입력될 수 있다.
    • 데이터 일관성 : 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다.
    • 값의 제한 부족 : String으로 상태나 카테고리를 표현하면 잘못된 문자열을 실수로 입력할 가능성이 있다.
    • 컴파일 시 오류 감지 불가 : 잘못된 값은 컴파일 시에는 감지되지 않고 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다.

🙄문자열과 타입 안전성2

public class StringGrade {
	public static final String BASIC = "BASIC";
	public static final String GOLD = "GOLD";
	public static final String DIAMOND = "DIAMOND";
}
public class DiscountServiceV1 {
	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;
	}
}
public class StringGradeEx1_1 {
	public static void main(String[] args) {
		int price = 10000;
		DiscountServiceV1 discountService = new DiscountServiceV1();
		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);
	}
}
  • ✔️문제
    • 문자열 상수를 사용해서 코드가 더 명확해졌지만 이 문자열 상수를 사용해도 지금까지 발생한 문제들을 근본적으로 해결할 수 없다. String타입은 어떤 문자열이든 입력할 수 있기 때문이다.
    • 어떤 개발자가 실수로 StringGrade에 있는 문자열 상수를 사용하지 않고, 다음과 같이 직접 문자열을 사용해도 막을 수 있는 방법이 없다.

🙄타입 안전 열거형 패턴

  • ✔️타입 안전 열거형 패턴 - Type-Safe Enum Pattern
    • 나열한 항목만 사용할 수 있다.
public class ClassGrade {
	private static final ClassGrade BASIC = new ClassGrade();
	private static final ClassGrade GOLD = new ClassGrade();
	private static final ClassGrade DIAMOND = new ClassGrade();
}
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 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.
static final이므로 상수이며 static이므로 애플리케이션 로딩 시점에 3개의 ClassGrade 인스턴스가 생성되고, 각각의 상수는 같은 ClassGrade 타입의 서로 다른 인스턴스 참조값을 가진다.

public class DiscountServiceV2 {
	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;
		DiscountServiceV2 discountService = new DiscountServiceV2();
		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);
	}
}
  • ✔️정리1
    • discount() 메서드는 매개변수로 ClassGrade 클래스를 사용한다.
    • 값을 비교할 때는 classGrade == ClassGrade.BASIC와 같이 == 참조값 비교를 사용한다.
public class ClassGradeEx2_2 {
	public static void main(String[] args) {
		int price = 10000;
		DiscountServiceV2 discountService = new DiscountServiceV2();
		// ClassGrade newClassGrade = new ClassGrade();	// 외부에서 임의로 인스턴스 생성할 수 있다는 문제가 발생
		// int result = discountService.discount(newClassGrade, price);
		// System.out.println("newClassGrade 등급의 할인 금액: " + result);
	}
}
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() {

	}
}
  • ✔️정리2
    • 외부에서 임의로 ClassGrade 인스턴스를 생성할 수 있다는 문제점이 있다.
    • 외부에서 생성할 수 없도록 생성자를 private으로 제한하면 된다.
  • ✔️타입 안전 열거형 패턴(Type-Safe Enum Pattern)의 장점
    • 타입 안전성 향상 : 정해진 객체만 사용할 수 있기 때문에 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있다.
    • 데이터 일관성 : 정해진 객체만 사용하므로 데이터 일관성이 보장된다.
  • ✔️작성한 코드의 단점
    • 이 패턴을 구현하려면 많은 코드를 작성해야 한다.
    • private 생성자를 추가하는 등 유의해야 하는 부분들도 존재한다.

🙄열거형 - Enum Type

자바는 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형을 제공한다.
Enumeration은 일련의 명명된 상수들의 집합을 정의하는 것을 의미하며, 프로그래밍에서는 이러한 상수들을 사용하여 코드 내에서 미리 정의된 값들의 집합을 나타낸다.

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

🙄열거형 - 주요 메서드

public class EnumMethodMain {
	public static void main(String[] args) {
		Grade[] grades = Grade.values();
		for (Grade grade : grades) {
			System.out.println("grade = " + grade);
		}

		String input = "GOLD";
		Grade grade = Grade.valueOf(input);
		System.out.println("grade = " + grade);

		System.out.println(Grade.BASIC.ordinal());
		System.out.println(Grade.GOLD.ordinal());
		System.out.println(Grade.DIAMOND.ordinal());
	}
}
  • ✔️열거형 - 주요 메서드
    • values() : 모든 ENUM 상수를 포함하는 배열을 반환한다.
    • valueOf(String name) : 주어진 이름과 일치하는 ENUM 상수를 반환한다.
    • name() : ENUM 상수의 이름을 문자열로 반환한다.
    • ordinal() : ENUM 상수의 선언 순서(0부터 시작)를 반환한다.
    • toString() : ENUM 상수의 이름을 문자열로 반환한다. name() 메서드와 유사하지만, toString() 은 직접 오버라이드 할 수 있다
  • ✔️ordinal()
    • 해당 메서드는 사용하지 않는 것이 좋다. 왜냐하면 이 값을 사용하다가 중간에 상수를 선언하는 위치가 변경되면 전체 상수 위치가 모두 변경될 수 있기 때문이다.
    • 기존의 상수 위치를 보면 BASIC(0), GOLD(1), DIAMOND(2)인데 만약 값이 1인 위치에 SILVER가 추가되면 데이터베이스나 파일에 있는 값은 그대로 1로 유지가 되지만 애플리케이션에서 갑자기 GOLD(2)로 변경이 되고 SILVER(1)가 된다. 이렇게 되면 기존 데이터와의 일관성이 보장되지 않아 큰 버그가 발생할 수 있다.

🙄열거형 - 리팩토링 결과

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

	private final int discountPercent;

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

	public int discount(int price) {
		return price * discountPercent / 100;
	}
}
public class EnumRefMain1 {
	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));
	}
}
public class EnumRefMain2 {
	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 내부에서 discount() 메서드를 만들어 할인율을 스스로 계산한다.
    • 열거형 타입 Grade에 새로운 등급이 추가되더라도 코드 변경없이 모든 등급의 할인을 출력할 수 있다.

0개의 댓글