개요
- 모듈: `java.base`
- 패키지: `java.lang`
- 클래스: `Enum<E extends Enum<E>>`
- 타입 매개변수: `E` - 열거형 하위 클래스의 타입
- 구현된 모든 인터페이스: `Serializable`, `Comparable<E>`, `Constable`
Java Enum은 Java 5에서 처음 도입된 특별한 클래스 타입으로, `java.lang.Enum` 클래스를 확장한다. Enum을 사용하면 코드의 가독성을 높이고, 컴파일 타임에 값이 검증되도록 만들어 안전한 코드를 작성할 수 있다.
모든 자바 열거형(`enum`) 클래스의 공통 부모 클래스이다.
즉, 자바에서 우리가 `enum`을 선언하면 이는 컴파일 시 자동으로 `java.lang.Enum` 클래스를 상속한 자식 클래스로 변환된다.
예를 들어 다음과 같다.
public enum Color {
RED, BLUE;
}
위 코드는 컴파일 시 내부적으로 아래와 비슷하게 해석된다.
public final class Color extends Enum<Color> {
public static final Color RED = new Color("RED", 0);
public static final Color BLUE = new Color("BLUE", 1);
}
즉, 모든 `enum`은 자동으로 `Enum` 클래스를 상속받기 때문에 사용자가 임의로 다른 클래스를 상속할 수 없다. 단, 인터페이스는 구현할 수 있다.
Nested Class
`Enum` 클래스 내부에는 정적(static) 중첩 클래스인 `Enum.EnumDesc<E extends Enum<E>>`가 존재한다. 이 클래스는 열거형 상수(Enum constant) 하나를 기술하기 위한 이름 기반 설명자 역할을 한다.
쉽게 말해 `EnumDesc`는 이 Enum 상수가 어떤 이름을 가지고 어떤 Enum 타입에 속하는지를 표현하는 메타데이터 객체다. 일반적인 코드에서는 직접 사용할 일이 거의 없지만, Java 12 이후 도입된 `Constable`과 `ConstantDesc` API에서 상수 표현을 다룰 때 내부에서 활용된다.
예를 들어, "`Color.RED` 같은 Enum 상수를 Color라는 Enum 타입의 RED 상수"로 기술하기 위해 `EnumDesc` 객체가 사용된다. 다음처럼 표현할 수 있다.
Color.RED.describeConstable();
// → Optional[EnumDesc[Color.RED]]
즉, `EnumDesc`는 Enum 상수를 구조적으로 표현하기 위한 설명자로, 리플렉션이나 JVM의 상수 표현 관련 기능에서 사용된다.
Constructor
`Enum` 클래스에는 유일한 생성자가 존재한다. 시그니처는 다음과 같다.
protected Enum(String name, int ordinal)
- 각 Enum 상수의 이름과 선언된 순서를 초기화하는 역할을 한다.
- 접근 제한자가 `protected`로 선언되어 있기 때문에 외부에서 호출할 수 없고, 오직 컴파일러가 자동으로 호출한다.
예를 들어, 다음과 같은 코드가 있을 때
public enum Color {
RED, BLUE;
}
컴파일러는 내부적으로 다음과 유사한 클래스를 생성한다.
public final class Color extends Enum<Color> {
public static final Color RED = new Color("RED", 0);
public static final Color BLUE = new Color("BLUE", 1);
private Color(String name, int ordinal) {
super(name, ordinal); // Enum의 생성자 호출
}
}
또한, 사용자가 Enum 내부에 자체 생성자를 정의할 수도 있지만 그 경우에도 내부적으로 `super(name, ordinal)`이 암묵적으로 호출된다.
핵심 메서드
메서드 요약
| 한정자와 타입 | 메서드 | 설명 |
| `protected final Object` | `clone()` | `CloneNotSupportedException`을 던진다. |
| `final int` | `compareTo(E o)` | 지정된 객체와의 순서를 비교한다. |
| `final Optional<Enum.EnumDesc<E>>` | `describeConstable()` | 이 인스턴스에 대한 `EnumDesc`를 생성할 수 있다면 반환하고, 그렇지 않으면 빈 Optional을 반환한다. |
| `final boolean` | `equals(Object other)` | 지정된 객체가 이 열거 상수와 같으면 true를 반환한다. |
| `protected final void` | `finalize()` | 폐기 예정 |
| `final Class<E>` | `getDeclaringClass()` | 이 열거 상수가 속한 열거형 타입의 Class 객체를 반환한다. |
| `final int` | `hashCode()` | 이 열거 상수의 해시 코드를 반환한다. |
| `final String` | `name()` | 이 열거 상수의 이름을 선언된 그대로 반환한다. |
| `final int` | `ordinal()` | 이 열거 상수의 선언 위치(순서)를 반환한다. |
| `String` | `toString()` | 선언된 이름을 포함하는 문자열을 반환한다. |
| `static <T extends Enum<T>> T` | `valueOf(Class<T> enumClass, String name)` | 지정된 이름을 가진 열거형 상수를 반환한다. |
name()
public final String name()
- 해당 Enum 상수의 이름을 문자열로 반환한다.
- 즉, 소스 코드에 선언된 그대로의 식별자(String)를 리턴하며, 변경하거나 오버라이드될 수 없는 고정된 문자열이다.
- 항상 정확히 선언된 이름을 반환하지만, `toString()`은 기본적으로 같은 값을 반환하면서도 개발자가 오버라이드하여 사람이 보기 좋은 형태로 바꿀 수 있다. (일반적으로 `toString()`을 더 자주 사용한다.)
예시)
public enum PizzaStatus {
ORDERED, READY, DELIVERED;
@Override
public String toString() {
return "Status: " + name().toLowerCase();
}
}
PizzaStatus s = PizzaStatus.ORDERED;
System.out.println(s.name()); // ORDERED
System.out.println(s.toString()); // Status: ordered
ordinal
public final int ordinal()
- 해당 Enum 상수가 선언된 순서를 정수로 반환한다. → 직접 사용하는 것 권장X
- 가장 처음 선언된 상수는 `0`, 그 다음은 `1`, 이런 식으로 차례대로 번호가 매겨진다.
- 즉, Enum이 선언된 순서에 따라 자동으로 부여되는 인덱스 값이라고 할 수 있다.
- `EnumSet`이나 `EnumMap` 같은 고급 enum 기반 자료구조에서 사용하기 위해 설계되었다.
예시)
public enum Color {
RED, BLUE, GREEN
}
System.out.println(Color.RED.ordinal()); // 0
System.out.println(Color.BLUE.ordinal()); // 1
System.out.println(Color.GREEN.ordinal()); // 2
toString
public String toString()
- 해당 Enum 상수의 이름을 문자열로 반환한다.
- 보통 재정의할 필요는 없지만, 더 프로그래머 친화적인 문자열 표현이 필요한 경우 재정의할 수 있다.
예시)
public enum PizzaStatus {
ORDERED, READY, DELIVERED;
@Override
public String toString() {
return "Status: " + name().toLowerCase();
}
}
System.out.println(PizzaStatus.ORDERED.toString()); // Status: ordered
equals
public final boolean equals(Object other)
- 지정된 객체가 현재 Enum 상수와 같을 경우 `true`를 반환한다. (그렇지 않으면 `false`를 반환한다.)
- 내부적으로는 `Object` 클래스의 `equals()`를 오버라이드한 메서드이지만, Enum의 각 상수는 JVM 내에서 단 하나의 인스턴스로만 존재하므로 실제 동작은 `==` 연산자와 완전히 동일하다.
- 일반적으로 `equals()` 대신 `==`을 사용하는 것이 더 간결하고 안전하다.
- `==`은 `null` 비교에서도 예외를 발생시키지 않으며, 컴파일 시점에 타입 검사가 이루어져 오류를 미리 방지할 수 있다.
예시)
public enum Color {
RED, BLUE, GREEN
}
Color c1 = Color.RED;
Color c2 = Color.RED;
Color c3 = Color.BLUE;
System.out.println(c1.equals(c2)); // true
System.out.println(c1.equals(c3)); // false
System.out.println(c1 == c2); // true
System.out.println(c1 == c3); // false
hashCode
public final int hashCode()
- 해당 Enum 상수의 해시 코드를 반환한다.
- `equals()`와 마찬가지로 `hashCode()`는 Enum의 객체 동일성(`==`)에 의존하여 일관된 비교 결과를 보장한다.
clone
protected final Object clone()
- 항상 `CloneNotSupportedException` 예외를 던진다.
- Enum 상수가 JVM 내에서 항상 단 하나의 인스턴스(singleton)로 존재해야 하며, 이를 보장하기 위해 복제가 불가능하도록 설계되어 있다.
compareTo
public final int compareTo(E o)
- Enum 상수 간의 선언 순서를 기준으로 크기 비교를 수행한다.
- 반환값은 현재 상수가 비교 대상보다
- 앞에 있으면 음수(-)
- 같으면 0
- 뒤에 있으면 양수(+)를 반환한다.
- Enum의 선언순서가 변경되면 비교 결과도 달라지므로, 순서에 의존하는 로직보다 가독성과 의미를 중심으로 사용하는 것이 좋다.
예시)
public enum Color {
RED, BLUE, GREEN
}
System.out.println(Color.RED.compareTo(Color.BLUE)); // -1
System.out.println(Color.BLUE.compareTo(Color.BLUE)); // 0
System.out.println(Color.GREEN.compareTo(Color.RED)); // 2
getDeclaringClass
public final Class<E> getDeclaringClass()
- 현재 Enum 상수가 속한 Enum 타입의 `Class` 객체를 반환한다.
- 즉, 어떤 Enum 상수가 어느 Enum 클래스에 속해 있는지를 알아낼 수 있다.
- 상수가 상수 전용 클래스 본문(constant-specific class body)을 가지고 있을 경우, `Object.getClass()`가 반환하는 값과 `getDeclaringClass()`가 다를 수 있다.
- `getDeclaringClass()`는 항상 상수가 선언된 기본 Enum 타입을 가리킨다.
- 두 열거 상수 `e1`, `e2`는 아래 조건이 참일 때 동일한 타입으로 간주된다.
e1.getDeclaringClass() == e2.getDeclaringClass()
예시)
public enum Color {
RED, BLUE, GREEN;
}
System.out.println(Color.RED.getDeclaringClass()); // class Color
System.out.println(Color.BLUE.getDeclaringClass() == Color.GREEN.getDeclaringClass()); // true
describeContsable
public final Optional<Enum.EnumDesc<E>> describeConstable()
- Java 12에서 추가된 기능으로, 해당 Enum 상수를 `EnumDesc`라는 constant descriptor 형태로 표현해 반환한다.
예시)
public enum Color {
RED, BLUE, GREEN;
}
System.out.println(Color.RED.describeConstable());
// 출력: Optional[EnumDesc[Color.RED]]
valueOf
public static <T extends Enum<T>> T valueOf(Class<T> enumClass, String name)
- 특정 Enum 타입과 이름으로 Enum 상수를 찾아 반환한다.
- `enumClass`에는 Enum의 클래스 객체가, `name`에는 Enum 상수 이름이 들어가야 한다. (공백이나 대소문자 불일치는 허용되지 않는다.)
- 만약 존재하지 않는 이름을 전달하면 `IllegalArgumentException`이 발생한다.
- `enumClass`나 `name`이 `null`이면 `NullPointerException`이 발생한다.
예시)
public enum Color {
RED, BLUE, GREEN;
}
Color c1 = Enum.valueOf(Color.class, "RED");
System.out.println(c1); // RED
기본 Enum 사용법
Java Enum은 관련된 상수들을 집합으로 표현하는 방식이다. 예를 들어 피자 주문 상태를 나타내는 Enum을 정의할 수 있다.
public enum PizzaStatus {
ORDERED,
READY,
DELIVERED;
}
열거 시에는 콤마(`,`), 끝날 시에는 세미콜론(`;`)
이와 같이 Enum으로 정의된 상수는 코드 가독성을 높이고, 잘못된 값이 전달되는 예외 사항을 방지할 수 있다. 또한 유용한 메서드들이 많이 제공된다.
커스텀 Enum 메서드
기본 Enum에 추가적인 메서드를 정의하여 더 풍부한 API로 확장할 수 있다. 예를 들어, 피자가 `READY` 상태일 때만 배달이 가능하도록 `isDeliverable` 메서드를 정의해보겠다.
public class Pizza {
private PizzaStatus status;
public enum PizzaStatus {
ORDERED,
READY,
DELIVERED;
}
public boolean isDeliverable() {
if (getStatus() == PizzaStatus.READY) {
return true;
}
return false;
}
// Methods that set and get the status variable.
}
Enum 비교에서 "==" 연산자 사용
Enum 타입은 JVM에서 단 하나의 인스턴스만 존재하기 떄문에 `==` 연산자를 사용하여 안전하게 비교할 수 있다. `equals` 메서드 대신 `==`을 사용하면 `NullPointerException`을 방지할 수 있고, 컴파일 시점에 타입 안전성도 제공한다.
if (testPizza.getStatus() == Pizza.PizzaStatus.DELIVERED) {
// 배달 완료 상태
}
Switch문에서 Enum 사용
Enum은 `switch`문에서 사용이 가능하여 코드 가독성을 높인다.
public int getDeliveryTimeInDays() {
switch (status) {
case ORDERED: return 5;
case READY: return 2;
case DELIVERED: return 0;
}
return 0;
}
필드, 메서드, 생성자가 포함된 Enum
Enum 내부에 필드, 메서드, 생성자를 정의하여 유연하게 사용할 수 있다. 각 피자 상태에 따라 다른 `timeToDelivery` 값을 설정하고, 이에 따른 상태별 메서드를 추가해보았다. 이전에 사용했던 `if`와 `switch`문을 제거할 수 있다.
public class Pizza {
private PizzaStatus status;
public enum PizzaStatus {
ORDERED (5){
@Override
public boolean isOrdered() {
return true;
}
},
READY (2){
@Override
public boolean isReady() {
return true;
}
},
DELIVERED (0){
@Override
public boolean isDelivered() {
return true;
}
};
private int timeToDelivery;
public boolean isOrdered() {return false;}
public boolean isReady() {return false;}
public boolean isDelivered(){return false;}
public int getTimeToDelivery() {
return timeToDelivery;
}
PizzaStatus (int timeToDelivery) {
this.timeToDelivery = timeToDelivery;
}
}
public boolean isDeliverable() {
return this.status.isReady();
}
public void printTimeToDeliver() {
System.out.println("Time to delivery is " +
this.getStatus().getTimeToDelivery());
}
// Methods that set and get the status variable.
}
테스트 코드
@Test
public void givenPizaOrder_whenReady_thenDeliverable() {
Pizza testPz = new Pizza();
testPz.setStatus(Pizza.PizzaStatus.READY);
assertTrue(testPz.isDeliverable());
}
Enum 확장하기
상속 불가
Java의 `enum`은 내부적으로 `java.lang.Enum` 클래스를 상속하며, 컴파일 시 자동으로 `final`로 처리된다. 따라서 일반 클래스처럼 다른 enum을 상속할 수 없다.
public enum BasicStringOperation {
TRIM("Removing leading and trailing spaces."),
TO_UPPER("Changing all characters into upper case."),
REVERSE("Reversing the given string.");
private String description;
// constructor and getter
}
// ❌ 컴파일 에러 발생
public enum ExtendedStringOperation extends BasicStringOperation {
MD5_ENCODE("Encoding the given string using the MD5 algorithm."),
BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.");
private String description;
// constructor and getter
}
오류
Cannot inherit from enum BasicStringOperation
`enum`이 이미 `java.lang.Enum`을 상속하고 있기 때문에, 추가적인 클래스 상속을 허용하면 다중 상속 문제가 발생하기 때문이다.
인터페이스로 확장 흉내내기
상속은 불가능하지만 인터페이스를 사용하면 `enum`을 확장한 것처럼 보이게 만들 수 있다.
상수 확장 흉내내기
1. 인터페이스 정의
public interface StringOperation {
String getDescription();
}
2. Enum들이 인터페이스를 구현하도록 변경
public enum BasicStringOperation implements StringOperation {
TRIM("Removing leading and trailing spaces."),
TO_UPPER("Changing all characters into upper case."),
REVERSE("Reversing the given string.");
private String description;
// constructor, getter, override
}
public enum ExtendedStringOperation implements StringOperation {
MD5_ENCODE("Encoding using MD5."),
BASE64_ENCODE("Encoding using BASE64.");
private String description;
// constructor, getter, override
}
3. 인터페이스 타입으로 통합 처리
public class Application {
public String getOperationDescription(StringOperation operation) {
return operation.getDescription();
}
}
기능 확장 흉내내기
각 상수가 기능(메서드)을 가지도록 확장해보자
1. 인터페이스에 동작 메서드 추가
public class Application {
public String applyOperation(StringOperation operation, String input) {
return operation.apply(input);
}
}
public interface StringOperation {
String getDescription();
String apply(String input);
}
2. Enum이 직접 동작 구현
public enum BasicStringOperation implements StringOperation {
TRIM("Removing leading and trailing spaces.") {
@Override
public String apply(String input) {
return input.trim();
}
},
TO_UPPER("Changing all characters into upper case.") {
@Override
public String apply(String input) {
return input.toUpperCase();
}
},
REVERSE("Reversing the given string.") {
@Override
public String apply(String input) {
return new StringBuilder(input).reverse().toString();
}
};
//...
}
public enum ExtendedStringOperation implements StringOperation {
MD5_ENCODE("Encoding the given string using the MD5 algorithm.") {
@Override
public String apply(String input) {
return DigestUtils.md5Hex(input);
}
},
BASE64_ENCODE("Encoding the given string using the BASE64 algorithm.") {
@Override
public String apply(String input) {
return new String(new Base64().encode(input.getBytes()));
}
};
//...
}
이제 두 `enum` 모두 같은 방식으로 처리할 수 있다.
app.applyOperation(ExtendedStringOperation.MD5_ENCODE, "hello");
테스트 코드 예시
@Test
public void givenAStringAndOperation_whenApplyOperation_thenGetExpectedResult() {
String input = " hello";
String expectedToUpper = " HELLO";
String expectedReverse = "olleh ";
String expectedTrim = "hello";
String expectedBase64 = "IGhlbGxv";
String expectedMd5 = "292a5af68d31c10e31ad449bd8f51263";
assertEquals(expectedTrim, app.applyOperation(BasicStringOperation.TRIM, input));
assertEquals(expectedToUpper, app.applyOperation(BasicStringOperation.TO_UPPER, input));
assertEquals(expectedReverse, app.applyOperation(BasicStringOperation.REVERSE, input));
assertEquals(expectedBase64, app.applyOperation(ExtendedStringOperation.BASE64_ENCODE, input));
assertEquals(expectedMd5, app.applyOperation(ExtendedStringOperation.MD5_ENCODE, input));
}
코드 수정없이 Enum 확장하기
EnumMap으로 동작 매핑하기
외부 라이브러리에서 제공하는 Enum이 있다고 해보자.
public enum ImmutableOperation {
REMOVE_WHITESPACES, TO_LOWER, INVERT_CASE
}
이 Enum은 수정할 수 없지만, 이 상수들을 기능과 연결해 사용하고 싶다.
public interface Operator {
String apply(String input);
}
`EnumMap`을 사용하면 Enum 상수와 구현체를 연결할 수 있다.
public class Application {
private static final Map<ImmutableOperation, Operator> OPERATION_MAP;
static {
OPERATION_MAP = new EnumMap<>(ImmutableOperation.class);
OPERATION_MAP.put(ImmutableOperation.TO_LOWER, String::toLowerCase);
OPERATION_MAP.put(ImmutableOperation.INVERT_CASE, StringUtils::swapCase);
OPERATION_MAP.put(ImmutableOperation.REMOVE_WHITESPACES,
input -> input.replaceAll("\\s", ""));
}
public String applyImmutableOperation(ImmutableOperation op, String input) {
return OPERATION_MAP.get(op).apply(input);
}
}
이렇게 하면 Enum 자체를 바꾸지 않고도 각 상수에 대응하는 동작을 쉽게 추가할 수 있다.
EnumMap 검증하기
만약 외부 라이브러리의 Enum이 나중에 새로운 상수를 추가한다면, `EnumMap`에 그 상수를 추가하지 않은 상태로 실행될 위험이 있다. 그래서 초기화 시점에 모든 Enum 상수가 매핑되었는지 검사할 수 있다.
static {
OPERATION_MAP = new EnumMap<>(ImmutableOperation.class);
OPERATION_MAP.put(ImmutableOperation.TO_LOWER, String::toLowerCase);
OPERATION_MAP.put(ImmutableOperation.INVERT_CASE, StringUtils::swapCase);
// REMOVE_WHITESPACES 누락
if (Arrays.stream(ImmutableOperation.values())
.anyMatch(it -> !OPERATION_MAP.containsKey(it))) {
throw new IllegalStateException("Unmapped enum constant found!");
}
}
이 검증은 앱 시작 시점에 예외를 던져 모든 상수가 매핑되었는지 보장한다.
EnumSet과 EnumMap 활용
EnumSet
`EnumSet`은 추상 클래스이며, `RegularEnumSet`, `JumboEnumSet`이라는 두 가지 구현을 갖고 있다. 이 중 어느 구현이 사용될지는 인스턴스 생성시 Enum의 상수 개수에 따라 결정된다.
따라서 Enum 상수 집합을 다루어야 할 때는 `EnumSet`을 사용하는 것이 좋다(부분 집합 생성, 추가, 삭제, `containsAll`, `removeAll`과 같은 대량 연산에 적합). 모든 상수를 순회만 하고 싶다면 `Enum.values()`를 사용하는 것이 좋다.
`EnumSet`을 사용하여 배달되지 않은 상태를 가진 피자만 필터링할 수 있다.
public class Pizza {
// 추가된 부분
private static EnumSet<PizzaStatus> undeliveredPizzaStatuses =
EnumSet.of(PizzaStatus.ORDERED, PizzaStatus.READY);
private PizzaStatus status;
public enum PizzaStatus {
...
}
public boolean isDeliverable() {
return this.status.isReady();
}
public void printTimeToDeliver() {
System.out.println("Time to delivery is " +
this.getStatus().getTimeToDelivery() + " days");
}
// 추가된 부분
public static List<Pizza> getAllUndeliveredPizzas(List<Pizza> input) {
return input.stream().filter(
(s) -> undeliveredPizzaStatuses.contains(s.getStatus()))
.collect(Collectors.toList());
}
public void deliver() {
if (isDeliverable()) {
PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
.deliver(this);
this.setStatus(PizzaStatus.DELIVERED);
}
}
// Methods that set and get the status variable.
}
테스트 코드
@Test
public void givenPizaOrders_whenRetrievingUnDeliveredPzs_thenCorrectlyRetrieved() {
List<Pizza> pzList = new ArrayList<>();
Pizza pz1 = new Pizza();
pz1.setStatus(Pizza.PizzaStatus.DELIVERED);
Pizza pz2 = new Pizza();
pz2.setStatus(Pizza.PizzaStatus.ORDERED);
Pizza pz3 = new Pizza();
pz3.setStatus(Pizza.PizzaStatus.ORDERED);
Pizza pz4 = new Pizza();
pz4.setStatus(Pizza.PizzaStatus.READY);
pzList.add(pz1);
pzList.add(pz2);
pzList.add(pz3);
pzList.add(pz4);
List<Pizza> undeliveredPzs = Pizza.getAllUndeliveredPizzas(pzList);
assertTrue(undeliveredPzs.size() == 3);
}
EnumMap
`EnumMap`은 Enum 상수를 키로 사용하는 맵을 효율적으로 관리하는 클래스이다.
EnumMap<Pizza.PizzaStatus, Pizza> map;
아래는 피자 상태별로 피자 리스트를 그룹화하는 예제이다.
public static EnumMap<PizzaStatus, List<Pizza>> groupPizzaByStatus(List<Pizza> pizzas) {
return pizzas.stream()
.collect(Collectors.groupingBy(
Pizza::getStatus,
() -> new EnumMap<>(PizzaStatus.class),
Collectors.toList()
));
}
테스트 코드
@Test
public void givenPizaOrders_whenGroupByStatusCalled_thenCorrectlyGrouped() {
List<Pizza> pzList = new ArrayList<>();
Pizza pz1 = new Pizza();
pz1.setStatus(Pizza.PizzaStatus.DELIVERED);
Pizza pz2 = new Pizza();
pz2.setStatus(Pizza.PizzaStatus.ORDERED);
Pizza pz3 = new Pizza();
pz3.setStatus(Pizza.PizzaStatus.ORDERED);
Pizza pz4 = new Pizza();
pz4.setStatus(Pizza.PizzaStatus.READY);
pzList.add(pz1);
pzList.add(pz2);
pzList.add(pz3);
pzList.add(pz4);
EnumMap<Pizza.PizzaStatus, List<Pizza>> map = Pizza.groupPizzaByStatus(pzList);
assertTrue(map.get(Pizza.PizzaStatus.DELIVERED).size() == 1);
assertTrue(map.get(Pizza.PizzaStatus.ORDERED).size() == 2);
assertTrue(map.get(Pizza.PizzaStatus.READY).size() == 1);
}
디자인 패턴 구현하기
Singleton 패턴
Java의 `enum`은 Singleton 패턴을 간단하게 구현할 수 있다.
Enum 클래스는 내부적으로 `Serializable` 인터페이스를 구현하므로, JVM이 싱글톤을 보장해준다. 이는 기존의 싱글톤 구현 방식과 달리, 역질렬화 시에 새로운 인스턴스가 생성되지 않도록 따로 관리할 필요가 없음을 의미한다.
싱글톤 패턴을 구현하는 방법
public enum PizzaDeliverySystemConfiguration {
INSTANCE;
PizzaDeliverySystemConfiguration() {
// Initialization configuration which involves
// overriding defaults like delivery strategy
}
private PizzaDeliveryStrategy deliveryStrategy = PizzaDeliveryStrategy.NORMAL;
public static PizzaDeliverySystemConfiguration getInstance() {
return INSTANCE;
}
public PizzaDeliveryStrategy getDeliveryStrategy() {
return deliveryStrategy;
}
}
Strategy Pattern
전통적으로 strategy pattern은 여러 클래스가 구현하는 인터페이스를 통해 작성된다.
새로운 strategy을 추가하려면 새로운 구현 클래스를 추가해야 한다. 그러나 Enum을 사용하면 더욱 간단하게 이 패턴을 구현할 수 있으며, 새로운 구현ㅇ르 추가하는 것은 단순히 새로운 인스턴스에 구현을 정의하는 것으로 가능하다.
아래는 피자 배달 전략을 `EXPRESS`와 `NORMAL`로 나누고, 각 Enum에 구현을 추가해본다.
public enum PizzaDeliveryStrategy {
EXPRESS {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in express mode");
}
},
NORMAL {
@Override
public void deliver(Pizza pz) {
System.out.println("Pizza will be delivered in normal mode");
}
};
public abstract void deliver(Pizza pz);
}
이후 `Pizza` 클래스에 다음 메서드를 추가한다:
public void deliver() {
if (isDeliverable()) {
PizzaDeliverySystemConfiguration.getInstance().getDeliveryStrategy()
.deliver(this);
this.setStatus(PizzaStatus.DELIVERED);
}
}
테스트 코드
@Test
public void givenPizaOrder_whenDelivered_thenPizzaGetsDeliveredAndStatusChanges() {
Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
pz.deliver();
assertTrue(pz.getStatus() == Pizza.PizzaStatus.DELIVERED);
}
이렇게 Enum을 사용하면 싱글톤과 전략 패턴을 쉽게 구현할 수 있다.
JSON으로 Enum 표현하기
`Jackson` 라이브러리를 사용하면 Enum을 JSON 객체로 직렬화할 수 있다.
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum PizzaStatus {
ORDERED (5) {
@Override
public boolean isOrdered() {
return true;
}
},
READY (2) {
@Override
public boolean isReady() {
return true;
}
},
DELIVERED (0) {
@Override
public boolean isDelivered() {
return true;
}
};
private int timeToDelivery;
public boolean isOrdered() { return false; }
public boolean isReady() { return false; }
public boolean isDelivered() { return false; }
@JsonProperty("timeToDelivery")
public int getTimeToDelivery() {
return timeToDelivery;
}
private PizzaStatus(int timeToDelivery) {
this.timeToDelivery = timeToDelivery;
}
}
Pizza pz = new Pizza();
pz.setStatus(Pizza.PizzaStatus.READY);
System.out.println(Pizza.getJsonString(pz));
위 코드 실행 시 다음과 같은 피자의 상태에 대한 JSON표현이 생성된다.
{
"status" : {
"timeToDelivery" : 2,
"ready" : true,
"ordered" : false,
"delivered" : false
},
"deliverable" : true
}
Enum 타입의 Json 직렬화/역직렬화 방법에 대한 추가 정보는 이 문서를 참고할 수 있다.
참고
'우아한테크코스' 카테고리의 다른 글
| [우아한 테크코스] 테스트 함수 (0) | 2025.11.02 |
|---|---|
| [Java] 정적 팩토리 메서드 (0) | 2025.10.30 |
| [Git] Git Commands (0) | 2025.10.29 |
| [우아한 테크코스] 프리코스 2주차 회고 (0) | 2025.10.28 |
| [Java] 일급 컬렉션 (0) | 2025.10.27 |