열거형(Enum Type)을 제대로 이해하기 위해서는 타입 안정성에 대한 개념을 이해하는 것이 중요하다. 예를 들어, 등급에 따라 할인 금액을 내어주는 클래스를 만든다고 가정해보자.
package ex0;
public class DiscuntService {
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 + ": 할인 불가");
}
return price * discountPercent / 100;
}
}
// --------------------------------------- //
// main //
package ex0;
public class StringGradeEx0_1 {
public static void main(String[] args) {
int price = 10000;
DiscuntService discuntService = new DiscuntService();
int basic = discuntService.discount("BASIC", price);
int gold = discuntService.discount("GOLD", price);
int diamond = discuntService.discount("DIAMOND", price);
System.out.println("basic = " + basic);
System.out.println("gold = " + gold);
System.out.println("diamond = " + diamond);
}
}
위 코드에서는 discount 메서드의 첫 번째 인자로 문자열(grade)을 사용하여 등급을 비교하고 있다. 하지만 "BASIC" 대신 "basic"과 같은 소문자 입력이나, "BASCI"와 같은 오타가 발생하면 "할인 불가"로 처리되는 문제가 발생할 수 있다. 이처럼 문자열을 다룰 때는 실수로 인한 오류가 자주 발생할 수 있다.
이 문제를 해결하기 위해 상수를 사용하는 방법이 있다. 상수를 사용하면 문자열을 직접 입력하는 대신 미리 정의된 값을 사용하여 실수의 위험을 줄일 수 있다.
package ex1;
public class StringGrade {
public static final String BASIC = "BASIC";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}
//
package ex1;
public class DiscuntService {
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 + ": 할인 불가");
}
return price * discountPercent / 100;
}
}
// main
package ex1;
import ex0.DiscuntService;
public class StringGradeEx0_1 {
public static void main(String[] args) {
int price = 10000;
ex0.DiscuntService discuntService = new DiscuntService();
int basic = discuntService.discount(StringGrade.BASIC, price);
int gold = discuntService.discount(StringGrade.GOLD, price);
int diamond = discuntService.discount(StringGrade.DIAMOND, price);
System.out.println("basic = " + basic);
System.out.println("gold = " + gold);
System.out.println("diamond = " + diamond);
}
}
다음과 같이 상수의 모음집을 설정할 수 있지만, 이는 근본적인 해결책이 되지 않는다. 그 이유는 애초에 discount 메서드가 입력으로 String 타입을 받기 때문이다.
discount의 파라미터에 항상 상수를 넣으면 될 것 같지만, 만약 "VIP", "DIamond"와 같이 입력해도 동작에는 문제가 없다. 이는 String을 입력으로 받기 때문에, 대소문자나 철자 오류가 있어도 프로그램이 정상적으로 동작할 수 있기 때문이다.
따라서, 상수를 사용하여 실수를 줄일 수는 있지만, 근본적인 문제를 해결하려면 입력값의 유효성을 보장할 수 있는 방법이 필요하다.
결국 "VIP"라고 입력을 넣었을 때도 컴파일 에러를 발생시킬 수 있다면 좋을 것이다.
위와 같이 설계할 경우 에러 발생시 책임은?
만약 선임 개발자가 주석을 통해 항상 상수가 담긴 파일인 StringGrade를 사용하라고 명시했지만, 주니어 개발자가 이를 까먹고 실수로 상수파일을 사용하지 않고 "gold" String을 사용했다면 이는 선임 개발자의 잘못이다. 애초에 파라미터에 들어가는 인자에 대해 의도되지 않는 인자가 들어오는 것을 막아야 한다.
위의 문제에 대한 근본적인 분석을 해보자. 회원등급은 오로지 BASIC, GOLD, DIAMOND뿐이며 이 세개를 제외한 어떠한 것도 들어올 수 없다.
이러한 제한을 구현하기 위해 옛날 개발자들은 초기 Enum 방식으로, Grade와 같은 상수 클래스를 직접 정의하여 사용했다. 이를 통해 허용된 값들만 사용할 수 있도록 제한하는 방식이었다.
package ex2;
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() {
}
}
//
package ex2;
import ex1.StringGrade;
public class DiscuntService {
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 클래스의 private 생성자를 사용한 이유는 외부에서 이 클래스의 객체를 생성할 수 없게 하기 위함이다. 즉, 외부에서 직접적으로 ClassGrade 객체를 만들 수 없으며, ClassGrade에 정의된 static 상수만 사용할 수 있게 된다.
하지만 이러한 방식은 코드가 길어지는 단점이 있다. ClassGrade 클래스처럼 매번 상수를 정의하고 관리하는 것은 번거로울 수 있다. 이러한 문제를 해결하기 위해 자바는 Enum 클래스를 제공하게 되었다.
Enum은 코드가 길어지고 복잡해지는 문제를 해결하면서도 타입 안전성을 보장할 수 있는 더 간편한 방법을 제공한다. Enum을 사용하면, 더 짧고 직관적인 코드로 제한된 값들을 관리할 수 있다.
package ex3;
public enum Grade {
BASIC, GOLD, DIAMOND
}
이 코드는 위의 ClassGrade 클래스와 동일한 기능을 한다.
현재 위의 코드를 보면 BASIC은 항상 10%이다. 즉 1대1 연결관계를 가지고 있다. 이를 Enum 통해 내부에서 처리가 가능하다.
// enum class
package enumeration1.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;
}
public int discount(int price) {
return price * discountPercent / 100 ;
}
}
// class
package enumeration1.ref1;
public class ClassGrade {
public static final ClassGrade BASIC = new ClassGrade(10);
public static final ClassGrade GOLD = new ClassGrade(20);
public static final ClassGrade DIAMOND = new ClassGrade(30);
private final int discountPercent;
private ClassGrade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
public int discount(int price) {
return price * discountPercent / 100 ;
}
}
위의 enum 클래스와 아래의 일반 클래스는 동일한 로직을 가지고 있다. private 생성자를 사용함으로써, 생성자는 내부 클래스 안에서만 사용할 수 있다. 이와 같은 방식은 enum에서도 동일하게 적용된다. enum도 클래스의 성질을 가지므로, 메서드를 정의하여 상수와 관련된 기능을 구현할 수 있다.
enum은 일반 클래스와 동일하게 메서드를 선언할 수 있으며, private 생성자를 통해 외부에서 객체 생성이 불가능하도록 제한할 수 있다. 이를 통해 상수 값을 관리하면서도 필요한 동작을 메서드로 구현할 수 있다.
이를 통해 외부의 DiscountService클래스 자체를 없애버릴 수 있다.
// 리펙토링 (discount 메서드 삭제, main 코드 복잡도 개선
// main
package enumeration1.ref2;
public class ClassGradeEx3_1 {
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));
}
}
좋은 개발자는 문제를 단순히 조심하는 것이 아니라, 문제가 발생할 가능성을 적절한 제한으로 원천 차단하는 개발자이다. 단순한 구현에 그치지 않고, 미래에 발생할 수 있는 시나리오를 고려하여 현재에서 다룰 수 있는 부분과 다룰 수 없는 부분을 명확히 구분해야 한다.
Enum 클래스는 상수들을 다루기 때문에, 생성 시점부터 클래스 영역에 상수들이 생성된다. 이 상수들을 다룰 때, 만약 상수와 연결된 또 다른 연결상수들이 필요하다면, private 생성자를 활용하여 이러한 상수들을 다룰 수 있다. 이를 통해 불필요한 외부 객체 생성을 막고, 필요한 값들만 관리할 수 있다.
Enum은 동일한 클래스의 성질을 가지기 때문에, 일반적인 메서드를 선언하는 데도 무리가 없다. 즉, Enum을 통해 상수뿐만 아니라 상수와 관련된 동작을 유연하게 다룰 수 있다.
좋은 설계는 미래에 발생할 수 있는 문제를 현재에서 예측하고 해결책을 마련하는 것이다. Enum은 상수 값을 안전하고 명확하게 관리하며, 필요한 메서드를 추가하여 더욱 유연하게 확장할 수 있는 방법을 제공한다.