주제 : 김영한님의 자바 중급 1편 총 정리
내용 : 열거형 ENUM에 대해 공부
- 자바가 제공하는 열거형을 제대로 이해하려면 먼저 열거형이 생겨난 이유를 알아야 한다.
비즈니스 요구사항
고객은 3등급으로 나누고, 상품 구매시 등급별로 할인율을 적용한다.
할인시, 소수점 이하는 버린다.
BASIC10% 할인GOLD20% 할인DIAMOND30% 할인ex)
GOLD유저가 10000원을 구매하면 할인 대상 금액은 2000원이다.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_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); } }
- 등급에 문자열을 사용하는 지금의 방식은 다음과 같은 문제가 있다.
- 타입 안정성 부족 : 문자열은 오타가 발생하기 쉽고, 유효하지 않은 값이 입력될 수 있다.
- 데이터 일관성 :
GOLD,gold,Gold등 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다.
String 사용 시 타입 안정성 부족 문제
String으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다. 예를들어, Monday, Tuesday 등을 나타내는 데 String 을 사용한다면, 오타(Munday)나 잘못된 값 (Funday)이 입력될 위험이 있다.이런 문제를 해결하려면 특정 범위로 값을 제한해야 한다. 예를 들어 BASIC , GOLD , DIAMOND 라는 정확한 문자만 discount() 메서드에 전달되어야 한다. 하지만 String 은 어떤 문자열이든 받을 수 있기 때문에 자바 문법 관점에 서는 아무런 문제가 없다. 결국 String 타입을 사용해서는 문제를 해결할 수 없다.
public class ClassGrade { 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; } }
- 문자열 상수를 사용한 덕분에 전체적으로 코드가 더 명확해졌다. 그리고
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); } }
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); } }
각각의 상수는 모두
ClassGrade타입을 기반으로 인스턴스를 만들었기 때문에getClass()의 결과는 모두ClassGrade이다.각각의 상수는 모두 서로 각각 다른
ClassGrade인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.
static이므로 애플리케이션 로딩 시점에 다음과 같이 3개ClassGrade인스턴스가 생성되고, 각각의 상수는 같은ClassGrade타입의 서로 다른 인스턴스의 참조값을 가진다.
ClassGrade BASIC:x001ClassGrade GOLD:x002ClassGrade DIAMOND:x003
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; } }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); } }
- 그런데 이 방식은 외부에서 임의로
ClassGrade의 인스턴스를 생성할 수 있다는 문제가 있다.- 이 문제를 해결하려면 외부에서
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() {} }코드 변경
package enumeration.ex2; public class ClassGradeEx2_2 { public static void main(String[] args) { int price = 10000; DiscountService discountService = new DiscountService(); } }
- 이렇게
private생성자를 사용해서 외부에서ClassGrade를 임의로 생성하지 못하게 막고 타입 안전 열거형 패턴을 완성할 수 있었다.- 하지만, 이 패턴을 구현하려면 다음과 같이 많은 코드를 작성해야 하며,
private생성자를 추가하는 등 유의해야 한는 부분들이 많다.
자바는 타입 안전 열거형 패턴 (Type-Safe Enum Pattern)을 매우 편리하게 사용할 수 있는 열거형(Enum Type) 을 제공한다.
Enumeration은 일련의 명명된 상수들의 집합을 정의하는 것을 의미하며, 프로그래밍에서는 이러한 상수들을 사용하여 코드 내에서 미리 정의된 값들의 집합을 나타낸다.- 쉽게 이야기해서 회원의 등급은 상수로 정의한다. 즉,
BASIC,GOLD,DIAMOND만 사용할 수 있다는 뜻이다.- 자바의
enum은 타입 안전성을 제공하고, 코드의 가독성을 높이며, 예상 가능한 값들의 집합을 표현하는 데 사용된다.
public enum Grade { BASIC, GOLD, DIAMOND }
class 대신에 enum을 사용한다.앞서 직접 ClassGrade를 구현할 때와는 비교가 되지 않을 정도로 편리하다.
자바의 열거형으로 작성한 Grade는 다음 코드와 거의 같다.
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( ) { } }
- 열거형도 클래스이다.
- 열거형은 자동으로
java.lang.Enum을 상속 받는다.- 외부에서 임의로 생성할 수 없다.
자바의 열거형을 사용해서 코드를 작성해보자.
public class DiscountService { public int discount(Grade grade, int price) { int discountPercent = 0; //enum switch 변경 가능 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 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); } }
실행 결과
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
switch 문에 사용할 수 있는 장점도 있다. 열거형은 외부 생성 불가
public class EnumEx3_2 { public static void main(String[] args) { int price = 10000; DiscountService discountService = new DiscountService(); /* Grade myGrade = new Grade(); //enum 생성 불가 double result = discountService.discount(myGrade, price); System.out.println("result price: " + result); */ } }
enum은 열거형 내부에서 상수로 지정하는 것 외에 직접 생성이 불가능하다. 생성할 경우 컴파일 오류가 발생한다.enum classes may not be instantiated열거형(ENUM)의 장점
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()메서드를 추가했다. 열거형도 클래스이므로 메서드를 추가할 수 있다.
public class DiscountService { public int discount(Grade grade, int price) { return price * grade.getDiscountPercent() / 100; } }
- 기존에 있던
if문이 완전히 제거되고, 단순한 할인율 계산 로직만 남았다.
public class EnumRefMain2 { 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); } }
실행 결과
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
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 { 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); } }
실행 결과
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
Grade 가 스스로 할인율을 계산하면서 DiscountService 클래스가 더는 필요하지 않게 되었다.
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()를 직접 호출하면 할인율을 구할 수 있다.
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의 상수 이름을 사용할 수 있다.
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)); } }
실행 결과
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
Grade.values()를 사용하면Grade열거형의 모든 상수를 배열로 구할 수 있다.