자바가 제공하는 열거형(Enum Type) 을 제대로 이해하려면 먼저 열거형이 생겨난 이유를 알아야 한다. 예제를 순서대로 따라가며 열거형이 만들어진 근본적인 이유를 알아보자.
비즈니스 요구사항
고객은 3등급으로 나누고, 상품 구매시 등급별로 할인을 적용한다. 할인시 소수점 이하는 버린다.
BASIC -> 10%
GOLD -> 20%
DIAMOND -> 30%
할인율을 계산하는 DiscountService 를 만들고, 해당 클래스를 사용해서 할인율을 출력해보자.
package enummeration.ex0;
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;
}
}
package enummeration.ex0;
public class StringGradeEx0_1 {
public static void main(String[] args) {
int price = 10_000;
DiscountService discountService = new DiscountService();
int basic = discountService.discount("BASIC", price);
int gold = discountService.discount("GOLD", price);
int diamond = discountService.discount("DIAMOND", price);
System.out.println("basic = " + basic);
System.out.println("gold = " + gold);
System.out.println("diamond = " + diamond);
}
}
위 예제는 다음과 같은 문제가 있다.
타입 안정성 부족 : 문자열은 오타가 발생하기 쉽고, 유효하지 않은 값이 입력될 수 있다.
데이터 일관성 : "GOLD", "gold", "Gold" 등 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다.
String 사용 시, 타입 안정성 부족 문제
값의 제한 부족 : String 으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다.
컴파일 시 오류 탐지 불가 : 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에, 디버깅이 어려워 질 수 있다.
이러한 문제를 해결하려면 특정 범위로 값을 제한해야 한다. 예를 들어 BASIC, GOLD, DIAMOND 라는 정확한 문자만 discount() 메서드에 전달되어야 한다. 하지만 String 은 어떤 문자열이든 받을 수 있기 때문에, 자바 문법 관점에서는 아무런 문제가 없다.. 결국 String 타입을 사용해서는 문제를 해결할 수 없다..
문자열과 타입 안정성2
이번에는 대안으로 문자열 상수를 사용해보자. 상수는 미리 정해진 변수명을 사용할 수 있기 때문에, 문자열을 직접 사용하는 것 보다는 더 안전하다.
package enummeration.ex1;
public class StringGrade {
public static final String BASIC = "BASIC";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}
package enummeration.ex1;
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;
}
}
package enummeration.ex1;
public class StringGradeEx1_1 {
public static void main(String[] args) {
int price = 10_000;
DiscountService discountService = new DiscountService();
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);
}
}
문자열 상수를 사용한 덕에 전체적으로 코드가 더 명확해졌다. 그리고 discount() 에 인자를 전달할 때도 StringGrade 가 제공하는 문자열 상수를 사용하면된다. 더 좋은 점은 만약 실수로 상수의 이름을 잘못 입력하면 컴파일 시점에 오류가 발생한다는 점이다. 따라서 오류를 쉽고 빠르게 찾을 수 있다.
하지만 문자열 상수를 사용해도, 지금까지 발생한 문제들을 근본적으로 해결할 수 없다. 왜냐면 String 타입은 어떤 문자열이든 입력할 수 있기 때문이다. 어떤 개발자가 실수로 StringGrade 에 있는 문자열 상수를 사용하지 않고, 다른 문자열을 입력해도 막을 방법이 없다.
결국 누군가 주석을 잘 남겨두어서, StringGrade 에 있는 상수를 사용해달라고 해야 한다. 물론 이렇게 해도 누군가는 주석을 깜빡하고 문자열을 직접 입력할 수 있다.
타입 안전 열거형 패턴
타입 안전 열거형 패턴 - Type-Safe Enum Pattern
지금까지 설명한 문제를 해결하기 위해서 많은 개발자들이 오랜기간 고민하고 나온 결과가 바로 타입 안전 열거형 패턴이다. 여기서 중요한 핵심은 타입 안전 열거형 패턴을 사용하면 이렇게 나열한 항목만 사용할 수 있다는 것이 핵심이다. 나열한 항목이 아닌 것은 사용할 수 없다.
직접 구현해보자.
private 생성자를 통해서 외부에서 ClassGrade 인스턴스를 생성하지 못하게 막았다.
private 생성자 덕분에 내부에서만 인스턴스를 생서할 수 있다.
쉽게 이야기해서 ClassGrade 타입에 값을 전달할 때는 우리가 앞서 열거한 BASIC, GOLD, DIAMOND 상수만 사용할 수 있다.
package enummeration.ex2;
public class ClassGrade {
public static final ClassGrade BASIC = new ClassGrade(); //x001
public static final ClassGrade GOLD = new ClassGrade(); //x002
public static final ClassGrade DIAMOND = new ClassGrade(); //x003
private ClassGrade() {
}
}
이 코드를 활용해서 이전 코드를 실행해보자.
package enummeration.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;
}
}
실행 결과를 살펴보면 상수들이 열거형으로 선언한 타입인 Grade 타입을 사용하는 것을 확인할 수 있다. 그리고 각각의 인스턴스도 서로 다른 것을 확인할 수 있다.
열거형도 클래스이다. 열거형을 제공하기 위해서 제약이 추가된 클래스라 생각하면 된다.
enum 은 열거형 내부에서 상수로 지정하는 것 외에 직접 생성이 불가능하다. 생성할 경우 컴파일 오류가 발생한다.
자바의 열거형을 사용해서 코드를 작성해보자.
package enummeration.ex3;
import static enummeration.ex3.Grade.*;
public class DiscountService {
public int discount(Grade grade, int price) {
int discountPercent = 0;
if (grade == BASIC) {
discountPercent = 10;
} else if (grade == GOLD) {
discountPercent = 20;
} else if (grade == DIAMOND) {
discountPercent = 30;
} else {
System.out.println("할인X");
}
return price * discountPercent / 100;
}
}
package enummeration.ex3;
import static enummeration.ex3.Grade.*;
public class ClassGradeEx3_1 {
public static void main(String[] args) {
int price = 10_000;
DiscountService discountService = new DiscountService();
int basic = discountService.discount(BASIC, price);
int gold = discountService.discount(GOLD, price);
int diamond = discountService.discount(DIAMOND, price);
System.out.println("basic = " + basic);
System.out.println("gold = " + gold);
System.out.println("diamond = " + diamond);
}
}
열거형(ENUM) 의 장점
타입 안정성 향상 : 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성은 없다. 이런 경우 컴파일 오류가 발생한다.
간결성 및 일관성 : 열거형을 사용하면 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.
확장성 : 새로운 회원 등급을 타입을 추가하고 싶을 때, ENUM 에 새로운 상수를 추가하기만 하면 된다.
열거형 - 리팩토링1
지금까지 구현한 코드들을 더 읽기 쉽게 리펙토링해보자.
enum 이전에 클래스를 직접 사용해서 열거형 패턴을 구현했던 ex2 코드를 먼저 리펙토링하자.
ClassGrade 에서 값을 갖도록하면 DiscountService 코드가 획기적으로 줄어든다.
package enummeration.ref1;
public class ClassGrade {
public static final ClassGrade BASIC = new ClassGrade(10); //x001
public static final ClassGrade GOLD = new ClassGrade(20); //x002
public static final ClassGrade DIAMOND = new ClassGrade(30); //x003
private final int discountPercent;
private ClassGrade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
}
package enummeration.ref1;
public class DiscountService {
public int discount(ClassGrade classGrade, int price) {
return price * classGrade.getDiscountPercent() / 100;
}
}
package enummeration.ref1;
public class ClassGradeRefMain1 {
public static void main(String[] args) {
int price = 10_000;
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);
}
}
열거형 - 리팩토링2
이제 열거형도 사용해보자.
타입 안전 열거형 클래스와 마찬가지로 enum 에서 값을 가지게 하면 된다.
package enummeration.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;
}
}
package enummeration.ref2;
public class DiscountService {
public int discount(Grade grade, int price) {
return price * grade.getDiscountPercent() / 100;
}
}
package enummeration.ref2;
import static enummeration.ref2.Grade.*;
public class EnumRefMain2 {
public static void main(String[] args) {
int price = 10_000;
DiscountService discountService = new DiscountService();
int basic = discountService.discount(BASIC, price);
int gold = discountService.discount(GOLD, price);
int diamond = discountService.discount(DIAMOND, price);
System.out.println("basic = " + basic);
System.out.println("gold = " + gold);
System.out.println("diamond = " + diamond);
}
}