머리말
이번장은 열거타입과 애너테이션으로 자바에서 열거타입을 어떻게 사용하고 또 애너테이션은 어떤 기능을 가지고 있고 어떤 상황에쓰는지 파악해 보는 시간을 갖도록 하겠습니다.
int 상수 대신 열거 타입을 사용하라
아래와 같이 int 상수 대신하여 enum 타입을 사용하교 int 상수형을 지향하라
public static final int APPLE = 0;
public static final int BANANA = 1;
public static final int ORANGE = 2;
public enum Fruit {
APPLE,
BANANA,
ORANGE;
}
전략 열거타입
요일별로 일한 만큼의 수당을 계산해주는 enum 타입인데, 그 안의 하위 타입으로 평일과 주말을 나누고 위임하여 계산한다.
public enum PayroolDay {
MONDAY(PayType.WEEKDAY),
TUESDAY(PayType.WEEKDAY),
WEDNESDAY(PayType.WEEKDAY),
THURSDAY(PayType.WEEKDAY),
FRIDAY(PayType.WEEKDAY),
SATURDAY(PayType.WEEKEND),
SUNDAY(PayType.WEEKEND);
private final PayType payType;
PayroolDay(PayType payType) {
this.payType = payType;
}
int pay(int minusWorked, int payRate) {
return payType.pay(minusWorked, payRate);
}
private enum PayType {
WEEKEND {
@Override
int overtimePay(int mins, int payRate) {
return mins <= MIN_PER_SHIEFT ? 0 :
(mins - MIN_PER_SHIEFT) * payRate / 2;
}
},
WEEKDAY {
@Override
int overtimePay(int mins, int payRate) {
return mins * payRate / 2;
}
};
abstract int overtimePay(int mins, int payRate);
private static final int MIN_PER_SHIEFT = 8*60;
int pay(int minusWorked, int payRate) {
int basePay = minusWorked * payRate;
return basePay + overtimePay(minusWorked, payRate);
}
}
}
✏️ 핵심정리
열거 타입은 확실히 정수 상수보다 뛰어나기때문에, 더 읽기 쉽고 안전하고 강력하다. 대다수의 열거 타입이 명시적 생성자나 메서드 없이 쓰이지만, 각 상수를 특정 데이터와 연결 짓거나 상수마다 다르게 동작해야 할때는 필요하다. 드물게는 하나의 메스드가 상수별로 다르게 동작해야 할 때도 있다. 이런 열거타입에서는 switch 문 대신에 상수별 메서드 구현을 사용하자. 열거 타입 상수가 일부 같은 동작을 구현한다면 전략 열거 타입 패턴을 사용하자.
ordinal 메서드 대신 인스턴스 필드를 사용하라
대부분의 열거 타입 상수는 자연스럽게 하나의 정수값에 대응한다. 그 정수값을 가져와 사용하는 **함수가 바로 ordinal()**인데 이 함수의 사용을 지향하자는 것이다. 이 메서드는 EnumSet과 EnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되어있다고 Enum API 문서에 적혀있다.
비트 필드 대신 EnumSet을 사용하라
비트필드를 사용하는 대신에 EnumSet을 사용하게되면 EnumSet은 내부에서 비트를 자동으로 최적화 해주기때문에 성능에도 더 좋을 뿐더러 가독성도 더 좋다 아래의 예제를 비교 확인해보자
비트 필드 int 를 사용
- 정수를 사용하여 비트를 계산하고 그 값을 리턴한다.
- 정수 값이기때문에 가독성이 떨어진다.
public class Text {
private int styles;
public static final int STYLE_BOLD = 1 << 0;
public static final int STYLE_ITALIC = 1 << 1;
public static final int STYLE_UNDERLINE = 1 << 2;
public static final int STYLE_STRIKETHROUGH = 1 << 3;
public void applyStyles(int styles) {
this.styles = styles;
}
public int getStyles() {
return styles;
}
}
Enum을 사용
- applyStyles 함수를 보면 인터페이스로 값을 받고 있다.
- 방어적 복사를 활용하여 내부 styles를 불변으로 만든다.
import java.util.EnumSet;
import java.util.Set;
public class Textenum {
private Set<Style> styles;
public enum Style {BOLD, ITALIC, UNDERLINE, STRIKETHROUGH};
public void applyStyles(Set<Style> styles) {
this.styles = EnumSet.copyOf(styles);
}
public Set<Style> getStyles() {
return EnumSet.copyOf(styles);
}
}
실제사용
public static void main(String[] argv) {
Text text = new Text();
text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC);
System.out.println(text.getStyles()); // 출력 3
Textenum textenum = new Textenum();
textenum.applyStyles(EnumSet.of(Textenum.Style.BOLD, Textenum.Style.ITALIC));
System.out.println(textenum.getStyles()); // 출력 [BOLD, ITALIC]
}
ordinal 메서드 대신 EnumMap을 사용하라
ordinal() 함수는 사용을 지향하자. enum을 Key로 두는 EnumMap을 활용하자 정수값을 사용한다는것은 직접 우리가 보증해야한다는것이고, 정수 타입은 타입 안전하지 않기때문이다. 잘못 동작될 경우 ArrayIndexOutofBoundsException을 던질 것이다.
Enum을 활용한 Plant 클래스
- Plant를 hashCode와 equals를 재 정의하여 추후 Set<Plant>를 사용하도록 보장해준다.
public 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;
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Plant))
return false;
Plant p = (Plant)o;
return p.name.equals(name) && p.lifeCycle == lifeCycle;
}
@Override
public int hashCode() {
return name.hashCode() * 31 + lifeCycle.hashCode();
}
}
클라이언트 코드
- enum을 Key로 두어 타입에 안전하게 사용할 수 있다.
- Plant 클래스에서 hashCode와 equals를 재정의 하였기때문에 중복된 값을 허용하지 않는다.
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lc, new HashSet<>());
}
Plant[] garden = { new Plant("Plant1", Plant.LifeCycle.ANNUAL),
new Plant("Plant2", Plant.LifeCycle.BIENNIAL),
new Plant("Plant3", Plant.LifeCycle.PERENNIAL),
new Plant("Plant4", Plant.LifeCycle.ANNUAL),
new Plant("Plant4", Plant.LifeCycle.ANNUAL) // 중복값
};
for(Plant p : garden)
plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle);
// 출력 결과 : {ANNUAL=[Plant1, Plant4], PERENNIAL=[Plant3], BIENNIAL=[Plant2]}
✏️ 핵심정리
배열의 인덱스를 얻기 위해서 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라. 다차원 관계에서는 EnumMap<…, EnumMap<…>>으로 표현하라. “ 애플리케이션 프로그래머는 Enum.ordinal을 (웬만해서는) 사용하지 말아야한다.
확장할 수 있는 열거 타입이 필요하다면 인터페이스를 활용하라
열거 타입 자체는 확장 할 수 없지만, 인터페이스와 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다. 이렇게 하면 클라이언트는 이 인터페이스를 구현해 자신만의 열거타입(혹은 다른 타입)을 만들 수 있다. 그리고 API가 ( 기본 열거타입을 직접 명시하지않고 ) 인터페이스 기반으로 작성되었다면 기본 열거 타입 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스로 대체해 사용 할 수 있다.
Operation 인터페이스
- 람다를 활용하기위해서 함수형 인터페이스 애노테이션을 사용 하였다.
@FunctionalInterface
public interface Operation {
double apply(double x, double y);
}
Operation 인터페이스를 정의한 enum 클래스
- Operation에 대한 구현을 람다로 표기하였다.
public enum BasicOperation implements Operation {
PLUS("+", ((x, y) -> x + y)),
MINUS("-", ((x, y) -> x - y)),
TIMES("*", ((x, y) -> x * y)),
DIVIDE("/", ((x, y) -> x / y));
private final String symbol;
private final Operation operation;
BasicOperation(String symbol, Operation operation) {
this.symbol = symbol;
this.operation = operation;
}
@Override
public String toString() {
return symbol;
}
@Override
public double apply(double x, double y) {
return this.operation.apply(x, y);
}
}
클라이언트코드
Operation을 구현한 다른 enum 클래스도 test 함수를 사용 할 수 있다.
test(BasicOperation.class); // 4.000000 + 2.000000 = 6.000000
// 4.000000 - 2.000000 = 2.000000
// 4.000000 * 2.000000 = 8.000000
// 4.000000 / 2.000000 = 2.000000
public static <T extends Enum<T> & Operation> void test(Class<T> opEnumType)
{
for (Operation op : opEnumType.getEnumConstants())
{
System.out.printf("%f %s %f = %f%n", 4.0, op, 2.0, op.apply(4.0, 2.0));
}
}
명명 패턴보다는 애너테이션을 사용하라
명명패턴의 단점
- 오타를 잡아 낼 수 없다.
- 올바른 프로그램 요소에서만 사용 되라는 보증이 없다.
- 클래스의 이름을 TestSafetyMechanishm 으로 지어 JUint에 던져줬다고 한다면 개발자는 클래스에 대하여 정의된 메서드에 한에서 테스트를 수행 할 것이라고 기대하지만 그렇지 않다.
- 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
위의 모든 문제를 에너테이션이 멋지게 해결해준다.
예제를 구현해보고 따라해보자 함수 범위에 Exception의 정보를 등록하는 요구사항이 있다고하자 ( 다중 가능 )
- 런타임에 정보를 가져와야 하기때문에 Retention은 RetentionPolicy.RUMTIME으로 지정한다.
- 함수 범위에 등록을 해야하기때문에 Target은 ElementType.METHOD로 지정한다.
- 여러개를 구현하기위해서는 Repeatable을 사용해야한다.
ExceptionTest 애너테이션
- Repeatable을 사용하기위해서는 담을 컨테이너를 만들어야한다.
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
ExceptionContainer 에너테이션
- ExceptionTest를 담을 컨테이너
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
DoLogic 클래스
- 애너테이션을 활용할 클래스이며 테스트 할 함수를 정의해놓은 클래스이다
- 아래와 같이 에너테이션을 활용하면 뭐가 어떻게 테스트를 할것인지가 훨씬 직관적이다.
public class DoLogic {
@ExceptionTest(NullPointerException.class)
public static void nullPointException() {
throw new NullPointerException();
}
@ExceptionTest(IllegalArgumentException.class)
public static void illegalArgumentException() {
throw new IllegalArgumentException();
}
@ExceptionTest(NullPointerException.class)
public static void nullPointExceptionFalse() {
throw new IllegalArgumentException();
}
@ExceptionTest(IllegalArgumentException.class)
public static void illegalArgumentExceptionFalse() {
throw new NullPointerException();
}
@ExceptionTest(IllegalArgumentException.class)
@ExceptionTest(NullPointerException.class)
public static void illegalArgumentExceptionAllAccept() {
boolean random = Math.random() > 0.5;
if(random)
throw new IllegalArgumentException();
else
throw new NullPointerException();
}
}
클라이언트코드
public static void main(String[] args) {
int tests = 0;
int passed = 0;
Class<?> testClass = DoLogic.class;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.println("Test " + m + " failed: no exception");
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionTest[] exeTest = m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest et : exeTest) {
if (et.value().isInstance(exc)) {
System.out.println("Test " + m + " passed");
passed++;
break;
}
}
if(passed == oldPassed) {
System.out.printf("Test %s failed: %s%n", m, exc);
}
}
}
}
System.out.println("Total: " + tests + ", Passed: " + passed + ", Failed: " + (tests - passed));
}
// 아래와 같이 결과를 확인 할 수 있다.
// Test public static void annoation.DoLogic.nullPointException() passed
// Test public static void annoation.DoLogic.illegalArgumentException() passed
// Test public static void annoation.DoLogic.nullPointExceptionFalse() failed: java.lang.IllegalArgumentException
// Test public static void annoation.DoLogic.illegalArgumentExceptionFalse() failed: java.lang.NullPointerException
// Test public static void annoation.DoLogic.illegalArgumentExceptionAllAccept() passed
// Total: 5, Passed: 3, Failed: 2
오버라이드 애너테이션을 일관되게 사용하라
재정의한 모든 메서드에 @Override 에너테이션을 의식적으로 달면 여러분이 실수 했을때 컴파일러가 바로 알려줄 것이다. 예외는 한가지 뿐이다. 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 이 애너테이션을 달지 않아도 된다. ( 단다고해서 해로울 것도 없다. )
오버라이트 애너테이션은 무조건 달자!!
정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
- 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 에너테이션은 그렇지 않다.
- 적용 대상을 정밀하게 지정 할 수 있다.
- 마커 애너테이션이 마커 인터페이스보다 나은 점으로는 거대한 애너테이션 시스템의 지원을 받는다는점이다.
✏️ 새로 추가하는 메서드 없이 단지 타입 정의만 목적이라면 마커 인터페이스를 사용하자. 클래스나 인터페이스 외의 프로그램 요소에 마킹해야하거나, 애너테이션을 적극 활용하는 프레임워크 일부로 그 마커를 편입시키고자 한다면 마커 애너테이션이 올바른 선택이다. 적용 대상이 ElementType.TYPE인 마커 애너테이션을 작성하고 있다면, 잠시 여유를 갖고 정말 애너테이션이 옳은지, 혹은 마커 인터페이스가 낫지는 않을지 곰곰이 생각해보자.
회고
자바에서 열거형 타입과 애너티이션에서 대해서 알아보았는데요, 애너테이션은 제가 처음 접하는 개념으로 앞으로 클래스와 메서드, 필드 등 여러곳에 메타 정보를 저장할 수 있어서 다양하게 활용할 수 있겠다는 생각이 들었습니다.
- 인터페이스를 구현하고 클라이언트는 인터페이스에 의존하게 만들어서 유연성, 확장성이 좋아진다는것을 몸소 깨닫았습니다.
- EnumSet과 EnumMap 으로 유연성, 확장성이 좋은 타입체계를 만들 수 있다는것도 깨닫았습니다.
- 애너테이션의 활용법과 적용 범위에 대해서 알 수 있었습니다.
- 마커 라는 개념을 알게 되었고, 마커 인터페이스와 마커 애너테이션을 언제 어떻게 사용할지에 대한 감도 잡았습니다.
'이팩티브 자바' 카테고리의 다른 글
제네릭 (0) | 2024.03.17 |
---|---|
클래스와 인터페이스 (0) | 2024.03.17 |
모든 객체의 공통 메서드 (0) | 2024.03.17 |
객체 생성과 파괴 (0) | 2024.03.10 |