[Effective Java] 6장. 열거 타입과 애너테이션

kkatal_chae·2022년 10월 4일
0

Effective Java

목록 보기
5/11
post-thumbnail

아이템34. int 상수 대신 열거 타입을 사용하라

열거 타입은 일정 개수의 상수 값을 정의한 다음, 그 외의 값은 허용하지 않는 타입이다.

// 가장 단순한 열거 타입
public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
public enum Orange { NAVEL, TEMPLE, BLOOD }

자바 열거 타입을 뒷받침하는 아이디어는 단순하다. 열거 타입 자체는 클래스이며, 상수 하나당 자신의 인스턴스를 하나씩 만들어 public static final 필드로 공개한다. 열거 타입은 밖에서 접근할 수 있는 생성자를 제공하지 않으므로 사실상 final이다.

⇒ 따라서 클라이언트가 인스턴스를 직접 생성하거나 확장할 수 없으니 열거 타입 선언으로 만들어진 인스턴스들은 딱 하나씩만 존재함이 보장된다.

열거 타입은 근본적으로 불변이라 모든 필드는 final 이어야 한다.

// 상수별 클래스 몸체와 데이터를 사용한 열거 타입
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);
}
// 전략 열거 타입 패턴
enum PayrollDay {
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY
  , SATURDAY(PayType.WEEKEND), SUNDAY(PayType.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 = 8 * 60;
			}
		}, 
		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);
		}
	}
}
			

필요한 원소를 컴파일타임에 다 알수 있는 상수 집합이라면 항상 열거 타입을 사용하자.



아이템 35. ordinal 메서드 대신 인스턴스 필드를 사용하라

대부분의 열거 타입 상수는 자연스럽게 하나의 정숫값에 대응된다.

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; }
}

Enum 의 API 문서를 보면 ordinal 메서드에 대해 이렇게 쓰여 있다. “대부분 프로그래머는 이 메서드를 쓸 일이 없다. 이 메서드는 EnumSetEnumMap 같이 열거 타입 기반의 범용 자료구조에 쓸 목적으로 설계되었다.” 따라서 이런 용도가 아니라면 ordianl 메서드는 절대 사용하지 말자.

( ordinal 메서드는 열거 타입 내 필드의 위치를 반환하는 메서드 )



아이템 36. 비트 필드 대신 EnumSet 을 사용하라

// 비트 필드 열거 상수 - 구닥다리 기법!
public class Text {
	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;
	
	// 매개변수 styles 는 0 개 이상의 STYLE_ 상수를 비트별 OR한 값이다. 
	public void applyStyles( int styles ) { ... }
}

다음과 같은 식으로 비트별 OR 를 사용해 여러 상수를 하나의 집합으로 모을 수 있으며, 이렇게 만들어진 집합을 비트 필드라 한다.

text.applyStyles( STYLE_BOLD | STYLE_ITALIC );

비트 필드를 사용하면 비트별 연산을 사용해 합집합과 교집합과 같은 집합 연산을 효율적으로 수행할 수 있다. 하지만 비트 필드는 정수 열거 상수의 단점을 그대로 지니며 추가로 다음과 같은 문제까지 안고 있다.

  • 비트 필드 값이 그대로 출력되면 단순한 정수 열거 상수를 출력할 때보다 해석하기 어렵다.
  • 비트 필드 하나에 녹아 있는 모든 원소를 순회하기도 까다롭다.
  • API 를 수정하지 않고는 비트 수를 늘릴 수 없기 때문에 최대 몇 비트가 필요한지 정확히 예측하여 적절한 타입 ( int, long ) 을 선택해야 한다.



// EnumSet - 비트 필드를 대체하는 현대적 기법 
public Class Text {
	public enum Styel { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

	// 어떤 Set을 넘겨도 되나, EnumSet 이 가장 좋다. 
	public void applyStyles( Set<Style> styles ) { ... } 
}

text.applyStyles( EnumSet.of( Style.BOLD, Style.ITALIC ) );

💡 열거할 수 있는 타입을 한데 모아 집합 형태로 사용한다고 해도 비트 필드를 사용할 이유는 없다.
EnumSet 클래스가 비트 필드 수준의 명료함과 성능을 제공하고 아이템 34 에서 설명한 열거 타입의 장점까지 선사하기 때문이다. EnumSet 의 유일한 단점이라면 아직 불변 EnumSet 을 만들 수 없다는 것이다. Collections.unmodifiableSet 으로 EnumSet 을 감싸 사용할 수 있다.



아이템 37.ordinal 인덱싱 대신 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);

EnumMap 의 성능이 ordinal 을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문이다. 내부 구현 방식을 안으로 숨겨서 Map 의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.

여기서 EnumMap 의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다.

스트림을 사용해 맵을 관리하면 코드를 더 줄일 수 있다.

// 스트림을 사용한 코드 2 - EnumMap 을 이용해 데이터와 열거 타입을 매핑했다.
System.out.println(Arrays.stream(garden) 
	.collect(groupBy(p -> p.lifeCycle, 
		() -> new EnumMap<>(LifeCycle.class), toSet())));

