peaches-book-study / effective-java

이펙티브 자바 3/E
0 stars 2 forks source link

Item 39. 명명 패턴보다 애너테이션을 사용하라 #40

Open heon118 opened 3 months ago

heon118 commented 3 months ago

Chapter : 6. 열거 타입과 애너테이션

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

Assignee : heon118


🍑 서론

명명 패턴

명명 패턴이란?

public class HelloWorldTest extends TestCase{
    public void testSayHello(){
        HelloWorld hello = new HelloWorld();
    }
}

명명패턴의 단점

🍑 본론

애너테이션

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() { }
}

마커 애너테이션 처리 프로그램

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

2. 매개변수 하나를 받는 애너테이션

매개변수 하나를 받는 애너테이션 타입

import java.lang.annotation.*;

/**
 * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
 */
@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() { } // 실패해야 한다. (예외가 발생하지 않음)
}

매개변수 하나짜리 애너테이션 처리 프로그램

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

3. 배열 매개변수를 받는 애너테이션

배열 매개변수를 받는 애너테이션 타입

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionArrayTest { Class<? extends Throwable>[] value(); }


#### 매개변수 하나짜리 애너테이션을 사용한 프로그램의 예
```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);
    }
}

4. 반복 가능한 애너테이션

반복 가능한 애너테이션 타입

import java.lang.annotation.*;

// 반복 가능한 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestConatiner.class)
public @interface ExceptionRepeatTest {
    Class<? extends Throwable> value();
}
import java.lang.annotation.*;

// 컨테이너 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestConatiner {
    ExceptionRepeatTest[] 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);
    }
}

🍑 결론

애너테이션이 명명 패턴보다 낫다. 따라서 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다. 자바가 제공하는 표준 애너테이션 타입들을 사용해야 한다.(아이템 40) IDE나 정적 분석 도구가 제공하는 애너테이션을 사용하면 도구를 바꾸거나 표준이 만들어지면 수정 작업을 거쳐야한다는 점에 유의하자.

Referenced by