문자열과 타입 안전성1
먼저 자바가 제공하는 열거형(Enum Type)이 만들어진 근본적인 이유를 알아봅시다.
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);
}
}
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
- 실행 결과를 보면 각각의 회원 등급에 맞는 할인이 적용된 것을 확인할 수 있다.
- 하지만 지금과 같이 단순히 문자열을 입력하는 방식은, 오타가 발생하기 쉽고, 유효하지 않는 값이 입력될 수 있다.
public class StringGradeEx0_2 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
// 존재하지 않는 등급
int vip = discountService.discount("VIP", price);
System.out.println("VIP 등급의 할인 금액: " + vip);
// 오타
int diamondd = discountService.discount("DIAMONDD", price);
System.out.println("DIAMONDD 등급의 할인 금액: " + diamondd);
// 소문자 입력
int gold = discountService.discount("gold", price);
System.out.println("gold 등급의 할인 금액: " + gold);
}
}
VIP: 할인X
VIP 등급의 할인 금액: 0
DIAMONDD: 할인X
DIAMONDD 등급의 할인 금액: 0
gold: 할인X
gold 등급의 할인 금액: 0
- 존재하지 않는 VIP 등급을 입력했다.
- 오타로 D가 하나 더 추가되었다.
- 등급은 모두 대문자인데, 소문자를 입력했다.
등급에 문자열을 사용하는 지금의 방식은 다음과 같은 문제가 있습니다.
- 타입 안전성 부족: 문자열은 오타가 발생하기 쉽고, 유효하지 않은 값이 입력될 수 있다.
- 데이터 일관성: "GOLD", "gold", "Gold" 등 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다.
String 사용 시 타입 안전성 부족 문제
- 값의 제한 부족: String 으로 상태나 카테고리를 표현하면, 잘못된 문자열 (오타나 잘못된 값)이 입력될 위험이 있다.
- 컴파일 시 오류 감지 불가: 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다.
이런 문제를 해결하려면 특정 범위로 값을 제한해야 합니다. 예를 들어 BASIC, GOLD, DIAMOND 라는 정확한 문자만
discount( ) 메서드에 전달되어야 합니다. 하지만 String은 어떤 문자열이든 받을 수 있기 때문에 자바 문법 관점에는
아무런 문제가 없고 결국 String 타입을 사용해서는 문제를 해결할 수 없습니다.
문자열과 타입 안전성2
이번에는 대안으로 문자열 상수를 사용해봅시다.
상수는 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 직접 사용하는 것 보다는 더 안전합니다.
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;
}
}
public class StringGradeEx1_1 {
public static void main(String[] args) {
int price = 10000;
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에 있는 문자열 상수를 사용하지 않고 직접 문자열을 사용해도 막을 수 있는 방법이 없습니다.
public class StringGradeEx1_2 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
// 존재하지 않는 등급
int vip = discountService.discount("VIP", price);
System.out.println("VIP 등급의 할인 금액: " + vip);
// 오타
int diamondd = discountService.discount("DIAMONDD", price);
System.out.println("DIAMONDD 등급의 할인 금액: " + diamondd);
// 소문자 입력
int gold = discountService.discount("gold", price);
System.out.println("gold 등급의 할인 금액: " + gold);
}
}
public int discount(String grade, int price) {}
사용해야 하는 문자열 상수가 어디에 있는지 discount( )를 호출하는 개발자는 알기 쉽지 않습니다.
위에 코드를 보면 분명히 String은 다 입력할 수 있다고 되어있기 때문입니다.
결국 누군가 주석을 잘 남겨두어서, StringGrade에 있는 상수를 사용해달라고 해야 합니다.
이렇게 해도 누군가는 주석을 깜빡하고 문자열을 직접 입력할 수도 있습니다.
타입 안전 열거형 패턴 (Type-Safe-Enum Pattern)
지금까지 설명한 문제를 해결하기 위해 많은 개발자들이 오랜기간 고민하고 나온 결과가 타입 안전 열거형 패턴입니다.
여기서 영어인 enum은 enumeration의 줄임말인데, 번역하면 열거라는 뜻이고, 어떤 항목을 나열하는 것을 뜻합니다.
우리의 경우 회원 등급인 BASIC, GOLD, DIAMOND를 나열하는 것 입니다. 여기서 중요한 것은 타입 안전 열거형 패턴을
사용하면 이렇게 나열한 항목만 사용할 수 있다는 것이 핵심입니다. 나열한 항목이 아닌 것은 사용할 수 없습니다.
쉽게 얘기하면 앞서본 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();
}
- 먼저 회원 등급을 다루는 클래스를 만들고, 각각의 회원 등급별로 상수를 선언한다.
- 이때 각각의 상수마다 별도의 인스턴스를 생성하고, 생성한 인스턴스를 대입한다.
- 각각을 상수로 선언하기 위해 static, final을 사용한다.
- static을 사용해서 상수를 메서드 영역에 선언한다.
- final을 사용해서 인스턴스(참조값)를 변경할 수 없게 한다.
public class ClassRefMain {
public static void main(String[] args) {
System.out.println("class BASIC = " + ClassGrade.BASIC.getClass());
System.out.println("class GOLD = " + ClassGrade.GOLD.getClass());
System.out.println("class DIAMOND = " + ClassGrade.DIAMOND.getClass());
System.out.println("ref BASIC = " + ClassGrade.BASIC);
System.out.println("ref GOLD = " + ClassGrade.GOLD);
System.out.println("ref DIAMOND = " + ClassGrade.DIAMOND);
}
}
class BASIC = class enumeration.ex2.ClassGrade
class GOLD = class enumeration.ex2.ClassGrade
class DIAMOND = class enumeration.ex2.ClassGrade
ref BASIC = enumeration.ex2.ClassGrade@1d81eb93
ref GOLD = enumeration.ex2.ClassGrade@7291c18f
ref DIAMOND = enumeration.ex2.ClassGrade@34a245ab
- 각각의 상수는 모두 ClassGrade 타입을 기반으로 인스턴스로 만들었기 떄문에 getClass( ) 의 결과는 모두 ClassGrade이다.
- 각각의 상수는 모두 서로 각각 다른 ClassGrade 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.
static 이므로 애플리케이션 로딩 시점에 다음과 같이 3개의 ClassGrade 인스턴스가 생성되고,
각각의 상수는 같은 ClassGrade 타입의 서로 다른 인스턴스의 참조값을 가집니다.
- ClassGrade BASIC: x001
- ClassGrade GOLD: x002
- ClassGrade DIAMOND: x003
여기서 BASIC, GOLD, DIAMOND 를 상수로 열거했습니다.
이제 ClassGrade 타입을 사용할 때는 앞서 열거한 상수들만 사용하면 됩니다.
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 == ClassGrade.BASIC과 같이 == 참조값 비교를 사용하면 된다.
- 매개변수에 넘어오는 인수도 ClassGrade가 가진 상수 중에 하나를 사용한다. 따라서 열거한 상수의 참조값을 비교(==) 하면 된다.
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 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);
}
}
- discount() 를 호출할 때 미리 정의한 ClassGrade 의 상수를 전달한다
public class StringGradeEx2_2 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
ClassGrade newClassGrade = new ClassGrade(); // x009
int result = discountService.discount(newClassGrade, price);
System.out.println("newClassGrade 등급의 할인 금액: " + result);
}
}
할인X
newClassGrade 등급의 할인 금액: 0
- 이 문제를 해결하려면 외부에서 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 생성자 추가
private ClassGrade() {
}
}
- private 생성자를 사용해서 외부에서 ClassGrade 를 임의로 생성하지 못하게 막았다.
- private 생성자 덕분에 ClassGrade의 인스턴스를 생성하는 것은 ClassGrade 클래스 내부에서만 할 수 있다. 앞서 우리가 정의한 상수들은 ClassGrade 클래스 내부에서 ClassGrade 객체를 생성한다.
- 이제 ClassGrade 인스턴스를 사용할 때는 ClassGrade 내부에 정의한 상수를 사용해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.
- 쉽게 이야기하면 ClassGrade 타입에 값을 전달할 때는 우리가 아서 열거한 BASIC, GOLD, DIAMOND 상수만 사용할 수 있다.
이렇게 private 생성자까지 사용하면 타입 안전 열거형 패턴을 완성할 수 있습니다.
타입 안전 열거형 패턴 (Type-Safe-Enum Pattern)의 장점
- 타입 안전성 향상: 정해진 객체만 사용할 수 있기 때문에, 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있다.
- 데이터 일관성: 정해진 객체만 사용하므로 데이터의 일관성이 보장된다.
- 제한된 인스턴스 생성: 클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용할 수 있도록 한다. 이를 통해 미리 정의된 값들만 사용하도록 보장한다.
- 타입 안전성: 이 패턴을 사용하면, 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지할 수 있다. 예를 들어, 특정 메서드가 특정 열거형 타입의 값을 요구한다면, 오직 그 타입의 인스턴스만 전달할 수 있다. 여기서는 메서드의 매개변수로 ClassGrade를 사용하는 경우, 앞서 열거한 BASIC, GOLD, DIAMOND만 사용할 수 있다.
단점
- 이 패턴을 구현하려면 다음과 같이 많은 코드를 작성해야 한다.
- 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() {
}
}
열거형 - Enum Type
자바는 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을 제공합니다.
자바의 enum은 타입 안전성을 제공하고, 코드의 가독성을 높이며, 예상 가능한 값들의 집합을 표현하는 데
사용되고 있습니다.
public enum Grade {
BASIC, GOLD, DIAMOND
}
- 열거형을 정의할 때는 class 대신에 enum을 사용한다.
- 원하는 상수의 이름을 나열하면 된다.
앞서 직접 ClassGrade를 구현할 때와는 비교가 되지 않을 정도로 편리합니다.
public class ClassGrade extends Enum {
public static final ClassGrade BASIC = new ClassGrade();
public static final ClassGrade GOLD = new ClassGrade();
public static final ClassGrade DIAMOND = new ClassGrade();
// private 생성자 추가
private ClassGrade() {
}
}
- 열거형도 클래스이다.
- 열거형은 자동으로 java.lang.Enum 상속 받는다.
- 외부에서 임의로 생성할 수 없다.
public class EnumRefMain {
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 = 1d81eb93
ref GOLD = 7291c18f
ref DIAMOND = 34a245ab
- 실행 결과를 보면 상수들이 열거형으로 선언한 타입인 Grade 타입을 사용하는 것을 확인할 수 있다. 그리고 각각의 인스턴스도 다른것은 확인할 수 있다.
- 참고로 열거형은 toString( )을 재정의 하기 때문에 참조값을 직접 확인할 수 없어서 refValue( )를 만들었다.
- 열거형도 클래스이다. 열거형을 제공하기 위해 제약이 추가된 클래스라 생각하면 된다.
public class DiscountService {
public int discount(Grade grade, int price) {
int discountPercent = 0;
if (grade == Grade.BASIC) {
discountPercent = 10;
} else if (grade == Grade.GOLD) {
discountPercent = 20;
} else if (grade == Grade.DIAMOND) {
discountPercent = 30;
} else {
System.out.println("할인X");
}
return price * discountPercent / 100;
}
}
public class StringGradeEx3_1 {
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);
}
}
- 열거형의 사용법이 앞서 타입 안전 열거형 패턴을 직접 구현한 코드와 같은 것을 확인할 수 있다.
- 참고로 열거형은 switch 문에 사용할 수 있는 장점도 있다.
열거형은 외부 생성 불가
Grade grade = new Grade(); // enum 생성 불가
- enum은 열거형 내부에서 상수로 지정하는 것 외에 직접 생성이 불가능하다. 생성할 경우 컴파일 오류가 발생한다.
열거형(ENUM)의 장점
- 타입 안전성 향상: 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성이 없다. 이런 경우 컴파일 오류가 발생한다.
- 간결성 및 일관성: 열거형을 사용하면 코드가 더 간결해지고 명확해지며, 데이터 일관성이 보장된다.
- 확장성: 새로운 회원 등급을 타입에 추가하고 싶을 때, ENUM에 새로운 상수를 추가하기만 하면 된다.
참고: 열거형을 사용하는 경우 static import를 적절하게 사용하면 더 읽기 좋은 코드를 만들 수 있다.
// 추가
import static enumeration.ex3.Grade.BASIC;
public class DiscountService {
public int discount(Grade grade, int price) {
int discountPercent = 0;
if (grade == BASIC) {
discountPercent = 10;
} else if (grade == Grade.GOLD) {
discountPercent = 20;
} else if (grade == Grade.DIAMOND) {
discountPercent = 30;
} else {
System.out.println("할인X");
}
return price * discountPercent / 100;
}
}
열거형 - 주요 메서드
모든 열거형은 java.lang.Enum 클래스를 자동으로 상속 받습니다.
따라서 해당 클래스가 제공하는 기능들을 사용할 수 있습니다.
import java.util.Arrays;
public class EnumMethodMain {
public static void main(String[] args) {
// 모든 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 변환
String input = "GOLD";
Grade gold = Grade.valueOf(input);
System.out.println("gold = " + gold); // toString() 오버라이딩 가능
}
}
- Arrays.toString( ) 배열의 참조값이 아니라 배열 내부의 값을 출력할 때 사용한다.
ENUM - 주요 메서드
- values( ): 모든 ENUM 상수를 포함하는 배열을 반환한다.
- valueOf(String name): 주어진 이름과 일치하는 ENUM 상수를 반환한다.
- name( ): ENUM 상수의 이름을 문자열로 반환한다.
- ordinal( ): ENUM 상수의 선언 순서(0부터 시작)를 반환한다.
- toString( ): ENUM 상수의 이름을 문자열로 반환한다. name( ) 메서드와 유사하지만, toString( )은 직접 오버라이드 할 수 있다.
주의: ordinal은 가급적 사용하지 않는 것이 좋다.
- 이 값을 사용하다가 중간에 상수를 선언하는 위치가 변경되면 전체 상수의 위치가 모두 변경될 수 있기 때문이다.
- 예를 들어 중간에 BASIC 다음에 SILVER 등급이 추가되는 경우 GOLD, DIAMOND 값이 하나씩 추가된다.
기존
- BASIC: 0
- GOLD: 1
- DIAMOND: 2
추가
- BASIC: 0
- SILVER: 1
- GOLD: 2
- DIAMOND: 3
기존 GOLD의 ordinal( ) 값인 1을 데이터베이스나 파일에 저장하고 있었는데, 중간에 SILVER가 추가되면 데이터베이스나
파일에 있는 값은 그대로 1로 유지되지만, 애플리케이션 상에서 GOLD는 2가 되고, SILVER는 1이 된다.
쉽게 이야기하면 ordinal( ) 의 값을 사용하면 기존 GOLD 회원이 갑자기 SILVER가 되는 큰 버그가 발생할 수 있다.
열거형 정리
- 열거형은 java.lang.Enum를 자동(강제)으로 상속 받는다.
- 열거형은 이미 java.lang.Enum을 상속받기 때문에 추가로 다른 클래스를 상속 받을 수 없다.
- 열거형은 인터페이스를 구현할 수 있다.
- 열거형에 추상 메서드를 선언하고, 구현할 수 있다.
열거형 - 리펙토링1
클래스를 직접 사용해서 열거형 패턴을 구현했던 코드를 먼저 리펙토링 해보겠습니다.
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;
}
}
- 불필요한 if 문을 제거하자.
- 이 코드에서 할인율은 각각의 회원 등급별로 판단된다. 할인율은 결국 회원등급을 따라간다는 것이다.
- 따라서 회원 등급 클래스가 할인율(discountPercent)을 가지고 관리하도록 변경하면 된다.
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;
}
}
- ClassGrade에 할인율(discountPercent) 필드를 추가했다. 조회 메서드도 추가한다.
- 생성자를 통해서만 discountPercent를 설정하도록 했고, 중간에 이 값이 변하지 않도록 불변으로 설계했다.
- 정리하면 상수를 정의할 때 각각의 등급에 따른 할인율이 정해진다.
public class DiscountService {
public int discount(ClassGrade classGrade, int price) {
return price * classGrade.getDiscountPercent() / 100;
}
}
- 기존에 있던 if 문이 완전히 제거되고, 단순한 할인율 계산 로직만 남았다.
- 기존에는 if 문을 통해서 회원의 등급을 찾고, 각 등급 별로 discountPercent의 값을 지정했다.
- 변경된 코드에서는 if 문을 사용할 이유가 없다. 단순히 회원 등급안에 있는 getDiscountPercent( ) 메서드를 호출하면 인수로 넘어온 회원 등급의 할인율을 바로 구할 수 있다.
열거형 - 리펙토링2
public enum Grade {
BASIC(10), GOLD(20), DIAMOND(30);
private final int discountPercent;
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
}
- discountPercent 필드를 추가하고, 생성자를 통해서 필드에 값을 지정한다.
- 열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하다. 따라서 생성자에 접근제어자를 선언할 수 없게 막혀있다. private이라고 생각하면 된다.
- BASIC(10)과 같이 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출된다.
- 값을 조회하기 위해 getDiscountPercent( ) 메서드를 추가했다. 열거형도 클래스이므로 메서드를 추가할 수 있다.
열거형 - 리펙토링3
public class DiscountService {
public int discount(Grade grade, int price) {
return price * grade.getDiscountPercent() / 100;
}
}
- 이 코드를 보면 할인율 계산을 위해 Grade가 가지고 있는 데이터인 discountPercent 의 값을 꺼내서 사용한다.
- 결국 Grade의 데이터인 discountPercent를 할인율 계산에 사용한다.
- 객체지향 관점에서 이렇게 자신의 데이터를 외부에 노출하는 것보다는, Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.
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 내부에 discount( ) 메서드를 만들어서, 이제 할인율을 스스로 계산한다.
public class DiscountService {
public int discount(Grade grade, int price) {
return grade.discount(price);
}
}
- 할인율 계산은 이제 Grade가 스스로 처리한다. 따라서 DiscountService.discount( ) 메서드는 단순히 Grade.discount( )만 호출하면 된다.
public class EnumRefMain3_1 {
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);
}
}
실행 결과는 기존과 같습니다.
DiscountService 제거
Grade가 스스로 할인율을 계산하면서 DiscountService 클래스가 더는 필요하지 않게 되었습니다.
public class EnumRefMain3_2 {
public static void main(String[] args) {
int price = 10000;
int basic = Grade.BASIC.discount(price);
int gold = Grade.GOLD.discount(price);
int diamond = Grade.DIAMOND.discount(price);
System.out.println("BASIC 등급의 할인 금액: " + basic);
System.out.println("GOLD 등급의 할인 금액: " + gold);
System.out.println("DIAMOND 등급의 할인 금액: " + diamond);
}
}
public class EnumRefMain3_2 {
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));
}
}
- 각각의 등급별로 자신의 discount( )를 직접 호출하면 할인율을 구할 수 있다.
- DiscountService를 제거해도 된다.
중복 제거
출력 부분의 중복을 제거할 수 있습니다.
public class EnumRefMain3_3 {
public static void main(String[] args) {
int price = 10000;
printDiscount(Grade.BASIC, price);
printDiscount(Grade.GOLD, price);
printDiscount(Grade.DIAMOND, price);
}
private static void printDiscount(Grade grade, int price) {
System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
}
}
- grade.name( )을 통해서 ENUM 상수 이름을 사용할 수 있다.
ENUM 목록
이후에 새로운 등급이 추가되더라도 main( ) 코드의 변경 없이 모든 등급의 할인을 출력해봅시다.
public class EnumRefMain3_4 {
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));
}
}
- Grade.values( )를 사용하면 Grade 열거형의 모든 상수를 배열로 구할 수 있다.
'Java' 카테고리의 다른 글
[Java] 날짜와 시간 파싱, 포맷팅 (0) | 2024.04.15 |
---|---|
[Java] 날짜와 시간 (0) | 2024.04.13 |
[Java] Math, Random 클래스 (0) | 2024.04.11 |
[Java] System 클래스 (0) | 2024.04.11 |
[Java] Class 클래스?? (0) | 2024.04.05 |