일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링 mvc2 - 로그인 처리
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch9
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch4
- 스프링 mvc2 - 검증
- 자바 중급1편 - 날짜와 시간
- @Aspect
- 스프링 mvc1 - 서블릿
- 스프링 입문(무료)
- 스프링 mvc1 - 스프링 mvc
- 게시글 목록 api
- 자바의 정석 기초편 ch8
- 2024 정보처리기사 수제비 실기
- 스프링 mvc2 - 타임리프
- 코드로 시작하는 자바 첫걸음
- 2024 정보처리기사 시나공 필기
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch12
- 자바의 정석 기초편 ch7
- 자바 기본편 - 다형성
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch14
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch13
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch6
- Today
- Total
나구리의 개발공부기록
열거형 - ENUM, 문자열과 타입 안정성, 타입 안전 열거형 패턴, 열거형(Enum Type, 주요 메서드, 리펙토링) 본문
열거형 - ENUM, 문자열과 타입 안정성, 타입 안전 열거형 패턴, 열거형(Enum Type, 주요 메서드, 리펙토링)
소소한나구리 2025. 1. 20. 14:38출처 : 인프런 - 김영한의 실전 자바 - 중급1편 (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 문자열과 타입 안정성
1) 문자열과 타입 안정성
(1) 예제 요구사항
- 고객은 3등급으로 나누고 상품 구매시 등급별로 할인을 적용하고 할인 시 소수점 이하는 버림
- BASIC -> 10% 할인
- GOLD -> 20% 할인
- DIAMOND -> 30% 할인
(2) DiscountService
- discount() 메서드는 매개변수로 넘어오는 등급에 따라 할인율을 적용하고 회원 등급 외의 다른 값이 입력이 되면 "할인X"가 출력되고 적용되는 할인율은 없음
- 할인 금액을 구하기 위해 가격 * 할인율 / 100으로 연산하고 그 결과를 반환
- 예제의 단순화를 위해 회원 등급에 null은 입력되지 않는다고 가정
package enumeration.ex0;
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("할인X");
}
return price * discountPercent / 100;
}
}
(3) StringGradeEx0_1
- DiscountService를 호출하여 할인을 적용해보면 회원 등급에 맞게 할인적용이 잘 되었음
- 그러나 이렇게 단순히 문자열을 입력하는 방식은 오타가 발생하기 쉽고 유효하지 않는 값이 입력될 수 있음
package enumeration.ex0;
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
*/
(3) StringGradeEx0_2
- 다음 예제에서는 존재하지 않는 등급을 입력하거나 오타가 발생하여 애플리케이션은 동작하지만 원하는 비즈니스로직이 구현되지 않았음
- 등급에 문자열을 사용하는 방식이기 때문에 타입 안정성이 부족하고 데이터의 일관성이 떨어지는 문제가 발생함
- 타입 안정성 부족: 문자열은 오타가 발생하기 쉽고 유효하지 않은 값이 입력될 수 있음
- 데이터 일관성: "GOLD", "gold" 등 다양한 형식으로 문자열을 입력할 수 있기 때문에 일관성이 떨어짐
package enumeration.ex0;
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);
}
}
/* 실행 결과
할인X
VIP 등급의 할인 금액: 0
할인X
DIAMONDD 등급의 할인 금액: 0
할인X
gold 등급의 할인 금액: 0
*/
(4) String 사용 시 타입 안정성 부족 문제
- 값의 제한 부족: String으로 상태가 카테고리를 표현하면 잘못된 문자열을 실수로 입력할 가능성이 있음
- 컴파일 시 오류 감지 불가: 이러한 잘못된 값은 컴파일 시에 감지되지 않고 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있음
- 이런 문제를 해결하려면 특정 범위로 값을 제한하여 discount()메서드에 전달해야 하는데 String은 어떤 문자열이든 받을 수 있기 때문에 자바 문법 관점에서는 아무런 문제가 없으므로 String 타입을 사용해서는 문제를 해결할 수 없음
2) 문자열 상수를 사용
(1) StringGrade
- 문제를 해결해보기 위해 상수를 사용할 수 있도록 상수를 정의
- 미리 정의한 변수명을 사용하기 때문에 문자열을 직접 사용하는 것 보다는 안전함
package enumeration.ex1;
public class StringGrade {
public static final String BASIC = "BASIC";
public static final String GOLD = "GOLD";
public static final String DIAMOND = "DIAMOND";
}
(2) DiscountService - 수정
- discount()메서드의 if문 조건을 상수와 비교하도록 수정
package enumeration.ex1;
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("할인X");
}
return price * discountPercent / 100;
}
}
(3) StringGradeEx1_1
- discount()메서드를 호출 시 문자열을 직접 입력하는 것이 아니라 StringGrade에 정의된 상수를 인수로 메서드를 호출
- 실행해보면 정상적으로 모든 할인 금액이 출력되는 것을 확인할 수 있음
- 문자열 상수를 사용한 덕분에 전체적으로 코드가 명확해졌고 실수로 상수의 이름을 잘못 입력하면 컴파일 시점에 오류가 발생하기 때문에 오류를 쉽고 빠르게 찾을 수 있음
package enumeration.ex1;
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);
}
}
(4) 문제점
- 그러나 문자열 상수를 사용해도 지금까지 발생한 문제들을 근본적으로 해결할 수는 없음
- String 타입은 어떤 문자열이든 입력할 수 있으므로 다른 개발자가 discount()메서드를 호출할 때 문자열 상수를 사용하지 않고 직접 문자열을 입력해도 막을 수 있는 방법이 없기 때문에 실제로 상수를 정의했다고해도 메서드는 문자열을 입력할 수 있음
- 애초에 discount(String grade, int price)메서드의 매개변수 타입이 String이기 때문에 모든 문자열을 입력받도록 설계한 것 자체가 문제의 근본임
- discount()를 사용하는 개발자는 사용하는 문자열 상수가 어디에있는지 알 수가 없고 주석을 남겨놓는다고해도 못보고 문자열을 입력할 수 있기 때문에 애초에 설계할 때 잘못된 값을 입력하지 못하도록 막아서 설계해야 함
2.타입 안전 열거형 패턴
1) 타입 안전 열거형 패턴 - Type-Safe Enum Pattern
(1) 문제 해결
- 지금까지 설명한 문제를 해결하기 위해 많은 개발자들이 오랜기간 고민하고 나온 결과가 바로 타입 안전 열거형 패턴임
- enum은 enumeration의 줄임말인데 번역하면 '열거'라는 뜻으로 어떤 항목을 나열하는 것을 뜻함
- 여기서 중요한 것은 타입 안전 열거형 패턴을 사용하면 우리가 나열하고자한 항목만 사용할 수 있고 나열하지 않은 항목은 사용할 수 없다는 것이 핵심임
- String처럼 아무런 문자열이나 다 사용할 수 있는 것이 아니라 제약을 두는 것임
(2) ClassGrade
- 회원 등급을 다루는 클래스를 만들고 각각의 회원 등급별로 상수를 선언한 뒤 각각의 상수마다 별도의 인스턴스를 생성 및 생성한 인스턴스의 참조값을 대입함
- 필드를 상수로 선언하기 위해 static으로 메서드 영역에 선언하고 final을 사용하여 참조값을 변경할 수 없도록 작성
package enumeration.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();
}
(3) ClassRefMain
- 위에서 선언한 상수를 더 이해하기 쉽게 getClass()로 클래스 정보를 확인해보면 모두 ClassGrade 타입을 기반으로 만들었기 때문에 모두 ClassGrade로 나옴
- 하지만 각각의 상수는 서로 다른 ClassGrade 인스턴스를 참조하기 때문에 참조값이 모두 다름
- 상수로 열거한 BASIC, GOLD, DIAMOND는 static 이므로 애플리케이션 로딩 시점에 3개의 ClassGrade 인스턴스가 생성되고 각각의 상수는 ClassGrade 타입의 서로다른 인스턴스의 참조값을 가짐
- 이제 ClassGrade 타입을 사용할 때는 열거한 상수들만 사용하면 됨
package enumeration.ex2;
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@3f99bd52
ref GOLD = enumeration.ex2.ClassGrade@4f023edb
ref DIAMOND = enumeration.ex2.ClassGrade@3a71f4dd
*/
(4) DiscountService - 수정
- discount() 메서드를 매개변수로 ClassGrade 클래스를 사용하도록 변경
- 값을 비교할 때는 매개변수에 넘어오는 인수가 ClassGrade가 가진 상수중에 하나를 사용하기 때문에 열거한 상수의 참조값으로 비교하기 위해 == 비교를 사용하면 됨
package enumeration.ex2;
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;
}
}
(5) ClassGradeEx2_1
- discount()를 호출할 때 미리 정의한 ClassGrade의 상수를 전달하면 기존과 할인 금액이 정상적으로 출력되는 것을 확인할 수 있음
- 다른 타입이나 다른 값을 인수로 입력하려고 하면 컴파일 오류가 발생함
package enumeration.ex2;
public class ClassGradeEx2_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);
}
}
(6) ClassGradeEx2_2
- 하지만 이 방식은 외부에서 임의로 ClassGrade의 인스턴스를 생성할 수 있다는 문제가 존재함
- 객체의 상수를 사용해야 하지만 이렇게 객체를 생성할 수 있게 설계하면 이를 설계해서 사용하는 개발자에게는 이 클래스를 생성하면 안된다는 것을 알 수 없으므로 지금처럼 잘못 적용하게 될 수 있기 때문에 애초에 설계를 ClassGrade를 생성할 수 없게 설계해야 함
package enumeration.ex2;
public class ClassGradeEx2_2 {
public static void main(String[] args) {
int price = 10000;
DiscountService discountService = new DiscountService();
ClassGrade newClassGrade = new ClassGrade();
int result = discountService.discount(newClassGrade, price);
System.out.println("newClassGrade 등급의 할인 금액: " + result);
}
}
/* 실행 결과
할인X
newClassGrade 등급의 할인 금액: 0
*/
(6) ClassGrade - 수정
- 기존 ClassGrade 코드에 private 생성자를 추가하여 외부에서 객체를 생성하지 못하도록 막음
- private 생성자 덕분에 ClassGrade의 인스턴스를 생성하는 것은 ClassGrade 클래스 내부에서만 할 수 있기 때문에 상수에 인스턴스를 생성한 참조값을 저장하는 것은 그대로 유지할 수 있음
- 이렇게 하면 기존의 ClassGradeEx2_2에서 new ClassGrade()로 인스턴스를 생성하는 코드에 컴파일 오류가 발생하게 되고 개발자가 실수로 인스턴스를 생성할 수 있는 부분을 원천 차단하게 되고 의도한 값만 입력할 수 밖에 없게됨
package enumeration.ex2;
public class ClassGrade {
// 타입 안전 열거형 패턴 코드는 동일
// private 생성자 추가
private ClassGrade() {
}
}
(7) 타입 안전 열거형 패턴(Type-Safe Enum Pattern)의 장점과 단점
- 타입 안정성 향상: 정해진 객체만 사용할 수 있기 때문에 잘못된 값을 입력하는 문제를 근본적으로 방지할 수 있음
- 데이터 일관성: 정해진 객체만 사용하므로 데이터의 일관성이 보장됨
- 클래스는 사전에 정의된 몇 개의 인스턴스만 생성하고 외부에서는 이 인스턴스들만 사용할 수 있도록 제한하여 미리 정의된 값들만 사용하도록 보장함
- 특정 메서드가 특정 열거형 타입의 값을 요구한다면 오직 그 타입의 인스턴스만 전달할 수 있기 때문에 다른 타입이 들어올 수 없어 매우 안전하며 다른 타입이 들어올 경우 컴파일 오류가 발생하여 빠르게 문제를 파악할 수 있게됨
- 유일한 단점은 코드를 많이 구현해야한다는 것인데 자바가 이를 편리하게 사용할 수 있는 타입을 제공함
3. 열거형
1) Enum Type
(1) Grade - enum
- 자바는 타입 안전 열거형 패턴을 매우 편리하게 사용할 수 있는 열거형(Enum Type)을 제공함
- 자바의 enum은 타입 안정성을 제공하고 코드의 가독성을 높이며 예상 가능한 값들의 집합을 표현하는 데 사용됨
- 열거형을 정의할 때는 class 대신에 enum을 사용하고 원하는 상수의 이름을 나열하기만 하면 되기 때문에 직접 ClassGrade를 구현할 때와 비교가 되지 않을 정도로 편리함
- 지금 작성한 코드는 앞서 직접 만들었던 ClassGrade과 거의 같은 구조로 되어있으므로 상수 하나하나가 각각의 인스턴스임
- 열거형도 클래스이며 자동으로 java.lang.Enum을 상속 받고 외부에서 임의로 생성할 수 없음
package enumeration.ex3;
public enum Grade {
BASIC, GOLD, DIAMOND
}
(2) EnumRefMain
- 실행 결과를 보면 상수들이 열거형으로 선언한 타입인 Grade 타입을 사용하는 것과 각각 다른 인스턴스인 것을 확인할 수 있음
- 열거형은 toString()을 상수값을 출력하도록 재정의 하기 때문에 참조값을 직접 확인할 수 없으므로 refValue()메서드를 만들어서 참조값을 확인함
- enum은 열거형을 제공하기 위해 제약이 추가된 클래스라고 생각하면 됨
package enumeration.ex3;
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 = 3f99bd52
ref GOLD = 4f023edb
ref DIAMOND = 3a71f4dd
*/
(3) 열거형을 사용하여 코드 수정
- 열거형의 사용법은 앞서 타입 안전 열거형 패턴을 직접 구현한 코드와 똑같은 것을 알 수 있음
- 참고로 열거형은 switch문에 사용할 수 있는 장점도 있음
- 열거형은 내부에서 상수로 지정하는 것 외에 직접 생성이 불가능하므로 생성할 경우 컴파일 오류가 발생함
package enumeration.ex3;
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;
}
}
package enumeration.ex3;
public class EnumEx3_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);
}
}
(4) 열거형(ENUM)의 장점 정리
- 타입 안정성: 열거형은 사전에 정의된 상수들로만 구성되므로 유효하지 않은 값이 입력될 가능성이 없으며 이런 경우에는 컴파일 오류가 발생함
- 간결성 및 일관성: 열거형을 사용하면 코드가 더 간결하고 명확해지며 데이터의 일관성이 보장됨
- 확장성: 새로운 회원 등급을 추가하고 싶을 때 ENUM에 새로운 상수를 추가하기만 하면 됨
- 열거형을 사용하는 경우 static import를 적절하게 사용하면 더 읽기 좋은 코드를 만들 수 있음
2) 주요 메서드
(1) EnumMethodMain
- values(): 모든 ENUM 상수를 포함하는 배열을 반환
- valueOf(String name): 주어진 이름과 일치하는 ENUM 상수를 반환
- name(): ENUM 상수의 이름을 문자열로 반환
- ordinal(): ENUM 상수의 선언 순서(0부터 시작)를 반환
- toString(): ENUM 상수의 이름을 문자열로 반환, name() 메서드와 유사하지만 toString()은 직접 오버라이드 할 수 있음
package enumeration.ex3;
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, 잘못된 문자면 IllegalArgumentException 발생
String input = "GOLD";
Grade gold = Grade.valueOf(input);
System.out.println("gold = " + gold);
}
}
/* 실행 결과
values = [BASIC, GOLD, DIAMOND]
name = BASIC, ordinal = 0
name = GOLD, ordinal = 1
name = DIAMOND, ordinal = 2
gold = GOLD
*/
** 주의
- ordinal()은 가급적 사용하지 않는 것이 좋은데 이 값을 사용하다가 중간에 상수를 선언하는 위치가 변경되면 전체 상수의 위치가 모두 변경될 수 있기 때문임
- BASIC, GOLD, DIAMOND의 순서로 ENUM을 만들고 ordinal()값을 데이터베이스나 파일에 저장하고 있다가 중간에 SILVER가 추가되면 데이터베이스나 파일에 있는 값은 그대로 1로 유지되지만 애플리케이션에서는 GOLD는 2가되고 SILVER는 1이 되어버림
- 즉, ordinal()의 값을 사용하면 기존의 GOLD 회원이 갑자기 SILVER가 되는 큰 버그가 발생할 수 있게 됨
(2) 열거형 정리
- java.lang.Enum을 자동(강제)으로 상속 받았기 때문에 추가로 다른 클래스를 상속 받을 수 없음
- 열거형은 인터페이스를 구현할 수 있음
- 열거형에 추상 메서드를 선언하고 구현할 수 있으며 이 경우 익명 클래스와 같은 방식을 사용하는데, 익명 클래스는 뒤에서 다룸
4. 열거형 리펙토링
1) ex2(열거형 패턴 구현) - 리펙토링
(1) 리펙토링 포인트
- 지금은 열거형이 익숙하지 않으니 클래스를 사용하여 열거형 패턴을 구현했던 ex2의 코드를 먼저 리펙토링
- discount()의 너무 많은 if문을 제거하고, 할인율은 각각의 회원 등급별로 판단하므로 ClassGrade가 할인율을 가지고 관리하도록 변경
(2) ClassGrade - 수정
- discountPercent필드와 조회 메서드를 추가
- 생성자를 private으로 하여 외부에서 생성하지 못하도록하고 이 생성자를 통해서 내부에서 할인율을 설정되며 할인율이 중간에 값이 변하지 않도록 불변으로 설계
- 즉 상수를 정의할 때 각각의 등급에 따른 할인율이 정해짐
package enumeration.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;
}
}
(3) DiscountService - 수정
- 기존에 if문을 통해서 회원의 등급을 찾고 등급별로 할인율의 값을 지정하던 코드를 모두 지우고 단순히 할인율 계산 로직만 남게 됨
package enumeration.ref1;
public class DiscountService {
public int discount(ClassGrade classGrade, int price) {
return price * classGrade.getDiscountPercent() / 100;
}
}
(3) ClassGradeRefMain1
- ClassGradeEx2_1의 코드와 클래스 이름만 다르고 구현 코드가 똑같으므로 생략
- 프로그램을 실행해보면 할인 금액이 정상적으로 출력되는 것을 확인할 수 있음
2) 열거형 Grade 리펙토링
(1) Grade - 수정
- discountPercent 필드를 추가하고 생성자를 통해서 필드에 값을 저장
- 열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하므로 생성자에 접근 제어자를 선언할 수 없게 막혀있음(private 이라고 생각하면 됨)
- BASIC(10)처럼 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출됨
- 열거형도 클래스이므로 메서드를 추가할 수 있기 때문에 값을 조회하기 위해 getDiscountPercent()메서드를 추가함
- 바로 위에서 ClassGrade를 수정한 것과 비교하면 생성자에 인수를 입력하는 방법만 조금 다르고 나머지는 동일함
package enumeration.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;
}
}
(2) DiscountService - 수정
- 마찬가지로 if문이 제거되고 단순히 할인율 계산 로직만 남았음
package enumeration.ref2;
public class DiscountService {
public int discount(Grade grade, int price) {
return price * grade.getDiscountPercent() / 100;
}
}
(3) EnumRefMain2
- EnumEx3_1의 코드와 클래스 이름만 다르고 구현 코드가 똑같으므로 생략
- 마찬가지로 프로그램을 실행해보면 정상적으로 할인 금액이 출력됨
3) 할인율 계산 리펙토링
(1) 객체지향 관점 리펙토링
- 위에서 리펙토링된 DiscountService 코드를 보면 단순히 할인율을 계산하는 로직만 남아있지만 할인율 계산을 위해 Grade가 가지고 있는 데이터인 discountPercent의 값을 꺼내서 사용하고 있음
- 결국 Grade의 데이터인 discountPercent를 할인율 계산에 사용하고 있음
- 객체지향 관점에서 이렇게 자신의 데이터를 외부에 노출하는 것 보다는 Grade 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 알맞음
(2) Grade - 수정
- Grade 내부에 discount()메서드를 만들어 할인율을 바탕으로 할인금액을 Grade 스스로 계산하도록 변경
package enumeration.ref3;
public enum Grade {
// 기존 코드 동일 생략
// 추가
public int discount(int price) {
return price * discountPercent / 100;
}
}
(3) DiscountService - 수정
- 할인율 계산 자체를 Grade에서 하기 때문에 이제 DiscountServie에서는 discount()메서드를 호출하여 할인금액만 반환하면 됨
package enumeration.ref3;
public class DiscountService {
public int discount(Grade grade, int price) {
return grade.discount(price);
}
}
(4) EnumRefMain3_1
- EnumRefMain2와 클래스 이름만 변경되고 동일한 코드이므로 코드는 생략
- 프로그램을 실행하면 정상적으로 원하는 할인금액이 출력됨
(5) EnumRefMain3_2 - DiscountService 제거
- Grade가 스스로 할인율을 계산하면서 사실상 DiscountService는 값만 반환하는 로직만 존재할 뿐이므로 사실상 필요가 없음
- DiscountService를 완전히 제거하고 Grade에서 직접 할인금액을 계산하는 discount()를 직접 불러오도록 변경할 수 있음
- 실행해보면 정상적으로 등급별 할인율이 출력됨
package enumeration.ref3;
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));
}
}
(6) EnumRefMain3_3 - 출력 반복 제거
- 출력문에서 등급 부분만 다르고 나머지는 모두 동일하므로 등급과, 가격을 매개변수로 하는 printDiscount()메서드를 생성
- name()메서드로 ENUM상수의 이름을 꺼내수 있으므로 이를 활용하여 출력문을 완성
- 해당 메서드에 등급과 가격만 인수로 입력해주면 등급에 따른 할인금액이 출력됨
package enumeration.ref3;
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));
}
}
(7) EnumRefMain3_4 - 메서드 호출 변경
- ENUM에 새로운 등급이 추가되더라고 main() 코드의 변경이 없이 모든 등급의 할인이 출력되도록 변경
- values()를 통해 Enum의 모든 상수를 배열로 반환할 수 있으므로 반복문을 통해 메서드를 호출하면 됨
- Grade에 새로운 등급과 생성자에 할인율을 지정해보면 main()코드없이 할인금액이 반영되는 것을 확인할 수 있음
package enumeration.ref3;
public class EnumRefMain3_4 {
public static void main(String[] args) {
int price = 10000;
for (Grade grade : Grade.values()) {
printDiscount(grade, price);
}
}
private static void printDiscount(Grade grade, int price) {
System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
}
}
/* 실행 결과
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
MASTER 등급의 할인 금액: 4000
CHALLENGER 등급의 할인 금액: 5000
*/
5. 문제와 풀이
1) 인증 등급 만들기
(1) 문제 설명
- 회원의 인증 등급을 AuthGrade라는 이름의 열거형으로 생성
- 인증 등급은 3가지이며 인증 등급에 따른 레벨과 설명을 가짐
- 레벨과 설명을 getXxx()메서드로 조회할 수 있어야 함
GUEST(손님)
- level=1
- description=손님
LOGIN(로그인 회원)
- level=2
- description=로그인 회원
ADMIN(관리자)
- level=3
- description=관리자
(2) 정답
package enumeration.test.ex1;
public enum AuthGrade {
GUEST(1, "손님"),
LOGIN(2, "로그인 회원"),
ADMIN(3, "관리자");
private final int level;
private final String description;
AuthGrade(int level, String description) {
this.level = level;
this.description = description;
}
public int getLevel() {
return level;
}
public String getDescription() {
return description;
}
}
2) 인증 등급 열거형 조회하기
(1) 문제 설명
- 앞서 만든 AuthGrade을 활용하여 AuthGradeMain1이라는 클래스를 만들고 다음 결과가 출력되도록 코드를 작성
grade=GUEST, level=1, 설명=손님
grade=LOGIN, level=2, 설명=로그인 회원
grade=ADMIN, level=3, 설명=관리자
(2) 정답
package enumeration.test.ex1;
public class AuthGradeMain1 {
public static void main(String[] args) {
AuthGrade[] authGrades = AuthGrade.values();
for (AuthGrade authGrade : authGrades) {
System.out.println("grade=" + authGrade.name() +
", level=" + authGrade.getLevel() +
", 설명=" + authGrade.getDescription());
}
}
}
3) 인증 등급 열거형 활용
(1) 문제 설명
- AuthGradeMain2 클래스에 코드를 작성
- 인증 등급을 입력 받은 후 AuthGrade 열거형으로 변환
- 인증 등급에 따라 접근할 수 있는 화면이 다르며 각각의 등급에 따라서 출력되는 메뉴 목록이 달라짐
- GUEST는 메인 화면만 접근, ADMIN 등급은 모든 화면에 접근 - 출력 결과를 참고하여 코드들 완성
GUEST 입력 예
당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: GUEST
당신의 등급은 손님입니다.
==메뉴 목록==
- 메인 화면
LOGIN 입력 예
당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: LOGIN
당신의 등급은 로그인 회원입니다.
==메뉴 목록==
- 메인 화면
- 이메일 관리 화면
ADMIN 입력 예
당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: ADMIN
당신의 등급은 관리자입니다.
==메뉴 목록==
- 메인 화면
- 이메일 관리 화면
- 관리자 화면
잘못된 값이 입력 되는 경우 - 에러 발생
당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: x
Exception in thread "main" java.lang.IllegalArgumentException: No enum constant
enumeration.test.AuthGrade.X
at java.base/java.lang.Enum.valueOf(Enum.java:293)
at enumeration.test.AuthGrade.valueOf(AuthGrade.java:3)
at enumeration.test.AuthGradeMain2.main(AuthGradeMain2.java:12)
** 참고
- Enum.valueOf()를 사용할 때 잘못된 값이 입력되면 IllegalArgumentException예외가 발생하는데 예외를 잡아서 복구하는 방법은 이후 예외처리에서 학습함
(2) 정답
소문자로 입력하여도 정상적으로 동작하도록 설정
package enumeration.test.ex1;
import java.util.Scanner;
public class AuthGradeMain2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("당신의 등급을 입력하세요[GUEST, LOGIN, ADMIN]: ");
String grade = sc.nextLine().toUpperCase();
AuthGrade authGrade = AuthGrade.valueOf(grade);
System.out.println("당신의 등급은 " + authGrade.getDescription() + "입니다.");
printMenu(authGrade);
}
private static void printMenu(AuthGrade authGrade) {
System.out.println("==메뉴 목록==");
if (authGrade.getLevel() > 0) {
System.out.println("- 메인 화면");
}
if (authGrade.getLevel() > 1) {
System.out.println("- 이메일 관리 화면");
}
if (authGrade.getLevel() > 2) {
System.out.println("- 관리자 화면");
}
}
}
4) HTTP 상태 코드 정의
(1) 문제 설명
- enumeration.test.http 패키지를 사용하고 HttpStatus 열거형을 생성
- HTTP 상태 코드 정의
(1) OK
- code: 200
- message: "OK"
(2) BAD_REQUEST
- code: 400
- message: "Bad Request"
(3) NOT_FOUND
- code: 404
- message: "Not Found"
(4) INTERNAL_SERVER_ERROR
- code: 500
- message: "Internal Server Error"
** 참고
- HTTP 상태코드는 200 ~ 299 사이의 숫자를 성공으로 인정함
- HttpStatusMain 코드와 실행 결과를 참고하여 HttpStatus 열거형을 완성
HttpStatus
package enumeration.test.http;
public enum HttpStatus {
// 코드 작성
}
HttpStatusMain
package enumeration.test.http;
import java.util.Scanner;
public class HttpStatusMain {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("HTTP CODE: ");
int httpCodeInput = scanner.nextInt();
HttpStatus status = HttpStatus.findByCode(httpCodeInput);
if (status == null) {
System.out.println("정의되지 않은 코드");
} else {
System.out.println(status.getCode() + " " + status.getMessage());
System.out.println("isSuccess = " + status.isSuccess());
}
}
}
실행 결과 - 200
HTTP CODE: 200
200 OK
isSuccess = true
실행 결과 - 400
HTTP CODE: 400
400 Bad Request
isSuccess = false
실행 결과 - 404
HTTP CODE: 404
404 Not Found
isSuccess = false
실행 결과 - 500
HTTP CODE: 500
500 Internal Server Error
isSuccess = false
(2) 정답
package enumeration.test.http;
public enum HttpStatus {
OK(200, "OK"),
BAD_REQUEST(400, "Bad Request"),
NOT_FOUND(404, "Not Found"),
INTERNAL_SERVER_ERROR(500, "Internal Server Error"),
;
private final int code;
private final String message;
HttpStatus(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public boolean isSuccess() {
return code >= 200 && code <= 299;
}
public static HttpStatus findByCode(int code) {
for (HttpStatus status : values()) {
if (status.getCode() == code) {
return status;
}
}
return null;
}
}
5) 정리
(1) 문제를 원천 차단
- '조심해야한다' 라는것은 누구나 개발을 할 때 조심하지만 사람이기 때문에 실수할 수 있기 때문에 위험할 수 있음
- 제약을 두어 실무에서 개발할 때 문제를 원천적으로 차단하게 설계하는 것이 훨신 더 좋은 개발 방법이며 좋은 프로그램을 설계하는 것임
- 이런 좋은 제약을 두는 대표적인 케이스가 ENUM이며 반복되는 문제나 실수등을 이런 좋은 장치들을 잘 활용하는 것이 중요함