[Java] 김영한의 실전 자바 - 중급편 섹션5 열거형 - ENUM
○ 열거형의 탄생 유래
등급별로 할인을 적용해보자. 예제 단순화를 위해 회원 등급에 null 은 입력되지 않는다고 가정한다.
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;
}
}
public class StringGradeEx0_1 {
public static void main(String[] args) {
int price = 10000;
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);
}
}
위와 같이 grade를 문자열로 입력하는 방식은, 오타가 발생하기 쉽고 소문자등 유효하지 않는 값이 입력될 수 있다.
🟢 String 사용시 타입 안정성 부족
- 오타나 잘못된 값을 제한 할 수 없으며 데이터일관성이 떨어진다. "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";
}
if (grade.equals(StringGrade.BASIC)) {...} // 생략...
하지만 위 방식도 문제를 근본적으로 해결할 수는 없다. 왜냐하면 개발자가 StringGrade에 있는 문자열 상수를 사용하지 않고 직접 문자열을 사용해도 막을 수 있는 방법이 없다.
○ 타입 안전 열거형 패턴
타입 안전 열거형 패턴을 사용하면 나열한 항목만 사용할 수 있다는 것이 핵심이다. 나열한 항목이 아닌 것은 사용할 수 없다.
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
}
public class DiscountService {
public int discount(ClassGrade classGrade, int price) {
int discountPercent = 0;
if (classGrade == ClassGrade.BASIC) { // 참조값 비교
discountPercent = 10;
...
이제 위처럼 매개변수에 String이 아닌 ClassGrade 클래스를 사용한다.
그런데 위 방식은 외부에서 임의로 ClassGrade의 인스턴스를 생성할 수 있다.
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);
...
따라서 외부에서 생성할 수 없도록 기본 생성자를 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 생성자 추가
private ClassGrade() {}
}
🟢 타입 안전 열거형 패턴의 장점
- 타입 안정성 향상: 정해진 객체만 사용할 수 있기 때문에, 잘못된 값을 입력하는 문제를 근본적으로 방지 가능
- 데이터 일관성: 정해진 객체만 사용하므로 데이터의 일관성이 보장됨
○ 열거형 - Enum Type
위 패턴을 쉽게 사용할 수 있도록 한것이 enum이다.
public enum Grade {
BASIC, GOLD, DIAMOND
}
위는 아래와 거의 같다. 따라서 외부에서 임의로 생성할 수 없다.
public class Grade extends Enum {
public static final Grade BASIC = new Grade();
public static final Grade GOLD = new Grade();
public static final Grade DIAMOND = new Grade();
//private 생성자 추가
private Grade() {}
}
아래 실행결과를 보면 상수들이 위와같이 생성된 걸 알 수 있다.참고로 열거형은 toString() 을 재정의 하기 때문에 참조값을 직접 확인할 수 없다. 참조값을 구하기 위해 refValue() 를 만들었다.
public static void main(String[] args) {
System.out.println("class BASIC = " + Grade.BASIC.getClass());
System.out.println("class GOLD = " + Grade.GOLD.getClass());
System.out.println("class DIAMOND = " + Grade.DIAMOND.getClass());
System.out.println("ref BASIC = " + refValue(Grade.BASIC));
System.out.println("ref GOLD = " + refValue(Grade.GOLD));
System.out.println("ref DIAMOND = " + refValue(Grade.DIAMOND));
}
private static String refValue(Object grade) {
//자바가 관리하는 객체 참조값을 숫자로 변환
return Integer.toHexString(System.identityHashCode(grade));
}
class BASIC = class enumeration.ex3.Grade
class GOLD = class enumeration.ex3.Grade
class DIAMOND = class enumeration.ex3.Grade
ref BASIC = x001
ref GOLD = x002
ref DIAMOND = x003
참고로 열거형은 외부생성이 불가하다.
/*
Grade myGrade = new Grade(); //enum 생성 불가
*/
🟢 열거형(ENUM)의 장점
- 타입 안정성 향상: 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성이 없다. 이
런 경우 컴파일 오류가 발생한다. - 간결성 및 일관성: 열거형을 사용하면 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.
- 확장성: 새로운 회원 등급을 타입을 추가하고 싶을 때, ENUM에 새로운 상수를 추가하기만 하면 된다.
열거형을 사용하는 경우 static import 를 적절하게 사용하면 더 읽기 좋은 코드를 만들 수 있다.
int basic = discountService.discount(Grade.BASIC, price); //사용전
int basic = discountService.discount(BASIC, price); //사용후
○ 열거형 - 주요 메서드
- values(): 모든 ENUM 상수를 포함하는 배열을 반환한다.
- valueOf(String name): 주어진 이름과 일치하는 ENUM 상수를 반환한다. (잘못된 값 입력시 IllegalArgumentException)
- name(): ENUM 상수의 이름을 문자열로 반환한다.
- ordinal(): ENUM 상수의 선언 순서(0부터 시작)를 반환한다.
- toString(): ENUM 상수의 이름을 문자열로 반환한다. name() 메서드와 유사하지만, toString() 은 직접 오버라이드 할 수 있다.
//모든 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
🪄 주의 ordinal()은 가급적 사용하지 않는 것이 좋다.
🟢 열거형(ENUM) 정리
- 열거형은 java.lang.Enum 를 자동(강제)으로 상속 받는다.
- 열거형은 이미 java.lang.Enum 을 상속 받았기 때문에 추가로 다른 클래스를 상속을 받을 수 없다.
- 열거형은 인터페이스를 구현할 수 있다.
- 열거형에 추상 메서드를 선언하고, 구현할 수 있다.
- 이 경우 익명 클래스와 같은 방식을 사용한다. 익명 클래스는 뒤에서 다룬다.
○ 열거형 - 리팩토링1
불필요한 if문을 제거하고, 할인율을 등급 클래스가 관리하도록 변경하자.
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 class DiscountService {
public int discount(ClassGrade classGrade, int price) {
return price * classGrade.getDiscountPercent() / 100;
}
}
○ 열거형 - 리팩토링2
열거형도 동일하게 적용해보자.
public enum Grade {
BASIC(10), GOLD(20), DIAMOND(30); //생성자에 맞는 인수전달 및 호출
private final int discountPercent;
Grade(int discountPercent) { // private
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
}
public class DiscountService {
public int discount(Grade grade, int price) {
return price * grade.getDiscountPercent() / 100;
}
}
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
int basic = discountService.discount(Grade.BASIC, price);
int gold = discountService.discount(Grade.GOLD, price);
int diamond = discountService.discount(Grade.DIAMOND, price);
System.out.println("BASIC 등급의 할인 금액: " + basic);
System.out.println("GOLD 등급의 할인 금액: " + gold);
System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
}
○ 열거형 - 리팩토링3
열위 DiscountService를 보면, grade가 자신의 할인율을 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.
따라서 아래에 discount() 메서드를 추가해, 할인율을 스스로 계산하도록 한다.
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;
}
}
이제 위와같이 더 단순한 코드가 되었다!Grade 가 스스로 할인율을 계산하면서 DiscountService 클래스가 더는 필요치 않다.
public static void main(String[] args) {
int price = 10000;
System.out.println("BASIC 등급의 할인 금액: " + Grade.BASIC.discount(price));
System.out.println("GOLD 등급의 할인 금액: " + Grade.GOLD.discount(price));
System.out.println("DIAMOND 등급의 할인 금액: " + Grade.DIAMOND.discount(price));
}
○ ENUM 목록
Grade.values() 를 사용하면 Grade 열거형의 모든 상수를 배열로 구할 수 있다. 이후에 새로운 등급이 추가되더라도 main() 코드의 변경 없이 모든 등급의 할인을 출력해보자. 최종코드는아래와 같다.
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));
}
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 중급편 섹션9 예외처리1 - 이론 (0) | 2024.05.22 |
---|---|
[Java] 김영한의 실전 자바 - 중급편 섹션8 중첩 클래스,내부 클래스2 (0) | 2024.05.20 |
[Java] 김영한의 실전 자바 - 중급편 섹션3,4 String ,래퍼,Class 클래스 (0) | 2024.05.09 |
[Java] 김영한의 실전 자바 - 중급편 섹션2 불변 객체 (0) | 2024.05.05 |
[Java] 김영한의 실전 자바 - 중급편 섹션1 Object 클래스 (0) | 2024.05.05 |