728x90
반응형
6장 - 열거 타입과 애너테이션
💡 int 상수 대신 열거 타입을 사용하라
“열거 타입은 정수 상수보다 더 읽기 쉽고 안전하고 강력하다”
1. 정수 열거 패턴
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;
- 정수 열거 패턴(int enum pattern)의 문제점
- 타입 안전을 보장할 방법이 없음
- 표현력도 좋지 않음
- ex) 오렌지를 건네야 할 메서드에 사과를 보내고 동등 연산자(
==
)로 비교하더라도 컴파일러는 아무런 경고 메시지를 출력하지 않음.
- 자바가 정수 열거 패턴을 위한 별도의 이름공간(namespace)를 지원하지 않기 때문에 어쩔 수 없이 APPLE, ORANGE 등의 접두어를 사용해서 이름 충돌을 방지해야 함.
- 정수 대신 문자열 상수를 사용하는 문자열 열거 패턴(string enum pattern)은 더 나쁨.
- 문자열 상수의 이름 대신 문자열 값을 그대로 하드코딩하게 만들기 때문
- 하드코딩한 문자열에 오타가 있어도 컴파일러는 확인할 길이 없으므로 자연스럽게 런타임 버그가 생김
2. 열거 타입
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NEVEL, TEMPLE, BLOOD }
- 열거 타입 설명
- 완전한 형태의 클래스이며 상수 하나당 자신의 인스턴스를 하나씩 만들어
public static final
필드로 공개함. - 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상
final
임. - 싱글턴을 일반화한 형태, 싱글턴은 원소가 하나인 열거타입.
- 완전한 형태의 클래스이며 상수 하나당 자신의 인스턴스를 하나씩 만들어
- 열거 타입의 장점
- 열거 타입은 컴파일 타임 안전성을 제공함.
- 각자의 이름공간이 있어서 이름이 같은 상수도 평화롭게 공존함.
- 열거 타입에는 임의의 메서드나 필드를 추가할 수 있고, 임의의 인터페이스를 구현하게 할 수도 있음
- 데이터와 메서드를 갖는 열거 타입
- 열거 타입 상수 각각을 특정 데이터와 연결지으려면 생성자에서 데이터를 받아 인스턴스 필드에 저장하면 됨.
- 열거 타입은 근본적으로 불변이므로 모든 필드는
final
이어야 함.
// 데이터와 메서드를 갖는 열거 타입
public enum Planet {
MERCURY(3.302e+23, 2.439e6),
VENUS (4.869e+24, 6.052e6),
EARTH (5.975e+24, 6.378e6),
MARS (6.419e+23, 3.393e6),
JUPITER(1.899e+27, 7.149e7),
SATURN (5.685e+26, 6.027e7),
URANUS (8.683e+25, 2.556e7),
NEPTUNE(1.024e+26, 2.477e7);
private final double mass; // 질량(단위: 킬로그램)
private final double radius; // 반지름(단위: 미터)
private final double surfaceGravity; // 표면중력(단위: m / s^2)
// 중력상수(단위: m^3 / kg s^2)
private static final double G = 6.67300E-11;
// 생성자
Planet(double mass, double radius) {
this.mass = mass;
this.radius = radius;
surfaceGravity = G * mass / (radius * radius);
}
public double mass() { return mass; }
public double radius() { return radius; }
public double surfaceGravity() { return surfaceGravity; }
public double surfaceWeight(double mass) {
return mass * surfaceGravity; // F = ma
}
}
- 상수별 메서드 구현을 활용한 열거 타입
- 상수별로 다르게 동작하는 코드를 구현하고자 할 때 상수별 메서드 구현(constant-specific method implementation)을 사용하면 됨.
- 아래 예시에서는
apply
라는 추상 메서드를 선언하고 각 상수별 클래스 몸체, 즉 각 상수에서 자신에 맞게 재정의하였음.
// 상수별 클래스 몸체(class body)와 데이터를 사용한 열거 타입
public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
},
TIMES("*") {
public double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
public double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
public abstract double apply(double x, double y);
// 코드 34-7 열거 타입용 fromString 메서드 구현하기 (216쪽)
private static final Map<String, Operation> stringToEnum =
Stream.of(values()).collect(
toMap(Object::toString, e -> e));
// 지정한 문자열에 해당하는 Operation을 (존재한다면) 반환한다.
public static Optional<Operation> fromString(String symbol) {
return Optional.ofNullable(stringToEnum.get(symbol));
}
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
for (Operation op : Operation.values())
System.out.printf("%f %s %f = %f%n",
x, op, y, op.apply(x, y));
}
}
- 전략 열거 타입 패턴
PayrollDay
열거 타입의 생성자에서 적절한 잔업수당 전략(PayType
)을 선택PayrollDay
열거 타입은 잔업수당 계산을 그 전략 열거 패턴에 위임하여,switch
문이나 상수별 메서드 구현없이 안전하고 유연하게 설계.
// 전략 열거 타입 패턴
enum PayrollDay {
MONDAY(WEEKDAY), TUESDAY(WEEKDAY), WEDNESDAY(WEEKDAY),
THURSDAY(WEEKDAY), FRIDAY(WEEKDAY),
SATURDAY(WEEKEND), SUNDAY(WEEKEND);
private final PayType payType;
PayrollDay(PayType payType) { this.payType = payType; }
int pay(int minutesWorked, int payRate) {
return payType.pay(minutesWorked, payRate);
}
// 전략 열거 타입
enum PayType {
WEEKDAY {
int overtimePay(int minsWorked, int payRate) {
return minsWorked <= MINS_PER_SHIFT ? 0 :
(minsWorked - MINS_PER_SHIFT) * payRate / 2;
}
},
WEEKEND {
int overtimePay(int minsWorked, int payRate) {
return minsWorked * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MINS_PER_SHIFT = 8 * 60;
int pay(int minsWorked, int payRate) {
int basePay = minsWorked * payRate;
return basePay + overtimePay(minsWorked, payRate);
}
}
public static void main(String[] args) {
for (PayrollDay day : values())
System.out.printf("%-10s%d%n", day, day.pay(8 * 60, 1));
}
}
- 유용한 열거 타입 메서드
values()
: 정의된 상수들의 값을 배열에 담아 반환.valueOf(String)
: 상수 이름을 문자열로 입력받아 그 이름에 해당하는 상수를 반환
- 정리
- 필요한 원소를 컴파일타임에 알 수 있는 상수 집합이라면 항상 열거 타입을 사용하자.
- 열거 타입의 어떤 메서드가 상수별로 다르게 동작해야할 경우
switch
문 대신 상수별 메서드 구현 사용하기
- 열거 타입의 상수 일부가 같은 동작을 공유한다면?
- 전략 열거 타입 패턴 사용하기
💡 ordinal 메서드 대신 인스턴스 필드를 사용하라
“열거 타입 상수에 연결된 값은
ordinal
메서드로 얻지 말고, 인스턴스 필드에 저장하자”
1. ordinal 메서드
ordinal()
: 해당 상수가 그 열거 타입에서 몇 번째 위치인지를 반환- ordinal을 잘못 사용한 예시
- 유지보수하기 끔찍한 코드다..
- 상수 선언을 바꾸는 순간
numberOfMusicians
가 오동작하며, 이미 사용 중인 정수와 값이 같은 상수는 추가할 방법이 없음.- ex) 8중주가 이미 있으니 똑같이 8명이 연주하는 복4중주는 추가할 수 없음
public enum Ensemble {
SOLO, DUET, TRIO, QUARTET, QUINTET,
SEXTET, SEPTET, OCTET, NONET, DECTET;
public int numberOfMusicians() { return ordinal() + 1; }
}
2. 인스턴스 필드
→ 인스턴스 필드에 저장하면 위와 같은 문제들이 말끔히 해결됨
- 인스턴스 필드를 사용한 예
// 인스턴스 필드에 정수 데이터를 저장하는 열거 타입
public enum Ensemble {
SOLO(1), DUET(2), TRIO(3), QUARTET(4), QUINTET(5),
SEXTET(6), SEPTET(7), OCTET(8), DOUBLE_QUARTET(8),
NONET(9), DECTET(10), TRIPLE_QUARTET(12);
private final int numberOfMusicians;
Ensemble(int size) { this.numberOfMusicians = size; }
public int numberOfMusicians() { return numberOfMusicians; }
}
💡 비트 필드 대신 EnumSet을 사용하라
“
EnumSet
클래스 : 비트 필드 수준의 명료함과 성능 + 열거 타입의 장점”
1. 비트 필드
- 비트 필드 열거 상수 예시
- 비트 필드(bit field) : 비트별 OR을 사용해 여러 상수를 하나의 집합으로 모은 것.
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
처럼 사용.
public class Text {
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
// 매개변수 styles는 0개 이상의 STYLE_ 상수를 비트별 OR한 값이다.
public void applyStyles(int styles) { ... }
}
- 비트 필드의 단점
- 정수 열거 상수의 단점을 그대로 지님
- 비트 필드 값이 그대로 출력되면 단순한 정수 열거 상수를 출력할 때보다 해석하기가 훨씬 어렵다.
- 어떤 조합으로 비트 필드 값이 나왔는지를 알아내야하기 때문.
- 최대 몇 비트가 필요한지를 API작성 시 미리 예측하여 적절한 타입(
int
orlong
)을 선택해야 함.
2. EnumSet
→ 열거 타입 상수의 값으로 구성된 집합을 효과적으로 표현해주는 EnumSet
을 사용하자.
장
EnumSet
사용 예시
// EnumSet - 비트 필드를 대체하는 현대적 기법
public class Text {
public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH}
// 어떤 Set을 넘겨도 되나, EnumSet이 가장 좋다.
public void applyStyles(Set<Style> styles) {
System.out.printf("Applying styles %s to text%n",
Objects.requireNonNull(styles));
}
// 사용 예
public static void main(String[] args) {
Text text = new Text();
text.applyStyles(EnumSet.of(Style.BOLD, Style.ITALIC));
}
}
EnumSet
Set
인터페이스를 완벽히 구현하며, 타입 안전- 내부는 비트 벡터로 구현되어 있어, 대부분의 경우 비트 필드에 비견되는 성능을 보여줌
removeAll
과retainAll
과 같은 대량 작업은 비트를 효율적으로 처리할 수 있는 산술 연산을 써서 구현하였음
💡 ordinal 인덱싱 대신 EnumMap을 사용하라
“배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신
EnumMap
을 사용하라”
- Plant 클래스
// 식물을 아주 단순하게 표현한 클래스 (226쪽)
class Plant {
enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL }
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override public String toString() {
return name;
}
}
1. ordinal 인덱싱
ordinal()
을 배열 인덱스로 사용하는 예시
// ordinal()을 배열 인덱스로 사용 - 따라 하지 말 것!
Set<Plant>[] plantsByLifeCycleArr =
(Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycleArr.length; i++)
plantsByLifeCycleArr[i] = new HashSet<>();
for (Plant p : garden)
plantsByLifeCycleArr[p.lifeCycle.ordinal()].add(p);
// 결과 출력
for (int i = 0; i < plantsByLifeCycleArr.length; i++) {
System.out.printf("%s: %s%n",
Plant.LifeCycle.values()[i], plantsByLifeCycleArr[i]);
}
- 문제점
- 배열은 제네릭과 호환되지 않으니 비검사 형변환을 수행해야 하고 깔끔히 컴파일되지 않을 것임.
- 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 함.
- 정확한 정숫값을 사용한다는 것을 직접 보증해야 함. (정수는 열거 타입과 달리 타입 안전하지 않기 때문)
- 잘못된 값을 사용하면 잘못된 동작을 묵묵히 수행하거나 (운이 좋다면)
ArrayIndexOutOfBoundsException
을 던질 것임.
- 잘못된 값을 사용하면 잘못된 동작을 묵묵히 수행하거나 (운이 좋다면)
2. EnumMap
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);
System.out.println(plantsByLifeCycle);
- 장점
- 더 짧고 명료하며 안전하고 성능도
ordinal()
인덱싱 버전과 동일함.EnumMap
내부에서 배열을 사용하기 때문에 성능이 동일함.
- 안전하지 않은 형변환은 쓰지 않고, 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하니 출력 결과에 직접 레이블을 달 일도 없음.
- 배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 원천봉쇄됨.
- 더 짧고 명료하며 안전하고 성능도
- 다차원 관계 :
EnumMap<..., EnumMap<..>>
으로 표현해서 사용.
// 중첩 EnumMap으로 데이터와 열거 타입 쌍을 연결했다
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
private final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// 상전이 맵을 초기화한다.
private static final Map<Phase, Map<Phase, Transition>>
m = Stream.of(values()).collect(groupingBy(t -> t.from,
() -> new EnumMap<>(Phase.class),
toMap(t -> t.to, t -> t,
(x, y) -> y, () -> new EnumMap<>(Phase.class))));
public static Transition from(Phase from, Phase to) {
return m.get(from).get(to);
}
}
// 간단한 데모 프로그램 - 깔끔하지 못한 표를 출력한다.
public static void main(String[] args) {
for (Phase src : Phase.values()) {
for (Phase dst : Phase.values()) {
Transition transition = Transition.from(src, dst);
if (transition != null)
System.out.printf("%s에서 %s로 : %s %n", src, dst, transition);
}
}
}
}
💡 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
“열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 사용해 같은 효과를 낼 수 있음”
1. 열거 타입 확장
- 열거 타입은 확장할 수 없음.
- 사실 대부분 상황에서 열거 타입을 확장하는 건 좋지 않은 생각임
- 확장한 타입 원소는 기반 타입의 원소로 취급하지만 그 반대는 성립하지 않는다면 이상하기 때문
- 기반 타입과 확장된 타입들의 원소 모두를 순회할 방법도 마땅치 않음.
2. 인터페이스 구현 (확장 흉내 내기)
- 기본 아이디어 : 열거 타입이 임의의 인터페이스를 구현할 수 있다는 사실 이용
- 연산 코드용 인터페이스를 정의하고 열거 타입이 인터페이스를 구현한 예
public interface Operation {
double apply(double x, double y);
}
public enum ExtendedOperation implements Operation {
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};
private final String symbol;
ExtendedOperation(String symbol) {
this.symbol = symbol;
}
@Override public String toString() {
return symbol;
}
// 컬렉션 인스턴스를 이용해 확장된 열거 타입의 모든 원소를 사용하는 예 (235쪽)
public static void main(String[] args) {
double x = Double.parseDouble(args[0]);
double y = Double.parseDouble(args[1]);
test(Arrays.asList(ExtendedOperation.values()), x, y);
}
private 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));
}
}
- 장점 : API가 기본 열거 타입을 직접 명시하지 않고 인터페이스 기반으로 작성되었다면 기본 열거 타입 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스로 대체해 사용할 수 있음
- ex) 위 코드에서
for (Operation op : opSet)
처럼.
- ex) 위 코드에서
- 한계 : 열거 타입끼리 구현 상속 불가.
💡 명명 패턴보다 애너테이션을 사용하라
“어노테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유가 없다”
1. 명명 패턴
- 명명 패턴 : 변수나 함수의 이름을 일관된 방식으로 작성하는 패턴
- ex) JUnit은 버전 3까지 테스트 메서드 이름을
test
로 시작하게끔 했음. - 명명 패턴의 문제점
- 오타가 나면 안 됨
- ex) 실수로 메서드 이름을
tsetSafetyOverride
로 지으면 JUnit3은 이 메서드를 무시하고 지나침
- ex) 실수로 메서드 이름을
- 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없음
- ex) 클래스 이름을
TestSafetyMechanisms
로 지어 JUnit에 던져줬다고 해보자. 개발자는 이 클래스에 정의된 테스트 메서드들을 수행해주길 기대하겠지만 JUnit은 클래스 이름에는 관심이 없음
- ex) 클래스 이름을
- 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없음.
- 오타가 나면 안 됨
- 어노테이션은 명명 패턴의 모든 문제점을 해결해주는 개념으로, JUnit도 버전 4부터 전면 도입하였음.
2. 마커 어노테이션
- 마커 어노테이션 : 아무 매개변수 없이 단순히 대상에 마킹(marking)하는 어노테이션
- 메타 어노테이션 : 어노테이션 선언에 다는 어노테이션
@Retention(RetentionPolicy.RUNTIME)
: 어노테이션이 런타임에도 유지되어야 한다는 표시@Target(ElementType.METHOD)
: 어노테이션이 메서드 선언에서만 사용되어야 한다는 표시
- 마커 어노테이션 선언 예시
/**
* 테스트 메서드임을 선언하는 애너테이션이다.
* 매개변수 없는 정적 메서드 전용이다.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
- 마커 어노테이션 사용 예시
- Test 어노테이션에 주석으로 “매개변수 없는 정적 메서드 전용이다.”라고 쓰여있지만, 해당 주석을 컴파일러가 해석해서 강제할 수는 없음
- 그래서 아래 예시를 원하는 대로 동작시키기 위해서는 적절한 어노테이션 처리기가 필요함
- 관련 방법은
javax.annotation.processing
API 문서 참고.
- 관련 방법은
// 마커 애너테이션을 사용한 프로그램 예
public class Sample {
@Test
public static void m1() { } // 성공해야 한다.
public static void m2() { }
@Test public static void m3() { // 실패해야 한다.
throw new RuntimeException("실패");
}
public static void m4() { } // 테스트가 아니다.
@Test public void m5() { } // 잘못 사용한 예: 정적 메서드가 아니다.
public static void m6() { }
@Test public static void m7() { // 실패해야 한다.
throw new RuntimeException("실패");
}
public static void m8() { }
}
3. 어노테이션 with 매개변수
- 어노테이션에 매개변수를 사용하는 법을 알아보자
- 매개변수 하나를 받는 어노테이션 선언 예시
Class<? extends Throwable>
:Throwable
을 확장한 클래스의Class
객체.
/**
* 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
- 매개변수 하나를 받는 어노테이션 사용 예시
// 매개변수 하나짜리 애너테이션을 사용한 프로그램
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // 성공해야 한다.
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // 실패해야 한다. (다른 예외 발생)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // 실패해야 한다. (예외가 발생하지 않음)
}
- 배열 매개변수를 받는 어노테이션 선언 예시
// 배열 매개변수를 받는 애너테이션 타입
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
- 배열 매개변수를 받는 어노테이션 사용 예시
// 배열 매개변수를 받는 애너테이션을 사용하는 프로그램
public class Sample3 {
// 이 변형은 원소 하나짜리 매개변수를 받는 애너테이션도 처리할 수 있다
@ExceptionTest(ArithmeticException.class)
public static void m1() { // 성공해야 한다.
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // 실패해야 한다. (다른 예외 발생)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // 실패해야 한다. (예외가 발생하지 않음)
// 배열 매개변수를 받는 애너테이션을 사용하는 코드
@ExceptionTest({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() { // 성공해야 한다.
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
}
4. 반복가능 어노테이션
- 하나의 프로그램 요소에 같은 어노테이션을 여러 번 달도록 만들고 싶은 상황에 사용
- 반복 가능한 어노테이션 선언 예시
- 반복 가능 어노테이션 선언 방법 :
@Repeatable
메타 어노테이션을 달면 됨- 단,
@Repeatable
어노테이션의 매개변수로 ‘컨테이너 어노테이션’을 전달해야 함. - 컨테이너 어노테이션 : 내부 어노테이션 타입의 배열을 반환하는
value
메서드를 정의 해야함
- 단,
- 반복 가능 어노테이션 선언 방법 :
// 반복 가능한 애너테이션의 컨테이너 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
// 반복 가능한 애너테이션 타입
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
- 반복 가능한 어노테이션 사용 예시
// 반복 가능한 애너테이션을 사용한 프로그램
public class Sample4 {
// 반복 가능 애너테이션을 두 번 단 코드
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
}
💡 @Override 어노테이션을 일관되게 사용하라
“상위 클래스의 메서드를 재정의하려는 모든 메서드에
@Override
어노테이션을 달자”
- 재정의한 모든 메서드에
@Override
어노테이션을 의식적으로 달면 실수했을 때, 컴파일러가 잘 알려줄 것임 - 예외 경우가 한 가지 있음.
- 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 이 어노테이션을 달지 않아도 됨. 그런데 단다고 해서 해로울 것도 없음. → 걍 달자.
💡 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
“마커 인터페이스와 마터 어노테이션은 각자의 쓰임이 있다.”
1. 마커 인터페이스
- 마커 인터페이스 : 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스
- ex)
Serializable
인터페이스 - 마커 인터페이스가 마커 어노테이션보다 나은 점
- 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 어노테이션은 그렇지 않음.
- 마커 인터페이스는 어엿한 타입이므로 마커 어노테이션을 사용했다면 런타임에야 발견될 오류를 컴파일 타임에 잡을 수 있음
- 적용 대상을 더 정밀하게 지정할 수 있음
- 적용 대상(
@Target
)을ElementType.TYPE
으로 선언한 어노테이션은 모든 타입(클래스, 인터페이스, 열거 타입, 어노테이션)에 달 수 있음. 부착할 수 있는 타입을 세밀히 제한할 수 없음. - 특정 인터페이스를 구현한 클래스에만 적용하고 싶은 마커가 있다면 그냥 마킹하고 싶은 크래스에서만 그 인터페이스를 구현하면 됨.
- 적용 대상(
- 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 어노테이션은 그렇지 않음.
- 반대로, 마커 어노테이션이 마커 인터페이스보다 나은 점
- 거대한 어노테이션 시스템의 지원을 받음
2. 마커 어노테이션
- 그렇다면 마커 어노테이션은 언제 사용해야 하는가?
- 클래스와 인터페이스 외의 프로그램 요소(모듈, 패키지, 필드, 지역변수 등)에 마킹해야 할 때, 어노테이션을 사용
- 어노테이션을 활발히 활용하는 프레임워크에서 사용하려는 마커일 때.
- “이 마킹이 된 객체를 매개변수로 받는 메서드를 작성할 일이 있을까?”
- “그렇다”일 경우 → 마커 인터페이스를 사용해야 함. 컴파일타임 타입 안정성을 위해..
- 만약 적용 대상이
Element.TYPE
인 마커 어노테이션을 작성하고 있다면, 마커 인터페이스가 낫지는 않을지 고민해보자.
728x90
반응형
'📚 Reading > Tech' 카테고리의 다른 글
이펙티브 자바 - 8장. 메서드 (0) | 2023.01.24 |
---|---|
이펙티브 자바 - 7장. 람다와 스트림 (0) | 2023.01.24 |
이펙티브 자바 - 5장. 제네릭 (0) | 2023.01.24 |
이펙티브 자바 - 4장. 클래스와 인터페이스 (0) | 2023.01.24 |
이펙티브 자바 - 3장. 모든 객체의 공통 메서드 (0) | 2023.01.24 |