열거형을 배우기 이전에 왜 열거형이 나오게 되었고, 열거형이 아닌 문자열을 사용했을 때, 상수를 사용했을때와의 차이를 알아보자
문자열로 구현
먼저 회원등급별로 할인을 적용하는 메서드를 짤 때 문자열로 회원의 등급을 구분해보자
public int discountPrice(String grade, int price) {
int ratio = 0;
if (grade.equals("SILVER")){
ratio = 10;
} else if (grade.equals("GOLD")) {
ratio = 20;
} else if (grade.equals("DIAMOND")) {
ratio = 30;
} else {
System.out.println("할인 없음");
ratio = 0;
}
return price * ratio / 100;
}
문자열로 회원의 등급을 구분한다면 SILVERR
와 같이 오타가 났거나, "VIP" 와같이 해당하지 않는 등급이 들어올 경우 컴파일오류로 잡아주지 못하기때문에 런타임에러가 발생할 수 있다.
이처럼 문자열로 구현하면 데이터의 일관성이 떨어지기때문에 원하는것과 다른방향으로 코드가 사용될 수 있다.
상수로 구현
문자열에는 컴파일에러가 나지않는 문제를 해결하기위해 등급을 상수로 선언하여 메서드를 사용할때 매개변수로 상수를 전달하도록 변경해보자
public int discountPrice(String grade, int price) {
int ratio = 0;
if (grade.equals(GradeConst.SILVER)){
ratio = 10;
} else if (grade.equals(GradeConst.GOLD)) {
ratio = 20;
} else if (grade.equals(GradeConst.DIAMOND)) {
ratio = 30;
} else {
System.out.println("할인 없음");
ratio = 0;
}
return price * ratio / 100;
}
이렇게 변경해주면 이전보다는 조금 더 데이터 일관성을 유지할 수 있으며, 오타나 유효하지 않는 매개변수가 들어올 일도 없게된다.
하지만 상수를 사용했을 때에도 문제점이 하나 있는데 애초에 discountPrice메서드의 grade매개변수의 타입이 String이기때문에 상수를 사용한다하더라도 클라이언트 코드에서는 문자열로 넘길수 있다는 점이다.
타입 안전 열거형 패턴
이제는 클라이언트 코드에서 문자열로 넘길 수 있다는 것을 막아야한다.
즉, discountPrice메서드의 grade 매개변수의 타입을 String타입으로 받으면안된다.
public class GradeClass {
public final static GradeClass SILVER = new GradeClass();
public final static GradeClass GOLD = new GradeClass();
public final static GradeClass DIAMOND = new GradeClass();
private GradeClass() {
}}
등급을 의미하는 grade를 아예 하나의 클래스로 정의해버린 뒤, 상수의 개념을 적용해서 각각의 등급에 새로운 인스턴스를 하나씩 생성해주는 것이다.
이 때 생성자를 private으로 하여 외부로부터 인스턴스가 생성되는 것을 막아주어야 클라이언트 코드에서 파라미터 값을 넣을 때 정해진 등급의 인스턴스가 아닌 새로운 인스턴스가 들어오는 것을 막을 수 있다.
public int discountPrice(GradeClass grade, int price) {
int ratio = 0;
if (grade.equals(GradeClass.SILVER)){
ratio = 10;
} else if (grade.equals(GradeClass.GOLD)) {
ratio = 20;
} else if (grade.equals(GradeClass.DIAMOND)) {
ratio = 30;
} else {
System.out.println("할인 없음");
ratio = 0;
}
return price * ratio / 100;
}
discountPrice메서드에서도 GradeClass
타입으로 grade매개변수를 클라이언트로부터 받으며 클라이언트에서도 GradeClass
타입으로 파라미터를 넘기면 문자열, 상수를 사용함으로 발생했던 문제들을 모두 해결할 수 있다.
int i = discountService.discountPrice(GradeClass.DIAMOND, 10000);
결론은, '등급'정보를 담고있는 클래스를 따로 생성해주고 클래스 안에는 등급에 대한 정보를 담고있는 상수 인스턴스를 생성해놓고 필요할 때마다 정해진 객체의 주소값을 가지고있는 등급을 사용할 수 있는것이다.
이 개념을 개발자가 편리하게 사용할 수 있도록 자바에서 가공해준 개념이 Enum Type이다.
Enum Type
enum타입을 생성하고 이 enum클래스를 풀어서 쓴다면 어떤 자바코드가 나오는지 간단히 비교해보자
//enum type
public enum Grade {
SILVER, GOLD, DIAMOND
}
//간단히 구현한 유사enum type
public class Grade{
public final static Grade SILVER = new Grade();
public final static Grade GOLD = new Grade();
public final static Grade DIAMOND = new Grade();
private Grade() {}
}
위 코드의 비교를 통해 enum class가 어떤 구조로 구성되어있는지 이해할 수 있다.
역할 분리
public int discountPrice(GradeClass grade, int price) {
int ratio = 0;
if (grade.equals(GradeClass.SILVER)){
ratio = 10;
} else if (grade.equals(GradeClass.GOLD)) {
ratio = 20;
} else if (grade.equals(GradeClass.DIAMOND)) {
ratio = 30;
} else {
System.out.println("할인 없음");
ratio = 0;
}
return price * ratio / 100;
}
위 서비스 클래스의 코드를 분석해보면 SILVER등급의 GradeClass가 할인율인 ratio를 결정하고 있음을 알 수 있다.
즉, 회원의 등급만 알면 할인율이 결정되기때문에 회원의 등급클래스가 할인율을 관리할 수 있도록 리팩터링하면 코드를 효율적으로 짤 수 있다.
Enum적용 X
public class GradeClass {
public final static GradeClass SILVER = new GradeClass(10);
public final static GradeClass GOLD = new GradeClass(20);
public final static GradeClass DIAMOND = new GradeClass(30);
private final int discountPercent;
private GradeClass(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
}
회원의 등급을 결정하는 순간에 할인율을 함께 결정할 수 있도록 해보았다.
회원의 등급을 의미하는 GradeClass에서 discountPercent
필드를 생성하고 해당 필드는 등급에 맞게 생성될 수 있도록 생성자로 필드값을 입력해주었다.
이 값은 getXXX메서드를 통해 회원의 할인율을 알 수 있다.
SILVER: 10
GOLD: 20
DIAMOND: 30
public class DiscountService {
public int discountPrice(GradeClass grade, int price) {
return grade.getDiscountPercent() * price / 100;
}
}
할인율이 회원의 등급에의해 결정되도록 리팩터링한 결과 서비스 코드에있던 많은 if문을 제거할 수 있다.
enum type에서 적용
enum type도 마찬가지로 클래스이기때문에 생성자, 필드, 메서드를 선언해 줄 수 있다.
//등급별 할인율 주입 (생성자 파라미터)
SILVER(10), GOLD(20), DIAMOND(20);
private final int discountPercent;
//생성자를 의미
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
생성자 값은 열거로 선언한 값의 바로 직접 넣어주면 된다.
GradeClass와 Grade열거 타입을 비교해가며 코드를 보면 쉽게 이해할 수 있다.
enum type 주요 메서드
values(): 모든 ENUM 상수를 포함하는 배열을 반환한다.
valueOf(String name): 주어진 이름과 일치하는 ENUM 상수를 반환한다.
name(): ENUM 상수의 이름을 문자열로 반환한다.
ordinal(): ENUM 상수의 선언 순서(0부터 시작)를 반환한다.
toString(): ENUM 상수의 이름을 문자열로 반환한다. name()
메서드와 유사하지만, toString()
은 직접 오버라이드 할 수 있다.
ordinal()은 되도록이면 사용하지 않는 것이 좋다
DB연동과의 문제인데, 기존에 사용하던 등급이 3개였다가 중간에 요구사항 변경으로 인해 새로운 등급이 중간에 들어왔다고 가정해보자, DB에서는
SILVER:0, GOLD: 1, DIAMOND: 2 로 사용하던 중 Platinum등급이 새로 추가되었다면
SILVER:0, GOLD: 1, PLATIONUM: 2, DIAMOND: 3로 변경되어야하는데
기존에 다이아몬드등급은 2로 사용되고있었기때문에 DB값은 DIAMOND - 2로 매핑되어있다.
하지만 플래티넘이 추가될 경우 DB에서 회원의 등급에 맞는 컬럼을 모두 변경해주어야하며, 만약 이를 신경쓰지 못할 경우 서비스에서 다이아몬드등급 회원들의 등급이 플래티넘으로 변경되는 불상사가 생길수 있다. 때문에 ordinal()메서드를 사용하는 것을 추천하지 않는다.
'Language > Java' 카테고리의 다른 글
[JAVA] 불변객체 (0) | 2024.06.24 |
---|---|
[JAVA] 래퍼 클래스 (0) | 2024.06.24 |
[Java] - Generic (1) | 2024.06.15 |
[Java - 자료구조] Set인터페이스 (0) | 2024.06.12 |
[JAVA - 자료구조] ArrayList vs LinkedList (1) | 2024.06.09 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!