
'김영한의 실전 자바 - 중급 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 으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다. (예 오타)
컴파일 시 오류 감지 불가 : 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다.
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 에 정의된 문자열 상수를 사용하지 않고 직접 문자열을 사용해도 막을 수 있는 방법은 없다.
타입 안전 열거형 패턴 - 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 생성자를 추가하는 등 유의해야 하는 부분들도 있다.
자바는 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형(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에 새로운 상수를 추가하지만 하면 된다.
모든 열거형은 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() 은 직접 오버라이드 할 수 있다.
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;
}
}