명명 패턴이 뭘까요?
메서드나 타입의 이름을 특정 규칙으로 짓고, 이 규칙을 지켜 만든 메소드나 타입 등에 추가적인 처리를 제공하는 것
ex) JUnit은 test method의 이름을 test로 시작하게끔 하는 것....
명명 패턴은 효과적이지만, 단점도 많다.
명명 패턴의 단점
- 오타
- 메서드나 타입을 이름으로 구분하기 때문에, 오타가 나면 자칫 이 메서드는 무시된 채로 넘어갈 수 있다.
- JUnit의 test method의 경우 이름 실수가 났을 경우 테스트의 성공/실패 자체를 알 수 없기 때문에, 성공된줄 알고 지나칠 수도 있다.
- 올바른 프로그램 요소에서만 사용될 것이라는 보증이 없음
- Junit에서는 테스트 메서드 이름을 test로 시작해야 할뿐, 클래스 이름은 이를 따를 필요가 없지만, 사용자가 클래스에 위 규칙을 적용해 테스트 메서드가 수행되길 기대할 수 있다.
- JUnit은 위 클래스를 무시할 것이고, 사용자는 어떠한 경고 메시지도 받지 못할 뿐더러 당연히 의도한 테스트도 수행할 수 없다.
- 프로그램 요소를 매개변수로 전달할 방법이 마땅치 않다.
- 특정 예외를 던져야만 성공하는 테스트가 있고, 기대하는 예외 타입은 테스트에 매개변수로 전달해야 한다고 가정해보자.
- 예외의 이름을 테스트 메서드 이름에 덧붙이는 방법은 가독성도 나쁘고 깨지기도 쉽다.
- 컴파일러는 메서드 이름에 덧붙인 문자열이 예외인지 알 수 없다.
- 테스트를 실행하기 전까지 존재조차 알 수 없다.
- 특정 예외를 던져야만 성공하는 테스트가 있고, 기대하는 예외 타입은 테스트에 매개변수로 전달해야 한다고 가정해보자.
그럼 명명 패턴 말고 무엇을 사용해야 할까?
애너테이션을 사용하면 된다.
이해를 돕기 위해 테스트 프레임워크를 만들어봅시다.
// test method annotation
// 매개변수 없는 정적 메서드 전용
@Retention(RetentionPolicy.RUNTIME) // -> meta-annotaion. (애너테이션 선언에 다는 애너테이션)
@Target(ElementType.METHOD) // -> meta-annotaion
public @interface Test {
}
@Rentation(RetentionPolicy.RUNTIME)
- 위 메타 애너테이션은 @Test가 런타임에도 유지되어야 한다는 의미이다.
- 이를 생략하면 테스트 도구는 @Test를 인식할 수 없다.
@Target(ElementType.METHOD)
- 위 메타 애너테이션은 @Test가 반드시 메서드 선언에서만 사용되어야 한다는 의미이다.
- 클래스 선언, 필드 선언 등에는 달 수 없는 애너테이션이다.
적절한 애너테이션 처리기 없이 인스턴스 메서드나 매개변수가 있는 메서드에 애너테이션을 달면, 컴파일은 잘 되겠지만 테스트 도구를 실행할 때 문제가 생기게 된다.
marker annotation이란?
: 아무 매개변수 없이 단순히 대상에 마킹한다는 뜻에서 나온 애너테이션 이름.
실제로 적용해 봅시다.
매개변수가 없는 애너테이션을 처리하는 예제
@item39.Test public static void m1(){} //success
public static void m2(){} //fail
@item39.Test public static void m3(){
throw new RuntimeException("fail"); //ignore
}
public static void m4(){} //ignore
@item39.Test public void m5(){} //fail (wrong fail)
public static void m6(){} //ignore
@item39.Test public static void m7(){
throw new RuntimeException("fail"); //fail
}
public static void m8(){} //ignore
- 위 test가 매개변수 없는 정적 메서드 전용이기 때문에, 정적 메서드가 아닌 곳에 @Test를 붙이면 안된다.
- @Test를 붙이지 않으면 테스트 도구는 이를 무시한다.
- 예외를 던지면 결과는 실패를 반환한다.
- 이 외의 경우에서는 성공을 반환한다.
@Test 애너테이션은 위 class의 의미에 직접적인 영향을 주지 않는다. 마커 애너테이션은 표시일 뿐이다
대상 코드의 의미는 그대로 둔 채 해당 애너테이션에 관심 있는 도구에서 특별한 처리를 할 기회(추가정보)를 준다.
테스트 러너를 만들어 테스트를 해보자.
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 e) {
System.out.println("잘못 사용한 테스트 @Test : " + m);
}
}
}
System.out.printf("suc :: %d, fail : %d%n", passed, tests - passed);
}
@Test 애너테이션이 선언된 메서드만을 선별하여 호출한다. 이때 예외가 나지 않으면 테스트는 성공한다.
//예외를 던지면 실패하는 테스트 케이스의 출력
public static void Sample.m3() failed: RuntimeException: Boom
Invalid @Test: public void Sample.m5()
public static void Sample.m7() failed: RuntimeException: Crash
suc : 1, fail : 3
매개변수가 있는 애너테이션을 처리하는 예제
@item39.ExceptionTest(ArithmeticException.class)
public static void m1() {
int i = 1 / 0; //성공
}
@item39.ExceptionTest(ArithmeticException.class)
public static void m2() {
int[] arr = new int[0];
arr[1] = 1; // outOfIndex 예외 발생
}
@item39.ExceptionTest(ArithmeticException.class)
public static void m3() {} //실패(예외 없이)
- m1 : @ExceptionTest 애너테이션에 전달된 ArithmeticException 예외를 던지므로 테스트에 성공한다.
- m2 : ArithmeticException이 아닌 다른 예외를 던지므로 테스트에 실패한다.
- m3 : 예외를 던지지 않으므로 테스트에 실패한다.
예외를 여러 개 명시하고 그 중 하나만 발생해도 성공하게 만들 수 있다.
즉, 배열 매개변수를 받는 애너테이션도 존재한다.
배열 매개변수를 받는 애너테이션을 처리하는 예제
@Retention(RetentionPolicy.RUNTIME) // -> meta-annotaion. (애너테이션 선언에 다는 애너테이션)
@Target(ElementType.METHOD) // -> meta-annotaion
public @interface ExceptionTest {
Class<? extends Throwable>[] value();
}
@item39.ExceptionTest( {ArithmeticException.class, NullPointerException.class})
public static void m4() {
List<String> list = new ArrayList<>();
}
(테스트 러너는 생략하겠음)
아무튼 이러한 방식으로 여러 개의 값을 받는 애너테이션을 만들 수가 있는데,
다른 방식으로도 만들 수 있다.
배열 매개변수가 아니라 애너테이션에 @Repeatable 메타 에너테이션 하나만 달면 된다.
주의할 점은 몇개가 있다.
- @Repeatable을 단 애너테이션을 반환하는 컨에티너 애너테이션을 하나 더 정의하고, @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다.
- 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.
- 컨테이너 애너테이션 타입에는 적절한 보존 정책과 적용 대상(@Retention, @Target)을 명시해야 한다. 이를 지키지 않으면 컴파일되지 않는다.
//이런 식으로..
@Retention(RetentionPolicy.RUNTIME) // -> meta-annotaion. (애너테이션 선언에 다는 애너테이션)
@Target(ElementType.METHOD) // -> meta-annotaion
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest2 {
Class<? extends Throwable>[] value();
}
//Container 애너테이션
@Retention(RetentionPolicy.RUNTIME) // -> meta-annotaion. (애너테이션 선언에 다는 애너테이션)
@Target(ElementType.METHOD) // -> meta-annotaion
public @interface ExceptionTestContainer {
ExceptionTest2[] value();
}
//이런 느낌으로 변경된다.
@item39.ExceptionTest2(IndexOutOfBoundsException.class)
@item39.ExceptionTest2(NullPointerException.class)
public static void m5(){
List<String> list = new ArrayList<>();
}
핵심 정리
여러 예시들을 많이 봐서 알겠지만 (?)
다음 예시들과 같이 명명 패턴을 사용하기 보다는 애너테이션을 사용해 여러 가지를 처리하는것이 낫다.
애너테이션을 새로 정의할 필요는 없고, 자바가 제공하는 애너테이션들을 골라골라 사용하면 된다.
'JAVA > Effective Java' 카테고리의 다른 글
item 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2022.06.14 |
---|---|
item 40. @Override 애너테이션을 일관되게 사용하라 (0) | 2022.06.12 |
item 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2022.06.03 |
item 37. ordinal 인덱싱 대신 EnumMap을 사용하라 (0) | 2022.06.03 |
item 36. 비트 필드 대신 EnumSet을 사용하라 (0) | 2022.05.31 |
댓글