스트림을 사용하면 EnumMap 만 사용했을 때와는 살짝 다르게 동작한다.

EnumMap 버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만, 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.

💡 배열의 인덱스를 얻기 위해 ordinal 을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap 을 사용하라. 다차원 관계는 EnumMap<…, EnumMap<…>> 으로 표현하라.



아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

타입 안전 열거 패턴은 열거한 값들을 그대로 가져온 다음 값을 더 추가하여 다른 목적으로 쓸 수 있는 반면, 열거 타입은 그렇게 할 수 없다는 뜻이다.

기본 아이디어는 열거 타입이 임의의 인터페이스를 구현할 수 있다는 사실을 이용하는 것이다. 연산 코드용 인터페이스를 정의하고 열거 타입이 이 인터페이스를 구현하게 하면 된다.

// 확장 가능 열거 타입 
public enum ExtededOperation implements Operation {
	EXP("^") {
		public double apply ( double x, double y ) {
			return Math.pow( x, y );
		}
	},
	REMINDER("%") {
		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;
	}
}

💡 열거 타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거 타입을 함께 사용해 같은 효과를 낼 수 있다. API 가 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳을 새로 확장한 열거 타입의 인스턴스로 대체해 사용할 수 있다.



아이템 39. 명명 패턴보다 애너테이션을 사용하라

전통적으로 도구나 프레임워크가 특별히 다뤄야 할 프로그램 요소에는 딱 구분되는 명명 패턴을 적용해왔다.

예를 들어 테스트 프레임워크인 JUnit 3 까지 테스트 메서드 이름을 test 로 시작하도록 했다.

명명 패턴의 단점

  • 오타가 나면 안된다. 실수로 오타가 나면 해당 메서드는 무시하고 지나간다.
  • 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
  • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다는 것이다.
// 마커 애너테이션 타입 선언
import java.lang.annotation.*;

// 테스트 메서드임을 선언하는 애너테이션이다.
// 매개변수 없는 정적 메서드 전용이다. 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

위처럼 애너테이션 선언에 다는 애너테이션을 메타애너테이션이라 한다.

// 매개변수 하나를 받는 애너테이션 타입 
import java.lang.annotation.*;

// 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
	Class<? extends Throwable> value();
}

// 배열 매개변수를 받는 애너테이션 타입 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
	Class<? extends Throwable>[] value();
}

// 반복 가능 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
	Class<? extends Throwable> value();
}

// 컨테이너 애너테이션 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
	ExceptionTest[] value();
}

// 사용법
@ExceptionTest(ArithmeticException.class)
public static void method() { ... }

// 배열을 받는 애너테이션 사용법
@ExceptionTest( { ArithmeticException.class,
									NullPointerException.class } )
public static void method() { ... }

// 반복 가능 애너테이션 사용법
@ExceptionTest(ArithmeticException.class)
@ExceptionTest(NullPointerException.class)
public static void method() { ... }

다른 프로그래머가 소스코드에 추가 정보를 제공할 수 있는 도구를 만드는 일을 한다면 적당한 애너테이션 타입도 함께 정의해 제공하자.

애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다.

자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용해야 한다.



아이템 40. @Override 애너테이션을 일관되게 사용하라

상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 달자.

💡 재정의한 모든 메서드에 @Override 애너테이션을 의식적으로 달면 여러분이 실수했을 때 컴파일러가 바로 알려줄 것이다. 예외는 한 가지뿐이다. 구체 클래스에서 상위 클래스의 추상 메서드를 재정의한 경우엔 이 애너테이션을 달지 않아도 된다.



아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라

아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 마커 인터페이스라 한다.

마커 인터페이스는 두 가지 면에서 마커 애너테이션보다 낫다.

  • 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸 수 있으나, 마커 애너테이션은 그렇지 않다.
    • 마커 인터페이스는 어엿한 타입이기 때문에, 마커 애너테이션을 사용했다면 런타임에야 발견될 오류를 컴파일타임에 잡을 수 있다.
  • 적용 대상을 더 정밀하게 지정할 수 있다.
    • 적용 대상을 모든 타입 ( 클래스, 인터페이스, 열거 타입, 애너테이션 ) 에 달 수 있다.

반대로 마커 애너테이션이 마커 인터페이스보다 나은 점으로는 거대한 애너테이션 시스템의 지원을 받는다는 점이다.

💡 마커 인터페이스와 마커 애너테이션은 각자의 쓰임이 있다. 새로 추가하는 메서드 없이 단지 타입 정의가 목적이라면 마커 인터페이스를 선택하자. 클래스나 인터페이스 외의 프로그램 요소에 마킹해야 하거나, 애너테이션을 적극 활용하는 프레임워크의 일부로 그 마커를 편입시키고자 한다면 마커 애너테이션이 올바른 선택이다. 적용 대상이 ElementType.TYPE 인 마커 애너테이션을 작성하고 있다면, 잠시 여유를 갖고 정말 애너테이션으로 구현하는 게 옳은지, 혹은 마커 인터페이스가 낫지는 않을지 곰곰이 생각해보자.

0개의 댓글