"김영한의 실전 자바 - 중급편" 내용을 참고하여 정리함.
목차
1. 문자열과 타입 안전성
자바에서 열거형(Enum Type)이 등장한 이유는 문자열을 이용한 값 표현의 한계 때문이다.
기본적으로 String을 이용해 특정 상태(등급, 카테고리 등)를 나타낼 수 있지만, 다음과 같은 문제가 발생한다.
문자열을 사용한 방식 & 문제점
회원 등급에 따라 할인을 적용하는 시스템이다.
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;
}
}
문제점
- 오타 발생 가능: "GOLD" 대신 "gold"로 입력하면 정상적으로 동작하지 않는다.
- 잘못된 값 입력 가능: "VIP" 같은 존재하지 않는 값도 전달할 수 있다.
- 컴파일 시 오류 감지 불가: 문자열 값이 유효한지 여부는 런타임에서만 확인 가능하다.
- 데이터 일관성 부족: "GOLD", "gold", "Gold" 등 서로 다른 표기법이 가능하다.
이러한 문제를 해결하기 위해 문자열 대신 상수(constant)를 사용하는 방법이 등장했다.
문자열 상수를 사용한 방식 & 문제점
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;
}
}
장점
- 코드 가독성 증가: 직접 "GOLD" 같은 문자열을 입력하지 않고, `StringGrade.GOLD` 와 같이 상수를 사용할 수 있다.
- 컴파일 시 오류 감지 가능: 잘못된 상수명을 입력하면 컴파일 오류가 발생한다.
여전히 해결되지 않는 문제
- `String` 타입을 사용하기 때문에 여전히 어떤 문자열이든 입력할 수 있다.
- 개발자가 `StringGrade` 의 상수를 사용해야 한다는 것을 강제할 수 없다.
- 문자열 비교(`equals()`)를 해야 하는 불편함이 있다.
이 문제를 해결하기 위해 타입 안전 열거형 패턴이 등장했다.
2. 타입 안전 열거형 패턴 (Type-Safe Enum Pattern)
문자열과 문자열 상수를 사용한 방법의 문제점을 해결하기 위해 등장한 방식이다.
이 패턴에서는 미리 정의된 객체만 사용하도록 강제하여 타입 안정성을 확보한다.
타입 안전 열거형 패턴 구현
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` 을 사용한다.
- `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@x001
ref GOLD = enumeration.ex2.ClassGrade@x002
ref DIAMOND = enumeration.ex2.ClassGrade@x003
- 각각의 상수는 모두 `ClassGrade` 타입을 기반으로 인스턴스를 만들었기 때문에 `getClass()` 의 결과는 모두 `ClassGrade` 이다.
- 각각의 상수는 모두 서로 각각 다른 `ClassGrade` 인스턴스를 참조하기 때문에 참조값이 다르게 출력된다.
개선된 DiscountService
public class DiscountService {
public int discount(ClassGrade grade, int price) {
int discountPercent = 0;
if (grade == ClassGrade.BASIC) discountPercent = 10;
else if (grade == ClassGrade.GOLD) discountPercent = 20;
else if (grade == 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);
}
}
- `discount()` 를 호출할 때 미리 정의한 `ClassGrade` 의 상수를 전달한다.
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
장점
- 타입 안정성 보장: `ClassGrade` 타입만 전달할 수 있어 잘못된 값 입력을 컴파일 시점에 방지할 수 있다.
- 제한된 인스턴스 생성: 사전에 정의된 몇 개의 인스턴스만 생성하여, 미리 정의된 값들만 사용하도록 보장한다.
- 컴파일 타임 오류 감지 가능: 잘못된 타입이 전달되면 컴파일 오류 발생.
- 일관성 유지: BASIC, GOLD, DIAMOND 외의 값은 사용할 수 없음.
단점
- 구현이 번거로움: `private` 생성자와 인스턴스 상수를 직접 정의해야 한다.
- 클래스 코드가 많아짐: 단순한 값 표현을 위해 많은 코드가 필요하다.
이 단점을 해결하기 위해 자바에서는 열거형(Enum Type)을 도입했다.
3. 열거형 (Enum Type)
자바는 타입 안전 열거형 패턴(Type-Safe Enum Pattern)을 간결하게 사용할 수 있도록 열거형(Enum Type)을 제공한다.
이는 특정 값들의 집합을 정의할 때, 기존의 `final static` 상수를 사용하는 방식보다 안전하고 가독성이 뛰어난 방식을 제공한다.
public enum Grade {
BASIC, GOLD, DIAMOND
}
- `class` 대신 `enum` 키워드를 사용하여 정의한다.
- `Grade.BASIC`, `Grade.GOLD`, `Grade.DIAMOND` 처럼 사용 가능하다.
열거형 특징 및 내부 구조 분석
- 자동으로 `java.lang.Enum`을 상속받는다.
- 내부적으로 `Enum<Grade>`를 상속하며, 추가적인 클래스를 상속할 수 없다.
- 각각의 상수는 독립적인 객체로 싱글톤 패턴이 적용된다.
- 외부에서 `new` 를 사용하여 인스턴스를 생성할 수 없다.
- 생성할 경우 컴파일 오류가 발생: `enum classes may not be instantiated`
자바의 열거형은 Enum을 자동 상속하는 특수한 형태의 클래스이다.
따라서, 다음 코드와 거의 같다.
public class Grade 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 Grade() {}
}
실행 코드 - 열거형 값 출력
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) {
// System.identityHashCode(grade) : 자바가 관리하는 객체의 참조값을 숫자로 반환한다.
// Integer.toHexString() : 숫자를 16진수로 변환, 우리가 일반적으로 확인하는 참조값은 16진수
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 classGrade, int price) {
int discountPercent = 0;
// switch문 사용 가능
if (classGrade == BASIC) {
discountPercent = 10;
} else if (classGrade == GOLD) {
discountPercent = 20;
} else if (classGrade == DIAMOND) {
discountPercent = 30;
} else {
System.out.println("할인X");
}
return price * discountPercent / 100; // 가격 * 할인율 / 100
}
}
실행 코드
public class ClassGradeEx3_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
- 열거형의 사용법이 앞서 타입 안전 열거형 패턴을 직접 구현한 코드와 같은 것을 확인 할 수 있다.
열거형의 장점
- 타입 안정성 향상: 열거형은 사전에 정의된 상수들로만 구성되므로, 유효하지 않은 값이 입력될 가능성이 없다. 이런 경우 컴파일 오류가 발생한다.
- 간결성 및 일관성: 열거형을 사용하면 코드가 더 간결하고 명확해지며, 데이터의 일관성이 보장된다.
- 확장성: 새로운 회원 등급을 타입을 추가하고 싶을 때, ENUM에 새로운 상수를 추가하기만 하면 된다.
참고: 열거형을 사용하는 경우 `static import` 를 적절하게 사용하면 더 읽기 좋은 코드를 만들 수 있다.
4. 열거형 - 주요 메서드
자바의 모든 열거형(Enum)은 `java.lang.Enum`을 자동으로 상속받는다. 따라서 Enum 클래스에서 제공하는 주요 메서드를 활용할 수 있다.
주요 메서드
1. values()
- 열거형의 모든 상수를 배열로 반환한다.
Grade[] values = Grade.values();
System.out.println(Arrays.toString(values));
2. valueOf(String name)
- 문자열을 해당하는 ENUM 상수로 변환한다.
- 존재하지 않는 이름이면 `IllegalArgumentException` 예외가 발생한다.
Grade gold = Grade.valueOf("GOLD");
- ENUM 상수의 이름을 문자열로 반환한다.
System.out.println(Grade.GOLD.name()); // "GOLD"
- ENUM 상수의 선언 순서를 0부터 반환한다.
- 하지만 중간에 새로운 상수가 추가될 경우 기존 값들의 순서가 변경될 수 있으므로 `ordinal()` 사용은 지양하는 것이 좋다.
5. toString()
- ENUM 상수의 이름을 문자열로 반환한다.
- `name()` 과 유사하지만 `toString()` 은 직접 오버라이딩이 가능하다.
주의사항
- `ordinal()` 은 열거형 상수의 순서를 기반으로 값이 변경될 가능성이 있기 때문에 데이터 저장 목적으로 사용하면 안 된다.
- `valueOf()` 를 사용할 때는 반드시 존재하는 ENUM 상수인지 확인 후 호출해야 한다.
열거형 정리
- 열거형은 `java.lang.Enum` 를 자동(강제)으로 상속 받는다.
- 열거형은 이미 `java.lang.Enum` 을 상속 받았기 때문에 추가로 다른 클래스를 상속을 받을 수 없다.
- 열거형은 인터페이스를 구현할 수 있다.
- 열거형에 추상 메서드를 선언하고, 구현할 수 있다.
- 이 경우 익명 클래스와 같은 방식을 사용한다.
5. 열거형 - 리팩토링1
기존 코드에서는 `if` 문을 사용하여 할인율을 계산하고 있었다. 이를 개선하기 위해 할인율 정보를 ENUM 내부에서 직접 관리하도록 리팩토링한다.
기존 코드 - DiscountService.discount()
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");
}
- `if` 문을 사용하면 코드가 길어지고 유지보수가 어려워진다.
- 위 코드에서 할인율(`discountPercent`)은 각각의 회원 등급별로 판단된다.
따라서 회원 등급 클래스가 할인율 (`discountPercent`)을 가지고 관리하도록 변경할 수 있다.
리팩토링: 할인율을 ENUM 내부에서 관리
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;
}
}
- `discountPercent` 필드를 `ClassGrade` 내부에서 관리하여 `if` 문을 제거한다.
- 생성자를 통해서만 `discountPercent` 를 설정하도록 했고, 중간에 이 값이 변하지 않도록 불변으로 설계했다.
- 상수를 정의할 때 각각의 등급에 따른 할인율(`discountPercent`)이 정해진다.
리팩토링 후 DiscountService
public class DiscountService {
public int discount(ClassGrade classGrade, int price) {
return price * classGrade.getDiscountPercent() / 100;
}
}
- `if` 문이 사라지고, `classGrade.getDiscountPercent()` 를 직접 호출하는 방식으로 개선되었다.
- 단순히 회원 등급안에 있는 `getDiscountPercent()` 메서드를 호출하면 인수로 넘어온 회원 등급의 할인율을 바로 구할 수 있다.
실행 코드
public class ClassGradeRefMain1 {
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);
}
}
BASIC 등급의 할인 금액: 1000
GOLD 등급의 할인 금액: 2000
DIAMOND 등급의 할인 금액: 3000
실행 결과는 기존 코드와 같다.
6. 열거형 - 리팩토링2
열거형도 클래스이다. 자바의 enum을 활용하면 더 간결한 코드로 개선할 수 있다.
리팩토링: 열거형 사용 - Grade
public enum Grade {
BASIC(10), GOLD(20), DIAMOND(30);
private final int discountPercent;
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int getDiscountPercent() {
return discountPercent;
}
}
- 열거형을 사용하여 `ClassGrade` 대신 `Grade` 로 대체하였다.
- 열거형은 상수로 지정하는 것 외에 일반적인 방법으로 생성이 불가능하다.
따라서 생성자에 접근제어자를 선언할 수 없게 막혀있다. `private` 이라고 생각하면 된다. - `BASIC(10)` 과 같이 상수 마지막에 괄호를 열고 생성자에 맞는 인수를 전달하면 적절한 생성자가 호출된다.
- 상수별로 할인율을 직접 지정하여 관리할 수 있다.
리팩토링 후 DiscountService
public class DiscountService {
public int discount(Grade grade, int price) {
return price * grade.getDiscountPercent() / 100;
}
}
실행 코드
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);
}
}
실행 결과는 기존과 같다.
7. 열거형 - 리팩토링3
할인율 계산을 담당하는 `DiscountService` 를 제거하고, 할인율 계산을 ENUM 내부로 이동시켜 더욱 객체지향적인 설계를 만든다.
리팩토링: ENUM 내부에서 할인율 계산
public enum Grade {
BASIC(10), GOLD(20), DIAMOND(30);
private final int discountPercent;
Grade(int discountPercent) {
this.discountPercent = discountPercent;
}
public int discount(int price) {
return price * discountPercent / 100;
}
}
- 할인율을 Grade 내부에서 직접 계산하도록 discount(int price) 메서드를 추가하였다.
- 객체지향 관점에서 자신의 데이터를 외부에 노출하는 것 보다는, `Grade` 클래스가 자신의 할인율을 어떻게 계산하는지 스스로 관리하는 것이 캡슐화 원칙에 더 맞다.
DiscountService를 제거하고 ENUM 사용
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));
}
}
- `DiscountService` 를 제거하고, `Grade.BASIC.discount(price)` 와 같이 ENUM에서 직접 메서드를 호출하도록 변경하였다.
실행 코드
public class EnumRefMain3_4 {
public static void main(String[] args) {
int price = 10000;
Grade[] values = Grade.values();
for (Grade grade : values) {
printDiscount(grade, price);
}
}
private static void printDiscount(Grade grade, int price) {
System.out.println(grade.name() + " 등급의 할인 금액: " + grade.discount(price));
}
}
- 출력 부분의 중복을 제거했다.
- 새로운 등급이 추가되더라도 `main()` 코드의 변경없이 모든 등급의 할인을 출력할 수 있다.
- 기존과 동일한 결과를 얻으면서도 코드가 더 깔끔해지고 유지보수가 쉬워졌다.
'Course > Java' 카테고리의 다른 글
[java-mid1] 8. 중첩 클래스, 내부 클래스2 (0) | 2025.03.28 |
---|---|
[java-mid1] 7. 중첩 클래스, 내부 클래스1 (0) | 2025.03.24 |
[java-mid1] 4. 래퍼, Class 클래스 (0) | 2025.03.12 |
[java-mid1] 3. String 클래스 (0) | 2025.03.07 |
[java-mid1] 2. 불변 객체 (0) | 2025.03.04 |
댓글