Java 공부 44일차(열거형 - ENUM이란?)1편

임선구·2025년 3월 25일

몸 비틀며 Java

목록 보기
45/58

오늘의 잔디


오늘의 공부
와 개강하고 너무 바쁘다 학회라 이것저것 하느라 시간이 없다.
내일은 또 mt다.
하지만 꾸준히 코딩 하고 있다.
이제 학교 수업시간에도 이해가 가기 시작한다.
앞으로도 이렇게 느리더라도 꾸준히 해야겠다.
인턴도 알아봐야겠다.


문자열과 타입 안전성1

자바가 제공하는 열거형(Enum Type)을 제대로 이해하려면 먼저 열거형이 생겨난 이유를 알아야 한다. 예제를 순서대로 따라가며 열거형이 만들어진 근본적인 이유를 알아보자.

비즈니스 요구사항

고객은 3등급으로 나누고, 상품 구매시 등급별로 할인을 적용한다. 할인시 소수점 이하는 버린다.

  • BASIC -> 10% 할인
  • GOLD -> 20% 할인
  • DIAMOND -> 30% 할인

예) GOLD 유저가 10000원을 구매하면 할인 대상 금액은 2000원이다.

예제를 구현해보자.
회원 등급과 가격을 입력하면 할인 금액을 계산해주는 클래스를 만들어보자.
예를 들어서 GOLD , 10000원을 입력하면 할인 대상 금액인 2000원을 반환한다.

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(grade + ": 할인X");
 }
 return price * discountPercent / 100;
 }
}
  • price * discountPercent / 100 : 가격 * 할인율 / 100 을 계산하면 할인 금액을 구할 수 있다.
  • 회원 등급 외 다른 값이 입력되면 할인X 를 출력한다. 이 경우 discountPercent0 이므로 할인 금액도 0원으로 계산된다.
  • 예제를 단순화하기 위해 회원 등급에 null 은 입력되지 않는다고 가정한다.
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

실행 결과를 보면 각각의 회원 등급에 맞는 할인이 적용된 것을 확인할 수 있다.

그런데 지금과 같이 단순히 문자열을 입력하는 방식은, 오타가 발생하기 쉽고, 유효하지 않는 값이 입력될 수 있다.
다음 예를 보자.

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);
 }
}

실행 결과

VIP 등급의 할인 금액: 0
DIAMONDD: 할인X
DIAMONDD 등급의 할인 금액: 0
gold: 할인X
gold 등급의 할인 금액: 0

예제에서는 다음과 같은 문제가 발생했다.

  • 존재하지 않는 VIP라는 등급을 입력했다.
  • 오타: DIAMOND 마지막에 D가 하나 추가되었다.
  • 소문자 입력: 등급은 모두 대문자인데, 소문자를 입력했다.

등급에 문자열을 사용하는 지금의 방식은 다음과 같은 문제가 있다.

  • 타입 안정성 부족: 문자열은 오타가 발생하기 쉽고, 유효하지 않은 값이 입력될 수 있다.
  • 데이터 일관성: "GOLD", "gold", "Gold" 등 다양한 형식으로 문자열을 입력할 수 있어 일관성이 떨어진다.

String 사용 시 타입 안정성 부족 문제

  • 값의 제한 부족: String 으로 상태나 카테고리를 표현하면, 잘못된 문자열을 실수로 입력할 가능성이 있다. 예를 들어, "Monday", "Tuesday" 등을 나타내는 데 String 을 사용한다면, 오타("Munday")나 잘못된 값 ("Funday")이 입력될 위험이 있다.
  • 컴파일 시 오류 감지 불가: 이러한 잘못된 값은 컴파일 시에는 감지되지 않고, 런타임에서만 문제가 발견되기 때문에 디버깅이 어려워질 수 있다.

이런 문제를 해결하려면 특정 범위로 값을 제한해야 한다. 예를 들어 BASIC , GOLD , DIAMOND 라는 정확한 문자만 discount() 메서드에 전달되어야 한다. 하지만 String 은 어떤 문자열이든 받을 수 있기 때문에 자바 문법 관점에서는 아무런 문제가 없다. 결국 String 타입을 사용해서는 문제를 해결할 수 없다.

문자열과 타입 안전성2

이번에는 대안으로 문자열 상수를 사용해보자. 상수는 미리 정의한 변수명을 사용할 수 있기 때문에 문자열을 직접 사용
하는 것 보다는 더 안전하다.

package enumeration.ex1;
public class StringGrade {
 public static final String BASIC = "BASIC";
 public static final String GOLD = "GOLD";
 public static final String DIAMOND = "DIAMOND";
}
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(grade + ": 할인X");
 }
 return price * discountPercent / 100;
 }
}
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);
 }
}

실행 결과

BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000

문자열 상수를 사용한 덕분에 전체적으로 코드가 더 명확해졌다. 그리고 discount() 에 인자를 전달할 때도
StringGrade 가 제공하는 문자열 상수를 사용하면 된다. 더 좋은 점은 만약 실수로 상수의 이름을 잘못 입력하면 컴파일 시점에 오류가 발생한다는 점이다. 따라서 오류를 쉽고 빠르게 찾을 수 있다.

