등급에 따라 할인된 가격으로 제품을 구매하는 엡을 만들때 일반 String만 사용하면 어떻게 될까?
등급에 따라 할인 가격을 반환하는 메소드
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();
System.out.println(grade + ": 할인 X");
}
return price * discountPercent / 100;
}메인 코드
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 gold = discountService.discount("gold", price);
System.out.println("GOLD 등급의 할인 금액 : " + gold );
int diamondd = discountService.discount("DIAMONDD", price);
System.out.println("DIAMONDD 등급의 할인 금액 : " + diamondd );
}현제 계발자가 직접 GOLD, BASIC 등 등급을 직접 타이핑해서 할인 가격을 책정하고 있다.
하지만 이러한 방법을 사용할 경우 컴파일 단계에서 오류 검출 하기가 어렵다. 최악의 경우 서비스를 운영하는 중에
런타임 오류가 발생할 수 있다.
정리하자면 다음과 같다.
- 타입 안정성 부족 : 문자열은 오타가 발생하기 쉽고, 유효하지 안은 값이 입력될 수 있다.
- 데이터 일관성 :
GOLD,gold,Gold등 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다. - String 사용 시 타입 안정성 부족 문제
- 값의 제한 부족 :
String으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다. 예를 들어,Monday,Tuesday등을 나타내는 데String을 사용하면, 오타(Munday)나 잘못된 값(Funday)이 입력될 위험이 있다. - 컴파일 시 오류 감지 불가 : 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 떄문에 디버깅이 어려워질 수 있다.
- 값의 제한 부족 :
String이 아닌 상수를 사용하면 어떻게 될까?
클래스 선언
public class StringGrade {
public static final String BASIC = "BASIC";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}등급에 따라 할인 가격을 반환하는 메소드
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;
}문자열 상수를 사용한 덕분에 전체적으로 코드가 더 명확해졌다. 또한 실수로 상수의 이름을 잘못 입력하면 컴파일 시점에 오류가 발생한다. 따라서 오류를 쉽고 빠르게 찾을 수 있다.
하지만 메소드는 String타입이기 때문에 해당 상수를 사용하지 않고 문자열을 사용한다면 컴파일 오류를 막을 수는 없다.
즉 소문자, 문자열, 오타 등 실수 하더라도 근본적 설계 단계에서 에러가 발생하도록 해야한다.
혹은 누군가 주석을 남겨서 사용에 대한 방법을 적어 놓더라도 주석을 잃지 않고 사용할 경우도 생각해서 이러한 문제를 예방해야된다. 즉 제약을 걸어놔야 된다.
지금까지 설명한 문제를 해결하기 위해 나온 결과가 바로 타입 안전 열겨형 패턴이다.
타입 안전 열겨형 패턴을 사용하면 나열한 항목만 사용할 수 있다.(핵심), 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();
private ClassGrade(){}
}static을 사용해서 상수를 메서드 영역에 선언한다. 이렇게 되면 메모리 영역에 선언한 상수가 올라가게 된다.final을 ㅅ용해서 인스턴스(참조값)를 변경할 수 없게 한다.- 각 상수는 모두 서로 다른
ClassGrade인스턴스를 참조하기 때문에 참좌값이 다르게 출력된다.
- 각 상수는 모두 서로 다른
private 생성자()를 활용하여 외부에서 해당 클래스 생성을 막는다.
열겨형 패턴 클래스를 다루는 메소드
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;
}- 메서드 파라미터 타입만 사용이 가능하므로
String이나 임의 클래스 타입은 사용 불가 - 상수로 정의된 것만 사용가능
- 또한 이 값을 비교하기 위해서는
==참조값 비교를 사용하면 된다.(메모리 static 영역)
이를 통해 컴파일 과정에서 오류를 발견할 수 있다.
- 타입 안전 열겨형 패턴 장점
- 타입 안전성 향상 : 정해진 객체만 사용할 수 있기 때문에, 잘못된 값을 입력하는 문제를 근복적으로 방지할 수 있다. (오타 방지)
- 데이터 일관성 : 정해진 객체만 사용하므로 데이터의 일관성이 보장된다. (정해진 값만 사용)
-
장점
- 제한된 인스턴스 생성 : 클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용할 수 있다록 한다. 이를 통해 미리 정의된 값들만 사용하도록 보장된다.
- 사전에 인스턴스 정의 -> 이것만 사용 가능(
컴파일이 보장)
- 사전에 인스턴스 정의 -> 이것만 사용 가능(
- 타입 안정성 : 이 패턴을 사용하면, 잘못된 값이 할당되거나 사용되는 것을 컴파일 시점에 방지할 수 있다. 예를 들어, 특정 메서드가 특정 열거형 타입의 값을 요구한다면, 오직 그 타입의 인스턴스만 전달할 수 있다. 여기서는 메서드의 매개변수로 ClassGrade를 사용하는 경우, 앞서 열거한
BASIC,GOLD,DIAMOND만 사용할 수 있다.
- 제한된 인스턴스 생성 : 클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고, 외부에서는 이 인스턴스들만 사용할 수 있다록 한다. 이를 통해 미리 정의된 값들만 사용하도록 보장된다.
-
단점
- 이 패턴을 구현하려면 많은 코드를 작성해야한다. 그리고 private 생성자를 추가하는 등 유의해야 하는 부분들도 있다.
자바는 타입 안전 열거형 패턴(Type-Safe Enum Pattern)을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을 제공한다.
열거형 클래스
public enum Grade {
BASIC, GOLD, DIAMOND
/*
열거형 값만 있는 경우 세미콜론(;)이 필요없지만
그렇지 않은 경우(다른 로직이 들어가는 경우)에는 반드시 넣어줘야된다.
*/
}ENUM 특징
enum은 자동으로Enum을 상속 받는다.- enum은 제약이 있는 클래스다.
- 전에 제작한 "타입 안전 열거형 패턴" 코드와 똑같은 기능을 한다.
- private 생성ㄹ자로 막혀있다.
- 코드 가독성이 높다.
- 미리 정의한 값들의 집합
- 열거형은
switch문에 사용할 수 있다.
열거형도 클래스이다. 열거형을 제공하기 위해 제약이 추가된 클래스라 생각하면 된다.
enum을 사용한 메서드
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;
}열거형(ENUM)의 장점
- 타입 안전성 향상 : 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성이 없다. 이런 경우 컴파일 오류가 발생한다.
- 간결성 및 일관성 : 열거형을 사용하면 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.
- 확장성 : 새로운 회원 등급을 추가하고 싶을 때, ENUM에 새로운 상수를 추가하기만 하면 된다.
참고 : 열거형을 사용하는 경우 static import를 적절하게 사용하면 더 읽기 좋은 코드를 만들 수 있다.
- values() : 모든 ENUM 상수를 포함하는 배열을 반환한다.
- valueOf(String name): 주어진 이름과 일치하는 ENUM 상수를 반환한다.
- name() : ENUM 상수의 이름을 문자열로 반환한다
- ordinal() : ENUM 상수의 선언 순서(0부터 시작)를 반환한다. -> 가급적 사용하지 말기
- toString() : ENUM 상수의 일므을 문자열로 반환한다. name()메서드와 유사하지만, toString()은 직접 오버라이드 할 수 있다.
중요❗️ : ENUM도 클래스 이기 떄문에 메서드 추가가 가능하다!
- 열거형 정리
- 열거형은
java.lang.Enum을 자동(강제)으로 상속 받는다. - 열거형은 이미
java.lang.Enum을 상속 받았기 때문에 추가로 다른 클래스를 상속을 받을 수 없다. - 열거형은 인터페이스를 구현할 수 있다.
- 열거형에 추상 메서드를 선언하고, 구현할 수 있다.
- 이 경우 익명 클래스와 같은 방식을 사용한다.
- 열거형은
- 실무에서
ENUM을 활용하는 것은 매우 중요하다. - 개발할 때 중요한 것은 "문제가 근본적으로 발생하기 않게 어떻게 할 것이냐"다. -> 이게 중요
- "우리가 조심해야 된다." 이런거는 매우 위험하다. -> 사람이이게 실수할 수 가 있댜.
- 주용한 것은 문제가 생길 것들을 원천 차단하는 것이다.
- String 타입에 숫자를 못넣는것
- 더 나아가서 우리가 나열한 것만 넣을 수 있게 제약하는것
- 훨씬 좋은 개발 방법 : 컴파일 시점에서 오류를 잡는것 -> 타입 안정성
- 좋은 프로그래밍은 제약이 있는 프로그래밍이다. 그중 하나가 "열거형"이다.
사람은 누구나 실수하기 떄문에 원천적으로 이런걸 방지할 수 있는 것들을 잘 배워서 활용하는 것이 더 중요하다.