forked from depromeet/effective-java-study
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Item39]: 명명 패턴보다 애너테이션을 사용하라 (depromeet#114)(시연) (depromeet#117)
- Loading branch information
Showing
1 changed file
with
294 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,294 @@ | ||
# 명명 패턴보다 애너테이션을 사용하라 | ||
> 애너테이션으로 처리할 수 있다면 명명 패턴을 사용할 이유는 없다. 애너테이션을 사용하라 | ||
## 명명 패턴의 단점 | ||
1. 오타가 나면 안 된다 | ||
만약 test로 시작되어야 할 메서드 이름이 오타로 인해 tset로 작성되었다면, 명명 패턴에는 벗어나지만 프로그램 상에서는 문제가 없기 때문에 테스트 메서드로 인식하지 못하고 테스트를 수행하지 않는다 | ||
|
||
2. 명명 패턴을 의도한 곳에서만 사용할 거라는 보장이 없다 | ||
개발자는 JUnit3의 명명 패턴인 'test'를 메서드가 아닌 클래스의 이름으로 지음으로써 해당 클래스의 모든 테스트 메서드가 수행되길 바랄 수 있다. 하지만 JUnit은 클래스 이름에는 관심이 없다. 따라서 개발자가 의도한 테스트는 전혀 수행되지 않는다 | ||
|
||
3. 명명 패턴을 적용한 요소를 매개변수로 전달할 마땅한 방법이 없다 | ||
특정 예외를 던져야 성공하는 테스트가 있을 때, 메서드 이름에 포함된 문자열로 예외를 알려주는 방법이 있지만 보기 흉할 뿐 아니라 컴파일러가 문자열이 예외 이름인지 알 도리가 없다 | ||
|
||
--- | ||
|
||
## 마커(marker) 애너테이션 | ||
```java | ||
/** | ||
* 테스트 메서드임을 선언하는 애너테이션이다. | ||
* 매개변수 없는 정적 메서드 전용이다. | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.METHOD) | ||
public @interface Test { | ||
} | ||
``` | ||
- `@Test` 마커 애너테이션은 예외가 발생하면 테스트를 실패로 처리한다 | ||
- `@Retention(RetentionPolicy.RUNTIME)` - 보존 정책 | ||
- @Test가 런타임에도 유지되어야 한다는 표시 | ||
- `@Target(ElementType.METHOD)` - 적용 대상 | ||
- @Test가 반드시 메서드에 선언되어야 한다는 표시 | ||
|
||
### 예시 | ||
```java | ||
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() { | ||
} | ||
} | ||
``` | ||
- @Test 애너테이션은 Sample 클래스의 의미에 직접적인 영향을 주지 않고, 단지 관심 있는 프로그램에게 추가 정보를 제공한다 | ||
- 대상 코드의 의미는 그대로 둔 채 그 애너테이션에 관심 있는 도구에서 특별한 처리를 한다 | ||
|
||
### 애너테이션 처리기 | ||
@Test 어노테이션이 달린 테스트 메소드를 찾고, 실행 후 성공/실패 여부를 확인 | ||
```java | ||
import java.lang.reflect.*; | ||
|
||
public class RunTests { | ||
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)) { // @Test가 있는 메서드라면 | ||
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); | ||
} | ||
} | ||
``` | ||
|
||
--- | ||
|
||
## 매개변수를 받는 애너테이션 타입 | ||
```java | ||
import java.lang.annotation.*; | ||
|
||
/** | ||
* 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션 | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.METHOD) | ||
public @interface ExceptionTest { | ||
Class<? extends Throwable> value(); // 매개변수 | ||
} | ||
``` | ||
- `Class<? extends Throwable>` 매개변수 타입은 모든 예외/오류 타입을 수용한다 | ||
|
||
### 예시 | ||
```java | ||
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() { // 실패해야 한다. (예외가 발생하지 않음) | ||
} | ||
} | ||
``` | ||
|
||
### 애너테이션 처리기 | ||
```java | ||
public class RunTests { | ||
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(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("잘못 사용한 @ExceptionTest: " + m); | ||
} | ||
} | ||
} | ||
System.out.printf("성공: %d, 실패: %d%n", | ||
passed, tests - passed); | ||
} | ||
} | ||
``` | ||
|
||
--- | ||
|
||
## 배열 매개변수를 받는 애너테이션 타입 | ||
```java | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.METHOD) | ||
public @interface ExceptionTest { | ||
Class<? extends Exception>[] value(); // Class 객체의 배열 | ||
} | ||
``` | ||
|
||
### 예시 | ||
```java | ||
@ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class }) | ||
public static void doublyBad() { // 성공해야 한다. | ||
List<String> list = new ArrayList<>(); | ||
|
||
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나 NullPointerException을 던질 수 있다. | ||
list.addAll(5, null); | ||
} | ||
``` | ||
|
||
### 애너테이션 처리기 | ||
```java | ||
public class RunTests { | ||
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(ExceptionTest.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(ExceptionTest.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); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
``` | ||
|
||
## 반복 가능한 애너테이션 | ||
```java | ||
/* 반복 가능한 애너테이션 */ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.METHOD) | ||
@Repeatable(ExceptionTestContainer.class) // 컨테이너 애너테이션 | ||
public @interface ExceptionTest { | ||
Class<? extends Throwable> value(); | ||
} | ||
``` | ||
|
||
```java | ||
/* 반복 가능한 애너테이션의 컨테이너 애너테이션 */ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.METHOD) | ||
public @interface ExceptionTestContainer { | ||
ExceptionTest[] value(); // value 메서드 정의 | ||
} | ||
``` | ||
- 주의 사항 | ||
- @Repeatable을 단 애너테이션을 반환하는 '컨테이너 애너테이션'을 하나 더 정의하고, @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다 | ||
- 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다 | ||
- 컨테이너 애너테이션 타입에는 적절한 보존 정책(@Retention)과 적용 대상(@Target)을 명시해야 한다 (그렇지 않으면 컴파일 X) | ||
|
||
### 예시 | ||
```java | ||
@ExceptionTest(IndexOutOfBoundsException.class) | ||
@ExceptionTest(NullPointerException.class) | ||
public static void doublyBad() { | ||
List<String> list = new ArrayList<>(); | ||
|
||
list.addAll(5, null); | ||
} | ||
``` | ||
|
||
### 애너테이션 처리기 | ||
- 코드 가독성을 높일 수 있지만 애너테이션을 처리하는 부분의 코드가 늘어나고 복잡해서 오류가 발생할 확률이 커진다는 것을 명심하자 | ||
```java | ||
public class RunTests { | ||
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(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)) { | ||
tests++; | ||
try { | ||
m.invoke(null); | ||
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m); | ||
} catch (Throwable wrappedExc) { | ||
Throwable exc = wrappedExc.getCause(); | ||
int oldPassed = passed; | ||
ExceptionTest[] excTests = m.getAnnotationsByType(ExceptionTest.class); | ||
for (ExceptionTest excTest : excTests) { // 예외 순회 | ||
if (excTest.value().isInstance(exc)) { | ||
passed++; | ||
break; | ||
} | ||
} | ||
if (passed == oldPassed) | ||
System.out.printf("테스트 %s 실패: %s %n", m, exc); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
``` |