메서드 이름을 특정 문자열로 시작하게 만들어, 해당 문자열이 들어가면 실행하도록 하는 패턴
변수나 함수의 이름을 일관된 방식으로 작성하는 패턴
ex. JUnit은 버전 3까지 이름을 test로 시작해야했다.
public class HelloWorldTest extends TestCase{
public void testSayHello(){
HelloWorld hello = new HelloWorld();
}
}
명명패턴의 단점
오타가 나면 안 된다.
무시하고 지나치기 때문에 테스트가 통과되었다고 오해할 수 있다.
올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
클래스를 테스트하기 위해 메서드가 아닌 클래스 이름에 Test를 넣어도 JUnit은 클래스 이름에 관심 없다.
프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
특정 예외를 던져야만 성공하는 테스트가 있을 때, 기대하는 예외 타입을 테스트에 매개변수로 전달해야 하는 상황이다. 예외의 이름을 테스트 메서드 이름에 덧붙이는 방법도 있지만, 가독성이 떨어지고 컴파일러는 메서드 이름에 붙인 문자열이 예외를 가리키는 이름인지 알 수 없다. 테스트를 실행하기 전에는 그런 이름의 클래스가 존재하는지 혹은 예외가 맞는지조차 알 수 없다.
🍑 본론
애너테이션
Annotaion : 사전적인 의미로 주석. 자바에서 사용될 때는 코드 사이에 주석처럼 쓰여서 특별한 의미, 기능을 수행하도록 하는 기술. 즉, 프로그램에게 추가적인 정보를 제공해주는 메타데이터라고 볼 수 있다.
1. 마커(marker) 에너테이션
아무 매개변수 없이 단순히 대상에 마킹한다는 의미의 애너테이션
마커 애너테이션 타입 선언
import java.lang.annotation.*;
/**
* 테스트 메서드임을 선언하는 애너테이션.
* 매개변수 없는 static 메서드 전용이다.
*/
// @Test가 런타임에도 유지되어야 한다.
@Retention(RetentionPolicy.RUNTIME)
// @Test가 반드시 메서드 선언에서만 사용되어야 한다. 클래스, 필드 선언에는 사용 불가
@Target(ElementType.METHOD)
public @interface Test{
}
@Test라는 애너테이션을 선언
메타에너테이션 : 애너테이션 선언에 다는 애너테이션
위 코드에서 @Retention과 @Target
마커 애너테이션을 사용한 프로그램의 예
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() { }
}
7개 중 4개의 @Test
m3, m7 메서드는 예외를 던진다.
m1, m5는 예외를 던지지 않지만 m5는 인스턴스 메서드이므로 @Test를 잘못 사용한 것이다.
마커 애너테이션 처리 프로그램
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class RunTests {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + "실패: " + exc);
} catch (Exception exc) {
System.out.println("잘못 사용한 @Test: " + m);
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
}
결과
public static void item39.Sample.m3()실패: java.lang.RuntimeException: 실패
잘못 사용한 @Test: public void item39.Sample.m5()
public static void item39.Sample.m7()실패: java.lang.RuntimeException: 실패
성공: 1, 실패: 3
이 애너테이션의 매개변수 타입은 Class<? extends Throwable>로 와일드카드 타입의 의미는 "Throwable을 확장한 클래스의 Class 객체"라는 뜻이며, 모든 예외 타입을 수용한다.
매개변수 하나짜리 애너테이션을 사용한 프로그램의 예
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() { } // 실패해야 한다. (예외가 발생하지 않음)
}
매개변수 하나짜리 애너테이션 처리 프로그램
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample2");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패 : 예외를 던지지 않음 %n", m);
} catch (InvocationTargetException wrappedEx) {
Throwable exc = wrappedEx.getCause();
Class<? extends Throwable> excType =
m.getAnnotation(ExceptionTest.class).value();
if(excType.isInstance(exc)){
passed++;
} else {
System.out.printf("테스트 %s 실패: 기대한 예외 %s, 발생한 예외 %s%n", m, excType.getName(), exc);
}
} catch (Exception exc) {
System.out.println("잘못 사용한 @Test: " + m);
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
}
결과
테스트 public static void item39.Sample2.m2() 실패: 기대한 예외 java.lang.ArithmeticException, 발생한 예외 java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 0
테스트 public static void item39.Sample2.m3() 실패 : 예외를 던지지 않음
성공: 1, 실패: 2
이 코드는 애너테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던지는지 확인하는데 사용한다.
형 변환 코드가 없으니 ClassCastException 걱정은 없고 테스트 프로그램이 문제없이 컴파일 되면 애너테이션 매개변수가 올바른 예외 타입이라는 뜻이다.
3. 배열 매개변수를 받는 애너테이션
배열 매개변수를 받는 애너테이션 타입
@ExceptionTest 애너테이션의 매개변수 타입을 Class 객체의 배열로 수정하여 예외를 여러개 명시할 수 있게 하고 그중 하나가 발생하면 성공으로 만들 수 있다.
#### 매개변수 하나짜리 애너테이션을 사용한 프로그램의 예
```java
import java.util.ArrayList;
import java.util.List;
public class Sample3 {
@ExceptionArrayTest(ArithmeticException.class)
public static void m1() { // 성공해야 한다.
int i = 0;
i = i / i;
}
@ExceptionArrayTest(ArithmeticException.class)
public static void m2() { //실패해야 한다. (다른 예외 발생)
int[] a = new int[0];
int i = a[1];
}
@ExceptionArrayTest(ArithmeticException.class)
public static void m3() { } // 실패해야 한다. (예외가 발생하지 않음)
@ExceptionArrayTest({IndexOutOfBoundsException.class,
NullPointerException.class})
public static void doubleBad() { // 성공해야 한다.
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsExcption이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
}
배열 매개변수 애너테이션 처리 프로그램
import java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample3");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionArrayTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패 : 예외를 던지지 않음 %n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
Class<? extends Throwable>[] excTypes =
m.getAnnotation(ExceptionArrayTest.class).value();
for(Class<? extends Throwable> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if(passed == oldPassed){
System.out.printf("테스트 %s 실패: %s%n", m, exc);
}
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
}
결과
테스트 public static void item39.Sample3.m2() 실패: java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 0
테스트 public static void item39.Sample3.m3() 실패 : 예외를 던지지 않음
성공: 2, 실패: 2
4. 반복 가능한 애너테이션
여러 개의 값을 받는 애너테이션을 배열이 아닌 반복을 통해 처리
주의점
@Repeatable을 단 애너테이션을 반환하는 '컨테이너 애너테이션'을 하나 더 정의하고 @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다.
컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.
컨테이너 애너테이션 타입에는 적절한 보존 정책(@Retention)과 적용 대상(@Target)을 명시해야 한다. 그렇지 않으면 컴파일되지 않는다.
반복 가능한 애너테이션 타입
import java.lang.annotation.*;
// 반복 가능한 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestConatiner.class)
public @interface ExceptionRepeatTest {
Class<? extends Throwable> value();
}
import java.util.ArrayList;
import java.util.List;
public class Sample4 {
@ExceptionRepeatTest(ArithmeticException.class)
public static void m1() { // 성공해야 한다.
int i = 0;
i = i / i;
}
@ExceptionRepeatTest(ArithmeticException.class)
public static void m2() { //실패해야 한다. (다른 예외 발생)
int[] a = new int[0];
int i = a[1];
}
@ExceptionRepeatTest(ArithmeticException.class)
public static void m3() { } // 실패해야 한다. (예외가 발생하지 않음)
@ExceptionRepeatTest(IndexOutOfBoundsException.class)
@ExceptionRepeatTest(NullPointerException.class)
public static void doubleBad() { // 성공해야 한다.
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsExcption이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
}
반복 가능한 애너테이션 처리 프로그램
mport java.lang.reflect.*;
public class RunTests {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample4");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionRepeatTest.class) || m.isAnnotationPresent(ExceptionTestConatiner.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패 : 예외를 던지지 않음 %n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionRepeatTest[] excTests =
m.getAnnotationsByType(ExceptionRepeatTest.class);
for(ExceptionRepeatTest excTest : excTests) {
if (excTest.value().isInstance(exc)) {
passed++;
break;
}
}
if(passed == oldPassed){
System.out.printf("테스트 %s 실패: %s%n", m, exc);
}
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
}
결과
테스트 public static void item39.Sample4.m2() 실패: java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 0
테스트 public static void item39.Sample4.m3() 실패 : 예외를 던지지 않음
성공: 2, 실패: 2
반복 가능 애너테이션을 여러개 달면 하나만 달았을 때와 구분하기 위해 해당 '컨테이너' 애너테이션 타입이 적용된다.
getAnnotationsByTypes 메서드는 이 둘을 구분하지 않아서 반복 가능 애너테이션과 그 컨테이너 애너테이션을 모두 가져오지만, isAnnotationPresent 메서드는 둘을 명확히 구분한다. 매개변수가 달려있는 수와 상관없이 모두 검사하려면 둘을 따로따로 확인해야 한다.
반복 가능 애너테이션을 사용해 하나의 프로그램 요소에 같은 애너테이션을 여러 번 달 때 코드 가독성을 높여보았으나 코드양이 늘어나며 처리 코드가 복잡해져 오류가 날 가능성이 커짐을 명심해야 한다.
🍑 결론
애너테이션이 명명 패턴보다 낫다.
따라서 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다.
자바가 제공하는 표준 애너테이션 타입들을 사용해야 한다.(아이템 40) IDE나 정적 분석 도구가 제공하는 애너테이션을 사용하면 도구를 바꾸거나 표준이 만들어지면 수정 작업을 거쳐야한다는 점에 유의하자.
Chapter : 6. 열거 타입과 애너테이션
Item : 39. 명명 패턴보다 애너테이션을 사용하라
Assignee : heon118
🍑 서론
명명 패턴
명명 패턴이란?
명명패턴의 단점
🍑 본론
애너테이션
1. 마커(marker) 에너테이션
마커 애너테이션을 사용한 프로그램의 예
마커 애너테이션 처리 프로그램
2. 매개변수 하나를 받는 애너테이션
매개변수 하나를 받는 애너테이션 타입
매개변수 하나짜리 애너테이션을 사용한 프로그램의 예
매개변수 하나짜리 애너테이션 처리 프로그램
결과
이 코드는 애너테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던지는지 확인하는데 사용한다.
형 변환 코드가 없으니 ClassCastException 걱정은 없고 테스트 프로그램이 문제없이 컴파일 되면 애너테이션 매개변수가 올바른 예외 타입이라는 뜻이다.
3. 배열 매개변수를 받는 애너테이션
배열 매개변수를 받는 애너테이션 타입
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionArrayTest { Class<? extends Throwable>[] value(); }
배열 매개변수 애너테이션 처리 프로그램
4. 반복 가능한 애너테이션
반복 가능한 애너테이션 타입
반복 가능한 애너테이션을 사용한 프로그램의 예
반복 가능한 애너테이션 처리 프로그램
결과
반복 가능 애너테이션을 여러개 달면 하나만 달았을 때와 구분하기 위해 해당 '컨테이너' 애너테이션 타입이 적용된다.
getAnnotationsByTypes 메서드는 이 둘을 구분하지 않아서 반복 가능 애너테이션과 그 컨테이너 애너테이션을 모두 가져오지만, isAnnotationPresent 메서드는 둘을 명확히 구분한다. 매개변수가 달려있는 수와 상관없이 모두 검사하려면 둘을 따로따로 확인해야 한다.
반복 가능 애너테이션을 사용해 하나의 프로그램 요소에 같은 애너테이션을 여러 번 달 때 코드 가독성을 높여보았으나 코드양이 늘어나며 처리 코드가 복잡해져 오류가 날 가능성이 커짐을 명심해야 한다.
🍑 결론
애너테이션이 명명 패턴보다 낫다. 따라서 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다. 자바가 제공하는 표준 애너테이션 타입들을 사용해야 한다.(아이템 40) IDE나 정적 분석 도구가 제공하는 애너테이션을 사용하면 도구를 바꾸거나 표준이 만들어지면 수정 작업을 거쳐야한다는 점에 유의하자.
Referenced by