하지만 문자열 상수를 사용해도, 지금까지 발생한 문제들을 근본적으로 해결할 수 는 없다. 왜냐하면 String 타입은
어떤 문자열이든 입력할 수 있기 때문이다. 어떤 개발자가 실수로 StringGrade 에 있는 문자열 상수를 사용하지 않고, 다음과 같이 직접 문자열을 사용해도 막을 수 있는 방법이 없다.

package enumeration.ex1;
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);
 }
}

실행 결과

VIP: 할인X
VIP 등급의 할인 금액: 0
DIAMONDD: 할인X
DIAMONDD 등급의 할인 금액: 0
gold: 할인X
gold 등급의 할인 금액: 0

그리고 사용해야 하는 문자열 상수가 어디에 있는지 discount() 를 호출하는 개발자가 어떻게 알 수 있을까?
다음 코드를 보면 분명 String 은 다 입력할 수 있다고 되어있다.

public int discount(String grade, int price) {}

결국 누군가 주석을 잘 남겨두어서, StringGrade 에 있는 상수를 사용해달라고 해야 한다. 물론 이렇게 해도 누군가는 주석을 깜박하고 문자열을 직접 입력할 수 있다.

타입 안전 열거형 패턴

타입 안전 열거형 패턴 - Type-Safe Enum Pattern
지금까지 설명한 문제를 해결하기 위해 많은 개발자들이 오랜기간 고민하고 나온 결과가 바로 타입 안전 열거형 패턴이
다.
여기서 영어인 enumenumeration 의 줄임말인데, 번역하면 열거라는 뜻이고, 어떤 항목을 나열하는 것을 뜻한
다. 우리의 경우 회원 등급인 BASIC , GOLD , DIAMOND 를 나열하는 것이다. 여기서 중요한 것은 타입 안전 열거형 패턴을 사용하면 이렇게 나열한 항목만 사용할 수 있다는 것이 핵심이다. 나열한 항목이 아닌 것은 사용할 수 없다.
쉽게 이야기해서 앞서본 String 처럼 아무런 문자열이나 다 사용할 수 있는 것이 아니라, 우리가 나열한 항목인
BASIC , GOLD , DIAMOND 만 안전하게 사용할 수 있다는 것이다.

타입 안전 열거형 패턴을 직접 구현해보자.

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();
}
  • 먼저 회원 등급을 다루는 클래스를 만들고, 각각의 회원 등급별로 상수를 선언한다.
  • 이때 각각의 상수마다 별도의 인스턴스를 생성하고, 생성한 인스턴스를 대입한다.
  • 각각을 상수로 선언하기 위해 static , final 을 사용한다.
    • static 을 사용해서 상수를 메서드 영역에 선언한다.
    • final 을 사용해서 인스턴스(참조값)를 변경할 수 없게 한다.

      이 코드를 확실히 이해하기 위해 먼저 다음 코드를 실행해보자.
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@x001
ref GOLD = enumeration.ex2.ClassGrade@x002
ref DIAMOND = enumeration.ex2.ClassGrade@x003
  • 각각의 상수는 모두 ClassGrade 타입을 기반으로 인스턴스를 만들었기 때문에 getClass() 의 결과는 모두
    ClassGrade 이다.
  • 각각의 상수는 모두 서로 각각 다른 ClassGrade 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.

static 이므로 애플리케이션 로딩 시점에 다음과 같이 3개의 ClassGrade 인스턴스가 생성되고, 각각의 상수는 같은 ClassGrade 타입의 서로 다른 인스턴스의 참조값을 가진다.

  • ClassGrade BASIC : x001
  • ClassGrade GOLD : x002
  • ClassGrade DIAMOND : x003

여기서 BASIC , GOLD , DIAMOND 를 상수로 열거했다. 이제 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;
 }
}
  • discount() 메서드는 매개변수로 ClassGrade 클래스를 사용한다.
  • 값을 비교할 때는 classGrade == ClassGrade.BASIC 와 같이 == 참조값 비교를 사용하면 된다.
    • 매개변수에 넘어오는 인수도 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);
 }
}
  • discount() 를 호출할 때 미리 정의한 ClassGrade 의 상수를 전달한다.

실행 결과

BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000

private 생성자

그런데 이 방식은 외부에서 임의로 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(); //생성자 private으로 막아야 함
 int result = discountService.discount(newClassGrade, price);
 System.out.println("newClassGrade 등급의 할인 금액: " + result);
 }
}

실행 결과

할인X
newClassGrade 등급의 할인 금액: 0

이 문제를 해결하려면 외부에서 ClassGrade 를 생성할 수 없도록 막으면 된다. 기본 생성자를 private 으로 변경하자.

코드 변경

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();
 //private 생성자 추가
 private 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(); //생성자 private으로 막아야 함
 int result = discountService.discount(newClassGrade, price);
 System.out.println("newClassGrade 등급의 할인 금액: " + result);
*/
 }
}
  • 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 생성자 추가
 private ClassGrade() {}
}
profile
끝까지 가면 내가 다 이겨

0개의 댓글