💭 들어가며
프리코스 3주차 과제에서 "Java Enum을 적용하여 프로그램을 구현한다."라는 프로그래밍 요구 사항이 있었다. 이전에도 Enum을 사용해 본 적은 있지만, 이번 과제를 진행하면서 Enum을 정확히 이해하지 못한 상태로 사용해 왔다는 생각이 들었다. 단순히 요구사항 때문에 사용하기보다, 제대로 알고 활용하자는 마음으로 Enum을 다시 공부했다. Enum에 대해 잘 정리된 인파님의 글을 참고하여 과제에 적용해 보았고, 이번 글에서는 해당 부분을 정리하고 과제를 진행하면서 느낀 Enum의 장점을 정리하려고 한다.
✅ 이전의 상수 관리 방식
▶ static final 상수
final 키워드를 사용하여 하드 코딩된 값을 상수로 추출하는 방식이다.
🔽 2주차 과제(자동차 경주): static final 상수
public class Car {
public static final int MOVE_THRESHOLD = 4;
public static final int RANDOM_MIN = 0;
public static final int RANDOM_MAX = 9;
public static final int MOVE_INCREMENT = 1;
...
public void moveConditionally() {
int random = Randoms.pickNumberInRange(RANDOM_MIN, RANDOM_MAX);
if (random >= MOVE_THRESHOLD) {
increaseDistance();
}
}
private void increaseDistance() {
currentDistance += MOVE_INCREMENT;
}
}
🔽 장점
- final로 선언된 변수는 한 번 초기화되면 변경할 수 없다.
- static을 함께 사용하면 메모리에 한 번만 할당되어 메모리 효율이 높아진다.
🔽 단점
- 접근 제어자 설정으로 인해 가독성이 떨어질 수 있다.
- 다양한 종류의 상수를 한 곳에서 관리하기 어렵고, 여러 상수를 하나의 클래스에 모으면 코드가 복잡해질 수 있다.
▶ 인터페이스 상수
상수를 관리하는 또 다른 방법으로 상수만 포함된 인터페이스를 만드는 방식이다. 인터페이스는 별도 제어자 없이 상수만을 정의할 수 있다.
🔽 2주차 과제(자동차 경주): 인터페이스 상수
public interface CarConstants {
int MOVE_THRESHOLD = 4;
int RANDOM_MIN = 0;
int RANDOM_MAX = 9;
int MOVE_INCREMENT = 1;
}
🔽 장점
- 클래스에서 implements CarConstants를 통해 상수를 직접 사용할 수 있어 편리하다.
- 상수를 별도의 파일에 분리함으로써, 클래스 파일을 깔끔하게 유지할 수 있다.
🔽 단점
- 인터페이스가 상수 관리 목적으로만 사용될 경우, 이는 상수 인터페이스 안티패턴에 해당될 수 있다. 인터페이스는 주로 클래스의 행위를 정의하기 위한 용도이기 때문에, 의도에 맞지 않는 방식일 수 있다.
- 상수를 사용하는 모든 클래스에서 implements를 통해 상수 인터페이스를 상속받아야 하므로, 유연성이 떨어질 수 있다.
▶ 자체 클래스 상수
상수를 별도의 클래스로 분리하여 관리하는 방식이다. 기능별로 상수 클래스를 만들어 그룹화하며, 주로 Utility Class 형태로 사용된다. 인스턴스 생성을 방지하기 위해 private 생성자를 두고, 모든 상수를 public static final로 선언한다.
🔽 2주차 과제(자동차 경주): 자체 클래스 상수
public class CarConstants {
public static final int MOVE_THRESHOLD = 4;
public static final int RANDOM_MIN = 0;
public static final int RANDOM_MAX = 9;
public static final int MOVE_INCREMENT = 1;
private CarConstants() {
}
}
🔽 장점
- 상수를 기능별로 분리해 관리할 수 있어 코드가 깔끔하고 가독성이 높아진다.
- 객체 생성 없이 접근이 가능하여 메모리 사용이 효율적이다.
- 상수에 대한 모듈화가 용이하여 코드 유지보수가 쉽다.
🔽 단점
- 단일 값만 담을 수 있어, 복합적인 상수 그룹을 효과적으로 나타내기 어렵다.
✅ Enum의 기본 문법
▶ 선언
- enum 이름은 클래스와 동일하게 첫 글자를 대문자로 시작한다.
- 관례적으로 열거 상수는 모두 대문자로 작성한다.
- 열거 상수가 여러 단어로 구성될 경우, 단어 사이를 밑줄(_)로 연결한다.
🔽 3주차 과제(로또): 로또 순위를 enum으로 활용
public enum Rank {
FIRST_PLACE,
SECOND_PLACE,
THIRD_PLACE,
FOURTH_PLACE,
FIFTH_PLACE,
LAST_PLACE
}
▶ 메서드 종류
메서드 | 설명 | 반환 타입 |
name() | 열거 객체의 문자열을 반환 | String |
ordinal() | 열거 객체의 순번(0부터 시작)을 반환 | int |
valueOf(String name) | 문자열을 입력받아서 일치하는 열거 객체를 반환 | enum |
values() | 모든 열거 객체들을 배열로 반환 | enum[] |
compareTo() | 열거 객체 간 순번 차이를 비교하여 반환 | int |
equals() | 열거 객체와 다른 객체를 비교하여 일치 여부를 반환 | boolean |
🔽 name()
열거 객체가 가지고 있는 문자열을 반환한다.
Rank rank = Rank.SECOND_PLACE;
String rankName = rank.name();
System.out.println(rankName); // SECOND_PLACE
🔽 ordinal()
열거 객체의 순번(0부터 시작)을 반환한다.
Rank rank = Rank.SECOND_PLACE;
int rankNum = rank.ordinal();
System.out.println(rankNum); // 2
🔽 valueOf()
문자열을 입력받아 일치하는 열거 객체를 반환한다.
Rank rank = Rank.valueOf("SECOND_PLACE");
System.out.println(rank); // SECOND_PLCAE
🔽 values()
모든 열거 객체들을 배열로 반환한다.
Rank[] ranks = Rank.values();
System.out.println(Arrays.toString(ranks)); // [FIRST_PLACE, SECOND_PLACE, THIRD_PLACE, FOURTH_PLACE, FIFTH_PLACE, LAST_PLACE]
🔽 compareTo()
주어진 열거 객체를 비교해서 순번 차이를 반환한다.
- 열거 객체가 매개 변수의 열거 객체보다 순번이 빠르면 음수를 반환한다.
- 열거 객체가 매개 변수의 열거 객체보다 순번이 늦으면 양수를 반환한다.
Rank firstRank = Rank.FIRST_PLACE;
Rank secondRank = Rank.SECOND_PLACE;
int firstCompare = firstRank.compareTo(secondRank);
System.out.println(firstCompare); // -1
int secondCompare = secondRank.compareTo(firstRank);
System.out.println(secondCompare); // 1
🔽 equals()
해당 enum 상수와 다른 객체를 비교하여 boolean 형태로 반환한다.
Rank firstRank = Rank.FIRST_PLACE;
Rank secondRank = Rank.SECOND_PLACE;
boolean isEqual = firstRank.equals(secondRank);
System.out.println(isEqual); // false
✅ Enum 고급 문법
▶ 매핑
enum을 활용하면 특정 값과 매핑된 열거 상수를 출력할 수 있다. enum의 생성자에 매개변수를 전달해 각 열거 객체에 고유한 값을 부여할 수 있으며, 이를 필드에 저장한다. 이렇게 저장된 필드는 getter 메서드를 통해 접근할 수 있어, 열거 객체와 연관된 정보를 가져올 수 있다.
🔽 3주차 과제(로또): Enum 매핑을 활용한 Rank 객체
public enum Rank {
FIRST_PLACE(6, false, 2000000000),
SECOND_PLACE(5, true, 30000000),
THIRD_PLACE(5, false, 1500000),
FOURTH_PLACE(4, false, 50000),
FIFTH_PLACE(3, false, 5000),
LAST_PLACE(0, false, 0)
;
// 필드
private final Integer matchCount;
private final boolean requireBonusNumberMatch;
private final Integer prizeAmount;
// 생성자
Rank(Integer matchCount, boolean requireBonusNumberMatch, Integer prizeAmount) {
this.matchCount = matchCount;
this.requireBonusNumberMatch = requireBonusNumberMatch;
this.prizeAmount = prizeAmount;
}
// Getter (일치 개수)
public Integer getMatchCount() {
return matchCount;
}
// Getter (보너스 번호 일치 여부)
private boolean isRequireBonusNumberMatch() {
return requireBonusNumberMatch;
}
// Getter (상금)
public Integer getPrizeAmount() {
return prizeAmount;
}
...
}
✅ Enum의 특징
- 기본 자료형(primitive)이 아닌 참조(reference) 타입이다.
- 참조 타입이므로 null도 저장이 가능하다.
- == 연산자 비교 시 true를 반환한다.
- 상속을 지원하지 않는다.
- 모든 enum들은 내부적으로 java.lang.enum 클래스를 상속받고 있다. 자바는 다중 상속을 지원하지 않기 때문에, 다른 클래스를 상속받을 수 없다.
- Enum 클래스는 Object 클래스의 다음 네 가지 메서드를 오버라이딩할 수 없도록 final로 선언해 변경하지 못하도록 막아놓았다. 상수를 고유하게 유지하기 위한 목적이다.
메서드 | 설명 |
clone() | 객체를 복제하는 메서드 |
finalize() | Garbage Collection이 발생할 때 처리하기 위한 메서드 |
hashCode() | int 타입의 해시 코드 값을 반환하는 메서드 |
equals() | 두 개의 객체가 동일한지 확인하는 메서드 |
✅ Enum의 장점 ⭐️⭐️⭐️
🔽 기본적인 장점
- 코드가 단순해지고 가독성이 좋아진다.
- IDE의 적극적인 지원을 받을 수 있다. (자동 완성, 오타 검증, 텍스트 리팩토링 등)
- 리팩토링 시 수정 범위가 최소화된다. Enum에 내용이 추가되더라도 다른 코드 수정이 필요하지 않다.
- 특정 열거형 값만을 변수에 할당할 수 있어, 컴파일 단계에서 타입 안정성을 보장받을 수 있다.
- 열거형 상수뿐만 아니라 생성자, 메서드, 필드를 가질 수 있어 객체지향적으로 설계할 수 있다.
이러한 기본적인 장점 외에도 이번 과제를 하며 느꼈던 Enum의 핵심 장점은 다음과 같다.
▶ 상태와 행위를 한 곳에서 관리
이번 과제에서 로또 당첨 번호와 매칭 횟수, 보너스 번호 일치 여부를 통해 각 등수를 판별하는 로직이 필요했다. 이때, 상수마다 다른 동작이 필요하다면 보통 if-else나 switch문을 통해 각각의 조건을 분기하여 처리하는 것이 일반적이다.
🔽 3주차 과제(로또): 변경 전 (if-else 방식)
하지만, 프로그래밍 요구사항에 따르면 else 및 switch문을 사용하지 말라는 지침이 있었다. 해당 지침이 생겨난 이유를 생각해보면, 이런 식의 코드 구성 방식은 값(상태)과 메서드(행위)의 관계를 이해하는 데 시간이 걸린다는 문제가 있다. 또한, 새로운 조건이 추가될 때마다 불필요하게 분기문이 늘어나 코드의 복잡성이 증가할 수 있다는 단점도 있다.
public class LottoService {
public Rank evaluateRank(int matchCount, boolean isBonusNumberMatched) {
if (matchCount == 6) {
return Rank.FIRST_PLACE;
} else if (matchCount == 5 && isBonusNumberMatched) {
return Rank.SECOND_PLACE;
} else if (matchCount == 5) {
return Rank.THIRD_PLACE;
} else if (matchCount == 4) {
return Rank.FOURTH_PLACE;
} else if (matchCount == 3) {
return Rank.FIFTH_PLACE;
} else {
return Rank.LAST_PLACE;
}
}
}
🔽 3주차 과제(로또): 변경 후 (enum 활용)
Enum을 사용하여 상태(매칭 횟수, 보너스 번호 필요 여부, 상금)와 행위(등수 판별 로직)를 한 곳에 결합하여 구현했다.
public enum Rank {
FIRST_PLACE(6, false, 2000000000),
SECOND_PLACE(5, true, 30000000),
THIRD_PLACE(5, false, 1500000),
FOURTH_PLACE(4, false, 50000),
FIFTH_PLACE(3, false, 5000),
LAST_PLACE(0, false, 0)
;
...
public static Rank getRankByMatch(Integer matchCount, boolean isBonusNumberMatched) {
for (Rank rank : Rank.values()) {
if (isRankMatched(rank, matchCount, isBonusNumberMatched)) {
return rank;
}
}
return LAST_PLACE;
}
private static boolean isRankMatched(Rank rank, Integer matchCount, boolean isBonusNumberMatched) {
if (rank.isRequireBonusNumberMatch()) {
return rank.getMatchCount().equals(matchCount) && isBonusNumberMatched;
}
return rank.getMatchCount().equals(matchCount);
}
}
🔽 장점
- Rank enum 내부에 상태(필드)와 행위(메서드)를 함께 관리하므로 코드의 가독성이 크게 향상된다.
- if-else나 switch문을 대체하여, enum 상수마다 필요한 상태와 메서드를 작성함으로써 중복되는 분기문을 제거할 수 있다.
- Rank enum에 추가적인 로직을 넣어야 할 때, 다른 클래스의 코드를 건드리지 않고 enum에만 추가하면 되어 유지보수가 쉬워진다.
▶ Thread-safe인 싱글톤 객체
enum 자체가 싱글톤 패턴은 아니지만, 각 열거 상수는 싱글톤 객체로 취급된다. enum에 정의된 각 상수는 한 번만 초기화되고 JVM이 종료될 때까지 고유한 인스턴스로 유지된다. 따라서 enum의 열거형 상수는 고유한 인스턴스로, 싱글톤 패턴과 유사한 특징을 지니며, 특히 enum은 thread-safe하여 안전하게 사용할 수 있는 장점이 있다.
🔽 3주차 과제(로또): Enum Rank 객체
public enum Rank {
FIRST_PLACE(6, false, 2000000000),
SECOND_PLACE(5, true, 30000000),
THIRD_PLACE(5, false, 1500000),
FOURTH_PLACE(4, false, 50000),
FIFTH_PLACE(3, false, 5000),
LAST_PLACE(0, false, 0)
;
// 필드
private final Integer matchCount;
private final boolean requireBonusNumberMatch;
private final Integer prizeAmount;
// 생성자
Rank(Integer matchCount, boolean requireBonusNumberMatch, Integer prizeAmount) {
this.matchCount = matchCount;
this.requireBonusNumberMatch = requireBonusNumberMatch;
this.prizeAmount = prizeAmount;
}
...
}
🔽 3주차 과제(로또): Application의 main 메서드
public class Application {
public static void main(String[] args) {
Rank firstInstance = Rank.FIRST_PLACE;
Rank secondInstance = Rank.FIRST_PLACE;
// 동일한 인스턴스인지 비교
if (firstInstance == secondInstance) {
System.out.println("Rank.FIRST_PLACE는 싱글톤입니다!");
} else {
System.out.println("Rank.FIRST_PLACE는 싱글톤이 아닙니다.");
}
}
}
🔽 3주차 과제(로또): 출력 결과
Rank.FIRST_PLACE는 싱글톤입니다!
싱글톤 패턴의 핵심은 애플리케이션 전역에서 단 하나의 인스턴스만 생성하고, 그 인스턴스를 여러 곳에서 공유하도록 하는 것이다. 이렇게 되면 메모리 사용이 효율적이며, 특정 상태나 설정을 일관되게 유지할 수 있는 장점이 있다.
그러나 싱글톤 인스턴스를 멀티스레드 환경에서 안전하게 공유하려면 thread-safe해야 한다. 만약 여러 스레드가 동시에 인스턴스를 생성하려 시도한다면, 싱글톤 패턴의 의도와 달리 인스턴스가 여러 개 생성되는 상황이 발생할 수 있다. thread-safe한 싱글톤 구현은 이런 문제를 방지하여 여러 스레드가 동시에 접근해도 동일한 인스턴스만 사용하도록 보장한다.
📍 참고 자료
'우아한테크코스' 카테고리의 다른 글
[우아한테크코스] 1차 합격, 최종 코딩 테스트 기록 (4) | 2024.12.15 |
---|---|
[우아한테크코스] 프리코스 4주차 기록 (3) | 2024.11.12 |
[우아한테크코스] 프리코스 3주차 기록 (8) | 2024.11.05 |
[우아한테크코스] 프리코스 2주차 기록 (2) | 2024.10.29 |
[우아한테크코스] 프리코스 1주차 기록 (4) | 2024.10.22 |