[이펙티브 자바] Enum과 Annotaion의 Effective한 사용
by rowing0328※ 책 내용을 바탕으로 제 관점에서 풀어쓴 글입니다. 일부 내용이 다를 수 있습니다.
아이템 34 - int 상수 대신 enum을 사용하라
왜 enum을 사용해야 할까?
다음과 같은 방식으로 상수를 정의해 사용하는 코드는 흔히 볼 수 있다.
public static final int APPLE = 0;
public static final int ORANGE = 1;
public static final int BANANA = 2;
public static final int GRAPE = 3;
하지만 이 방식은 여러 문제를 야기한다.
public static final int FRUIT_APPLE = 10;
public static final int FOOD_APPLE = 10;
- 구분의 모호성
과일과 음식으로서의 "사과"를 구분해야 할 경우, 값이 중복될 가능성이 있다. - 타입 안정성 부족
int는 단순한 숫자일 뿐, 그 의미를 강제할 수 없다. 이는 코드 리뷰와 디버깅 시 혼란을 야기할 수 있다.
enum을 사용하여 문제 해결하기
@Getter
@AllArgsConstructor
public enum Fruit {
APPLE(1000, 20, "RED"),
PEACH(2000, 9, "YELLOW");
private final int price;
private final int box;
private final String color;
public int boxPrice() {
return price * box;
}
}
- 명확한 타입
Fruit이라는 타입을 강제할 수 있어, 컴파일 시점에 오류를 잡을 수 있다. - 캡슐화된 데이터
가격(price), 박스 수량(box), 색상(color) 등을 한곳에 정의할 수 있다. - 추가 메서드 구현 가능
boxPrice()처럼 데이터를 활용한 메서드를 추가하여 객체 지향적인 접근을 할 수 있다.
실무에서 사용하는 fromString 패턴
실제 개발에서는 외부에서 전달된 문자열을 enum 값으로 변환해야 하는 경우가 자주 발생한다.
이를 해결하기 위해 fromString 메서드 패턴을 사용할 수 있다.
예제 코드
private static final Map<String, Fruit> stringToEnum =
Stream.of(values())
.collect(Collectors.toMap(Objects::toString, e -> e));
public static Optional<Fruit> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
활용 사례
- 불확실한 입력 처리
외부 서버나 데이터베이스에서 불확실한 값이 넘어올 경우, Optional을 사용하여 처리할 수 있다. - DB 값 매핑
문자열을 enum으로 변환하여 안전하게 사용할 수 있다.
아이템 35 - ordinal method 대신 instant field를 사용하라
ordinal() 메서드란
Enum 상수가 정의된 순서를 정수 값으로 반환하는 메서드이다.
예제 코드
public enum Ensemble {
SOLO, DUEL, TRIO, QUARTET;
}
위 코드에서 SOLO.ordinal()은 0, DUEL.ordinal()은 1을 반환한다.
- 의미 없는 값 반환
ordinal()이 반환하는 값은 단순히 Enum 상수의 선언 순서일 뿐,
비즈니스 로직에서 사용하기에 적합하지 않다. - 유지보수의 어려움
Enum에 새로운 상수를 추가하거나 순서를 변경할 경우,
기존 코드가 잘못된 값을 참조하게 될 위험이 있다. - 예상치 못한 오류 발생
특정 패턴(예: 1, 2, 4와 같은 비트 연산)에 맞게 구현된 경우,
ordinal() 값으로 인해 오류가 발생할 수 있다.
Instant Field로 문제 해결하기
Enum 내에 상수와 연결된 고유한 값을 저장하기 위해 사용된다.
예제 코드
@AllArgsConstructor
public enum Ensemble {
SOLO(1), DUEL(2), TRIO(3), QUARTET(4), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
public int numberOfMusicians() {
return numberOfMusicians;
}
}
- 명확한 값
각 상수와 연관된 고유한 값을 정의할 수 있어, 의미를 명확히 전달할 수 있다. - 확장성과 안정성
Enum의 순서를 변경하거나 새로운 상수를 추가하더라도 기존 로직에 영향을 주지 않는다. - 객체지향적인 접근
Enum 내에서 특정 필드와 메서드를 활용하여 보다 구조화된 데이터를 관리할 수 있다.
실무에서의 권장 사항
ordinal은 실질적으로 쓸모없는 값이며, 순서를 기반으로 한 논리는 매우 취약하다.
Instant Field를 통해 비즈니스 로직에서 사용할 명확한 값을 Enum에 저장하는 것이 매우 좋다.
아이템 36 - bit field 대신 Enumset을 사용하라
Bit Field는 비트를 이용해 스타일 코드를 나타내는 방식으로, 보통 다음과 같이 구현된다.
예제 코드
public class TextStyleUtil {
public static final int STYLE_BOLD = 1 << 0; // 1
public static final int STYLE_ITALIC = 1 << 1; // 2
public static final int STYLE_UNDERLINE = 1 << 2; // 4
public static final int STYLE_STRIKETHROUGH = 1 << 3; // 8
public static int getStyleCode(List<String> textStyle) {
// 스타일 코드를 조합하여 반환
...
}
}
- 가독성 저하
비트 연산을 사용하는 방식은 직관적이지 않아, 코드의 의도를 쉽게 이해하기 어렵다. - 확장성 부족
새로운 스타일을 추가하거나 수정할 때, 기존 비트 값과의 충돌을 방지해야 하며, 이는 관리 비용을 증가시킨다. - 타입 안정성 부족
상수 값들이 int 타입으로 정의되어 있어, 컴파일 시점에 타입 안전성을 보장받기 어렵다.
EnumSet을 활용한 개선된 구현
EnumSet은 Enum 상수들의 집합(Set)을 표현하기 위한 효율적인 방법이다.
이를 활용하면 Bit Field 방식의 문제점을 해결할 수 있다.
예제 코드
@Getter
@AllArgsConstructor
public enum TextStyle {
BOLD(1), ITALIC(2), UNDERLINE(4), STRIKETHROUGH(8);
private final int code;
public static int getStyleCode(Set<TextStyle> styles) {
return styles.stream()
.mapToInt(TextStyle::getCode)
.sum();
}
}
// EnumSet을 활용한 호출 예시
int styleCode = TextStyle.getStyleCode(EnumSet.of(TextStyle.BOLD, TextStyle.ITALIC));
- 가독성 향상
EnumSet을 사용하면 코드를 보다 직관적으로 읽고 이해할 수 있다. - 타입 안정성
TestStyle이라는 Enum 타입을 강제하여, 컴파일 시점에 타입 오류를 방지한다. - 효율성
EnumSet은 내부적으로 비트 벡터를 사용하므로 매우 빠르고 메모리 사용량도 적다. - 유지보수 용이성
Enum 상수의 추가, 수정, 삭제 작업이 Bit Field보다 훨씬 간단하다.
실무에서의 권장 사항
대부분의 상황에서 Bit Field는 사용이 복잡하며,
새로운 코드 작성 및 디버깅 시 어려움을 초래한다.
EnumSet은 Java에서 제공하는 기본 자료구조로,
가독성과 타입 안정성 측면에서 우수하다.
EnumSet은 Enum 상수로만 구성할 수 있으므로,
비 Enum 타입과의 조합이 필요한 경우 별도의 처리가 필요하다.
특정 비트 연산이 필요한 경우,
(예: 시스템 수준의 효율성을 요구하는 작업)에는 여전히 Bit Field를 사용할 수 있다.
아이템 37 - ordinal indxing 대신 EnumMap을 사용하라
ordinal 메서드를 사용해 배열 인덱싱으로 데이터를 관리하는 방식은 다음과 같다.
예제 코드
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) {
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
- 가독성 부족
ordinal()을 사용하면 Enum의 의미가 코드에서 드러나지 않아, 의도를 파악하기 어렵다. - 타입 안정성 부족
ordinal은 정수 값을 반환하므로, 잘못된 인덱스를 참조할 가능성이 있다. - 유지보수 어려움
Enum 값이 추가되거나 순서가 변경되면, 기존 코드가 제대로 작동하지 않을 수 있다.
EnumMap으로 개선하기
Enum 상수를 키로 사용하는 특별한 구현체로, ordinal 인덱싱의 문제를 해결할 수 있다. 다음은 EnumMap을 활용한 예제이다.
예제 코드
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantsByLifeCycle.get(p.lifeCycle).add(p);
}
- 가독성 향상
EnumMap은 Enum 상수를 키로 사용하므로 코드의 의도가 명확하게 드러난다. - 타입 안정성
ordinal 대신 Enum 자체를 사용하므로, 타입 오류를 방지할 수 있다. - 효율성
EnumMap은 내부적으로 배열을 사용하므로, 매우 빠르고 메모리 사용량도 적다. - 유지보수 용이성
Enum 값이 추가되거나 순서가 변경되어도 코드 수정이 필요없다.
실무에서의 권장 사항
ordinal은 단순히 Enum의 순서를 나타낼 뿐,
배열 인덱싱을 통한 데이터 관리 방식은 예측 가능한 문제를 초래한다.
Enum 상수를 기준으로 데이터르 관리해야 할 때,
EnumMap을 사용하는 것이 가장 적합하다.
아이템 38 - 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
우선, 연산(예: 더하기, 빼기)을 표현하기 위한 Operation 인터페이스를 정의한다.
public interface Operation {
double apply(double x, double y);
}
enum을 사용하여 기본 연산(더하기, 빼기)을 정의할 수 있다.
@AllArgsConstructor
public enum BasicOperation implements Operation {
PLUS("+") {
@Override
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
@Override
public double apply(double x, double y) {
return x - y;
}
};
private final String symbol;
}
- 명확한 구조
각 연산의 구현을 enum의 각 상수에서 직접 정의할 수 있다. - 타입 안정성
Operation 인터페이스를 구현하여 타입 안전성을 보장한다.
확장 가능한 열거 타입
기본 연산 외에 새로운 연산(곱하기, 나누기 등)을 추가하려면 다른 enum에서도 Operation 인터페이스를 구현할 수 있다.
예제 코드
@AllArgsConstructor
public enum ExtendedOperation implements Operation {
MULTIPLY("*") {
@Override
public double apply(double x, double y) {
return x * y;
}
},
DIVIDE("/") {
@Override
public double apply(double x, double y) {
return x / y;
}
};
private final String symbol;
}
활용 예시
public static void test(Collection<? extends Operation> opSet, double x, double y) {
for (Operation op : opSet) {
System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
}
}
// 사용 예시
test(EnumSet.allOf(BasicOperation.class), 2, 3);
test(EnumSet.allOf(ExtendedOperation.class), 2, 3);
연산 집합을 받아 테스트하는 메서드를 위와 같이 활용 할 수 있다.
실무에서의 활용
enum 자체는 확장이 불가능하므로,
기본 열거 타입만 사용하면 새로운 기능을 추가하기 어렵다.
인터페이스를 통해 여러 enum에서 동일한 동작을 구현할 수 있다.
API가 인터페이스 기반으로 작성되면,
확장된 enum을 자유롭게 추가할 수 있다.
아이템 39 - 명명 패턴보다 Annotaion을 사용하라
명명 패턴은 특정 클래스나 메서드의 이름에 규칙을 부여하여 동작을 정의하는 방식이다.
The most important part of the class name that corresponds to the fragment interface is the Impl postfix.
예를 들어, JPA에서는 커스텀 클래스 이름에 Impl postfix를 사용해야 한다.
명명 패턴의 문제점
- 오타에 취약
이름 기반의 규칙은 단순한 오타만으로도 동작하지 않을 수 있다. - 유지보수의 어려움
프로그램 요소의 이름이 바뀌거나, 불필요한 스캔이 발생할 경우 오류가 생길 가능성이 있다. - 확장성 부족
이름에 의존하기 때문에 매개변수를 활용하거나 동적인 동작을 정의하기 어렵다.
Annotation의 등장
메타 데이터를 코드에 추가하여 특정 동작을 정의할 수 있는 강력한 도구이다.
이를 통해 명명 패턴의 한계를 극복할 수 있다.
Annotation의 주요 속성
- RetentionPolicy
Annotation이 적용되는 시점을 지정한다.
- SOURCE : 컴파일에 의해 무시된다.
- CLASS : 컴파일 이후에도 유지되지만 런타임에는 사용되지 않는다.
- RUNTIME : 런타임에서도 유지되어 Reflection으로 접근 가능하다.
- ElementType
Annotaion을 적용할 수 있는 대상을 지정한다.- 예 : TYPE, FIELD, METHOD, PARAMETER 등.
Annotation을 활용한 예시
// Custom Test Annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
// Marker Annotation 사용
public class RunTest {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName(args[0]);
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (Throwable ex) {
System.out.println(m + " 실패: " + ex.getCause());
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
}
- 오류 방지
명명 패턴보다 명시적이며, 오타로 인한 오류를 방지할 수 있다. - 유지보수 용이
이름에 의존하지 않으므로 리팩토링이 간편하다. - 확장성
매개변수와 속성을 추가하여 동작을 유연하게 정의할 수 있다.
아이템 40 - Override 에노테이션을 일관되게 사용하라
@Override Annotation의 효능
- 컴파일 타임 오류 방지
@Override를 사용했을 때, 실제로 Override 되지 않은 메서드에 대해 컴파일 시점에 오류를 발생시킨다.
이로써 실수를 줄이고 재정의 여부를 명확히 확인할 수 있다. - 프로그래머에게 명확성 제공
@Override를 통해 해당 메서드가 재정의되었다는 사실을 명시적으로 나타낸다.
이는 협업 중인 다른 개발자에게도 중요한 정보를 제공한다.
Annotation의 일관된 사용
- 권장 사항
모든 재정의 메서드에 @Override Annotation을 명시적으로 추가한다.
특히, 인터페이스 메서드를 재정의하는 경우에도 @Override를 사용하는 것이 좋다. - 예외 사항
추상 메서드를 재정의할 때는 @Override를 생략해도 무방하다.
그러나, 코드의 일관성을 위해 이 경우에도 사용하는 것이 권장된다.
@Override Annotation 사용 예시
// 올바른 사용 예시
public class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
public class Dog extends Animal {
@Override
public void sound() { // 재정의 명시
System.out.println("Dog barks");
}
}
// 잘못된 사용 예시
public class Dog extends Animal {
public void soud() { // 1. 오타로 인한 새로운 메서드 생성
System.out.println("Dog barks");
}
} // 2. `@Override`가 없으므로 컴파일러가 오류를 감지하지 못함.
아이템 41 - 정의하려는 것이 타입이라면 인터페이스를 사용하라
마커 인터페이스 (Marker Interface)
특별한 메서드 없이, 특정 클래스가 구현해야 할 속성을 정의하기 위한 인터페이스이다.
대표적인 예로 Serializable 인터페이스가 있다.
public interface Serializable { }
- 직렬화가 가능한 타입임을 나타낸다.
- 메서드 구현 없이, 특정 속성을 타입 레벨에서 구별하는 역할을 한다.
마커 어노테이션 (Marker Annotation)
마커 인터페이스와 유사한 목적을 가지지만, Annotation을 사용하여 표현한다.
예를 들어, 다음은 커스텀 마커 어노테이션의 예시이다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CodeMarker { }
- 클래스 또는 인터페이스에 메타데이터를 추가하여 런타임에서 이를 활용할 수 있다.
- Reflection을 통해 마커 어노테이션을 감지하고 동작을 결정할 수 있다.
언제 마커 인터페이스를 사용하는가
- 타입 정보가 필요한 경우
마커 인터페이스는 컴파일 시점에서 타입을 확인할 수 있으므로, 타입 기반의 제어가 필요한 경우 적합하다. - API 설계에 사용
마커 인터페이스는 해당 타입이 특정 속성을 가짐을 명확히 나타내며, 설계 단계에서 의미를 부여할 수 있다.
언제 마커 어노테이션을 사용하는가
- 런타임 정보가 필요한 경우
마커 어노테이션은 런타임에서 Reflection을 통해 동작을 제어하거나 메타데이터를 확인할 때 적합하다. - 더 유연한 구조를 원할 때
어노테이션은 추가적인 속성을 가질 수 있어 확장성이 뛰어나다.
마무리
이번 아이템에서 다룬 내용은 자바 개발자로서
효율적이고 안전한 코드를 작성하기 위한 핵심적인 지침들을 담고 있었다.
결국 중요한 것은 명확성과 일관성이다.
코드를 작성할 때 의도를 명확히 드러내고,
유지보수성이 뛰어난 설계를 지향해야 한다는 점을 다시 한번 깨달았다.
더 나아가, 잘 설계된 코드라면 주석 없이도
코드 자체가 의도와 방향성을 명확히 안내할 수 있어야 한다는 점도 큰 교훈이었다.
이러한 과정을 통해 코드의 가독성과 품질을 한 단계 더 높일 수 있음을 느꼈다.
'📚Book Archive > Effective Java' 카테고리의 다른 글
[이펙티브 자바] 람다의 우아함과 Stream의 주의사항 (0) | 2025.01.17 |
---|---|
[이펙티브 자바] Generic method와 Generic의 주의사항 (0) | 2025.01.08 |
[이펙티브 자바] Generic 으로 만들어 사용하기 (0) | 2025.01.03 |
[이펙티브 자바] Interface와 Class 설계 원칙 (0) | 2024.12.30 |
[이펙티브 자바] Class와 상속 (4) | 2024.12.20 |
블로그의 정보
코드의 여백
rowing0328