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;
}
}
discount메서드의 파라미터의 grade 문자열에 따라 할인금액을 반환하는 메서드이다.
public class StringGradeEx0_1 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
int basic = discountService.discount("BASIC",price);
int diamond = discountService.discount("DIAMOND",price);
System.out.println("정상 할인금액반환");
System.out.println("diamond = " + diamond);
System.out.println("basic = " + basic);
System.out.println("비정상 할인금액반환");
int vip = discountService.discount("VIP",price);
int diamondd = discountService.discount("DIAMONDD",price);
System.out.println("diamondd = " + diamondd);
System.out.println("vip = " + vip);
}
}
basic,diamond는 정상적으로 금액이 반환되었으나,
존재하지 않는 VIP등급이나, DIAMONDD처럼 문자의 오타 같은 경우, 정상적으로 할인금액을 반환하지 못하고 있다.
등급에 문자열을 사용하는 방식은 다음과 같은 문제가 있다.
이런 문제를 해결하려면 특정 범위로 값을 제한해야한다.
예를 들어, BASIC,GOLD,DIAMOND라는 정확한 문자만 discount()메서드에 전달되야한다.
해당 문제를 해결하기 위해서 문자열 상수를 사용해보자.
public class StringGrade {
public static final String BASIC = "BASIC";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}
상수는 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 직접 사용하는것보다 안전한다.
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;
}
}
여기서 equals의 파라미터에 미리 정의한 StringGrade의 상수를 사용하였다.
public class StringGradeEx0_2 {
public static void main(String[] args) {
int price = 10000;
lang.enumeration.ex0.DiscountService discountService = new DiscountService();
int basic = discountService.discount(StringGrade.BASIC,price);
int diamond = discountService.discount(StringGrade.DIAMOND,price);
System.out.println("정상 할인금액반환");
System.out.println("diamond = " + diamond);
System.out.println("basic = " + basic);
System.out.println("비정상 할인금액반환");
int vip = discountService.discount("VIP",price);
int diamondd = discountService.discount("DIAMONDD",price);
System.out.println("diamondd = " + diamondd);
System.out.println("vip = " + vip);
}
그런데 이러면 물론, 문자열 실수를 할 가능성은 줄어들지만 근본적인 문제가 해결되지 않는다.
왜냐하면 discount메서드의 파라미터가 결국 String을 받기 때문이다.
여기서 discount메서드에 주석으로 StringGrade를 사용하세요. 라고 적어 둘수 있지만, 주석을 못보고 지나 칠 수 도 있다.
애초에, discount메서드에는 StringGrade의 상수만 파라미터로 받을 수 있게 해야하는데 String으로 받아버리니까 어떤 문자열을 받을 수 있기 때문에 문제를 해결 할 수 없다.
앞에서 이러한 문제를 해결하기 위해서 타입 안전 열거형 패턴이 나왔다.
쉽게 말하면, 어떤 항목을 나열하고 해당 항목만 사용할 수 있다는것이다. 나열한 항목이 아니면 사용할 수 없다.
우리의 경우 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();
}
회원등급을 다루는 ClassGrade를 만들고, 각 회원 등급별로 상수를 선언한다.
해당 상수마다 별도로 static으로 미리 BASIC,GOLD,DIAMOND에 해당하는 인스턴스를 생성하고 대입한다.
public class ClassRefMain {
public static void main(String[] args) {
System.out.println("Basic: " + ClassGrade.BASIC);
System.out.println("Gold: " + ClassGrade.GOLD);
System.out.println("Diamond: " + ClassGrade.DIAMOND);
}
}
static이므로 애플리케이션 로딩 시점에 다음과 같이 3개의 각각 다른 인스턴스가 생성되고, 서로 다른 인스턴스 참조값을 가진다.
예를 들면 b4c966a가 x001과 같다.
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클래스를 사용한다.
classGrade로 넘어오는것은 참조값 x001이므로 if문의 ClassGrade.BASIC과 ==참조값을 비교한다.
public class StringGradeEx2_1 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
int basic = discountService.discount(ClassGrade.BASIC,price);
int diamond = discountService.discount(ClassGrade.DIAMOND,price);
System.out.println("정상 할인금액반환");
System.out.println("diamond = " + diamond);
System.out.println("basic = " + basic);
}
}
discount메서드 호출시 인수로 미리 정의해둔 상수를 넘겨주는데 이건 메서드 영역에 이미 인스턴스가 만들어져있다. 우리는 해당 참조값 x001을 가져다 쓰기만 하면된다.
그런데 여기서는 문제가 있는데
외부에서 해당 Class.Grade를 생성할 수 도 있다는것이다.
public class ClassGradeEx2_2 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
ClassGrade newClassGrade = new ClassGrade(); //생성자 private으로 막아야 함
int result = discountService.discount(newClassGrade, price);
System.out.println("newClassGrade 등급의 할인 금액: " + result);
}
}
왜냐하면 discount의 첫번쨰 파라미터가 ClassGrade type이므로
현재 새로만든 newClassGrade인스턴스가 ClassGrade type이 맞기 때문이다.
그래서 외부에서 임의로 ClassGrade를 생성할 수 없게 기본생성자를 private로 변경한다.
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 ClassGrade(){}
}
이렇게, private생성자를 사용해서 외부에서 ClassGrade를 임의로 생성하지 못하게 막았고, ClassGrade타입을 전달 할때는 우리가 앞서 열거한 BASIC,GOLD,DIAMOND상수만 사용할 수 있다.
타입 안전 열거형 패턴의 장점
제한된 인스턴스 생성
클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용 할 수 있도록 한다. 이를 통해 미리 정의된 값들만 사용하도록 보장한다.
타입 안전성이 패턴을 사용하면, 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지할 수 있다. 특정 메서드가 특정 열거형 타입의 값을 요구한다면, 오직 그 타입의 인스턴스만 전달할 수 있다.
여기서는 메서드 파라미터로 ClassGrade를 사용했으므로, 앞서 열거한 BASIC,GOLD,DIAMOND만 사용가능하다.
자바는 타입 안전 열거형 패턴을 위해 열거형 type을 지원한다.
public enum Grade {
BASIC,GOLD,DIAMOND
}
그냥 이렇게만쓰면, 이전에 했던것처럼 복잡한 public static final설정과 인스턴스 생성, private 생성자까지 전부 만들어준다.
그림과 같이 동일하게 메서드 영역에 인스턴스들이 생성된다.
Basic,Gold,Diamond에 참조값이 들어가 있는거다.
public class DiscountService {
public int discount(Grade classGrade, int price){
int discountPercent =0;
if(classGrade == Grade.BASIC){
discountPercent =10;
} else if (classGrade == Grade.GOLD) {
discountPercent = 20;
} else if (classGrade == Grade.DIAMOND) {
discountPercent = 30;
} else {
System.out.println("할인 X");
}
return price*discountPercent/100;
}
}
discount메서드의 파라미터에 이제는 Grade라는 이넘 타입으로 받는다.
Grade.BASIC에는 참조값 x001이 들어있고 if문에서 동일성을 판단한다.
public class StringGradeEx2_1 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
int basic = discountService.discount(Grade.BASIC,price);
int diamond = discountService.discount(Grade.DIAMOND,price);
System.out.println("정상 할인금액반환");
System.out.println("diamond = " + diamond);
System.out.println("basic = " + basic);
}
}
우리는 파라미터에 이제 Grade.BASIC으로 넘겨주기만 하면된다.
Grade.BASIC 참조값 x001에 해당하는 인스턴스는 자바가 컴파일되는시점에 미리 인스턴스를 생성해놓기 때문이다.
모든 열거형은 java.lang.Enum을 자동으로 상속받는다.
public enum Grade extends Enum{}이렇게 되어있는데 생략된 것이다.
public class EnumMethodMain {
public static void main(String[] args) {
//모든 Enum반환
Grade[] values = Grade.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);
}
}
참고
ordinal()은 가급적 사용하지 않는것이 좋다.
만약 파일,DB에는 basic,GOLD,DIAMOND로 0,1,2 순서로 저장되어있는데,
운영 애플리케이션에서 silver가 추가되어 basic,silver,gold,diamond순이라면 01,2,3으로 gold와 diamond가 순서가 한개씩 밀린다.
고로, 상수의 위치가 전체 변경 될 수 있으므로 가급적 사용하지 않는다.
DiscountService에서 if문이 많다.
또한, 할인율은 회원 등급별로 판단되므로, 결국 회원 등급 클래스가 할인율을 가지고 관리하는게 맞다.
public enum Grade {
BASIC(10),GOLD(20),DIAMOND(30);
//원래는 이거
//public static final Grade BASIC = new BASIC(10);
private final int discountPercent;
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
}
BASIC(10) => 해당 Grade 생성자를 통해서 discountPercent를 초기화하면서 인스턴스를 생성한다.
열거형은 상수로 지정하는 것 외에는 생성이 불가능하므로, 따라서 외부에서 Grade 생성자를 호출하지 못하게 private으로 막혀있다.
해당 생성자는 private이 생략되었다고 보면 된다.
public class DiscountService {
public int discount(Grade grade, int price){
return price* grade.getDiscountPercent()/100;
}
}
if문이 완전히 제거되고, 단순히 할인률 계산 로직만 남게 되었다.
public class EnumRefMain2 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
int basic = discountService.discount(Grade.BASIC,price);
int diamond = discountService.discount(Grade.DIAMOND,price);
System.out.println("정상 할인금액반환");
System.out.println("diamond = " + diamond);
System.out.println("basic = " + basic);
}
}
discount메서드의 인자에 참조값을 넘겨주고있다.
enum클래스에서
public static final Grade BASIC = new BASIC(10);
이 BASIC(10)으로 생략되어있으므로,
메서드 영역에 있는 참조값 을 넘겨주어서 discountPercent를 가져와서 사용하고 있다.
리팩토링2
DiscountService의 discount메서드에서는 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;
}
}
public class EnumRefMain2 {
public static void main(String[] args) {
int price = 10000;
int basic = Grade.BASIC.discount(price);
int diamond = Grade.DIAMOND.discount(price);
System.out.println("정상 할인금액반환");
System.out.println("diamond = " + diamond);
System.out.println("basic = " + basic);
}
}
이제는 discountService를 사용하지 않고 enum class의 discount메서드를 호출하면된다.
활용예제
public enum AuthGrade{
GUEST(1,"손님"),
LOGIN(2,"로그인 회원"),
ADMIN(3,"관리자");
private final int level;
private final String description;
AuthGrade(int level, String description) {
this.level = level;
this.description = description;
}
//유용한 기능 추가
}