JAVA/Effective Java

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

꾸준함. 2024. 2. 28. 00:17

1. 명명 패턴의 문제점

메서드나 타입의 이름을 특정 규칙으로 짓고 해당 규칙을 지켜 만든 메서드나 타입 등에 추가적인 처리를 제공하는 것을 명명 패턴이라고 합니다.

JUnit은 3 버전까지 테스트 메서드의 이름을 "test"로 시작하도록 규정하였습니다.

그러나 이러한 명명 패턴에는 다음과 같은 단점이 있어서, JUnit 4부터는 @Test 어노테이션을 사용하는 방식으로 변경되었습니다.

 

  • 오타에 취약
  • 올바른 프로그램 요소에서만 사용되리라 보증할 수 없음
  • 프로그램 요소를 매개변수로 전달할 방법이 없음

 

1.1 오타에 취약

 

명명 패턴은 메서드명이나 타입명을 기준으로 구분하기 때문에 오타에 취약합니다.

이전에 언급한대로 JUnit 3 버전까지는 테스트 메서드의 이름이 "test"로 시작해야 했습니다.

그러나 사용자의 실수로 "tset"과 같이 오타가 발생하면 테스트가 실행되지 않아서, 테스트가 통과되었다고 오해할 수 있습니다.

 

1.2 올바른 프로그램 요소에서만 사용되리라 보증할 수 없음

 

JUnit 3 버전에서는 메서드명이 "test"로 시작하면 해당 메서드를 테스트하지만, 클래스명이 "Test"로 시작한다고 해서 클래스 내에 선언된 메서드들에 대해서는 테스트를 수행하지 않습니다.

그러나 이 라이브러리를 사용하는 클라이언트는 클래스명을 규칙에 맞게 지었기 때문에 클래스에 정의된 메서드들에 테스트를 수행할 것이라 예상할 수 있습니다.

그러나 유감스럽게도 클라이언트는 어떠한 경고 메시지도 받지 못하며, 당연한 듯이 의도한 테스트가 수행되지 않습니다.

정리하자면 명명 패턴이나 규칙이 있더라도 언어나 프레임워크 수준에서 강제되지 않기 때문에 개발자가 실수를 범할 수 있으며 이는 주로 컨벤션과 관례에 의존하는 방식에서 나타나는 한계입니다.

 

1.3 프로그램 요소를 매개변수로 전달할 방법이 없음

 

특정 예외를 던질 때 성공하는 테스트를 작성한다고 가정한다면 기대하는 예외 타입을 테스트에 전달해야 합니다.

하지만 명명 패턴 방식으로는 이 것이 현실적으로 불가능합니다.

 

2. 해결책은 어노테이션 사용

어노테이션을 사용할 경우 위에 거론된 문제를 모두 해결할 수 있고 이 때문에 JUnit 4 버전부터 어노테이션 @Test를 전면 도입했습니다.

 

2.1 마커 어노테이션 타입 선언

 

 

 

코드 부연 설명

  • 아래 두 어노테이션은 메타 어노테이션으로 어노테이션의 선언에 사용됨
    • @Retention: 어노테이션 scope 결정 (런타임에도 유지)
    • @Target: 어노테이션이 적용될 대상 결정 (메서드에만 적용)
    • 추가적인 설명은 https://jaimemin.tistory.com/2258 참고
 

[Spring Boot] 메타 애노테이션과 합성 애노테이션

개요 스프링 부트에는 annotation 관련 기능을 많이 제공하고 있는데 저 포함 많은 개발자가 동작 방식을 자세히 모르는 상태로 개발을 하고 있는 것 같습니다. 스프링에서 기본으로 제공하는 애노

jaimemin.tistory.com

 

  • @Test와 같은 어노테이션을 아무 매개변수 없이 단순히 대상에 마킹한다는 뜻에서 마커 어노테이션이라고 함
  • 주석에는 '매개변수 없는 정적 메서드 전용 어노테이션'이라고 작성되어 있지만 이 제약은 컴파일러 자체적으로 강제할 수는 없고 AnnotationProcessor를 적용해야 함

 

2.1.1 마커 어노테이션을 실제로 사용한 예시 코드는 아래와 같습니다.

 

 

코드 부연 설명

  • 위 코드에서 @Test 어노테이션을 @Tset와 같이 오타를 내면 컴파일 에러가 발생하므로, 컴파일 시점에 오류를 파악할 수 있음 (명명 패턴의 첫 번째 단점 해결)
  • @Test 어노테이션이 Sample 클래스의 의미에 직접적인 영향을 주지는 않고 그저 이 어노테이션에 관심 있는 프로그램에게 추가 정보를 제공
  • 정리하자면 프로그램 코드에의 의미는 그대로 둔 채 어노테이션에 관심있는 도구에서 특별히 처리하도록 하는 것
  • @Test 메서드가 m1, m3, m5, m7 메서드에만 적용되어있기 때문에 해당 메서드들에 대해서만 테스트 수행
    • m3와 m7 메서드는 예외를 던지고
    • m1은 성공
    • m5는 성공하지만 @Test의 전제 조건이 '매개변수 없는 정적 메서드 전용 어노테이션'에서만 사용이므로 잘 못 사용된 예 (AnnotationProcessor를 적용 시 실패하도록 처리 가능)

 

2.1.2 마커 어노테이션을 처리하는 프로그램은 아래와 같습니다.

 

 

 

코드 부연 설명

  • m.isAnnotationPresent(Test.class): @Test 어노테이션이 적용된 메서드인지 판별
  • m.invoke(): @Test 어노테이션이 적용된 메서드 실행
  • InvocationTargetException: 테스트 메서드가 예외를 던지면 리프렉션 메커니즘이 InvocationTargetException으로 감싸서 다시 던짐, 그래서 해당 프로그램은 InvocationTargetException에 대해 catch 절을 구성해 원래 예외에 담긴 정보를 getCause()를 출력 
  • 두 번째 catch 블록: 앞서 @Test 어노테이션은 매개변수가 없는 정적 메서드에서만 사용한다고 조건을 걸었는데 해당 조건을 지키지 않고 잘 못 사용했을 경우 발생한 예외를 붙잡아 두 번째 catch 블록에서 예외를 붙잡아 적절한 오류 메시지를 출력
    • 인스턴스 메서드
    • 매개변수가 있는 메서드
    • 호출할 수 없는 메서드
    • etc

 

 

2.2 매개변수 하나짜리 어노테이션 타입 선언

 

특정 예외를 던져야만 성공하는 테스트를 작성해야하는 경우 새로운 어노테이션 타입이 필요합니다.

 

 

 

 

코드 부연 설명

  • 해당 어노테이션의 매개변수 타입은 Throwable을 확장한 클래스의 Class 객체이기 때문에 모든 예외 타입을 수용
    • 아이템 33에서 다룬 한정적 타입 토큰의 활용 사례

 

 

2.2.1 매개변수 하나짜리 어노테이션을 사용한 프로그램

 

 

 

2.2.2 매개변수 하나짜리 어노테이션을 처리하는 프로그램

 

 

 

코드 부연 설명

  • @Test 어노테이션용 코드와 비슷해 보이지만 한 가지 차이라면 위 코드는 어노테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던지는지 확인하는 데 사용
  • 형변환 코드가 없으므로 ClassCastException 걱정 없음
  • 단, 해당 예외의 클래스 파일이 컴파일 타임에는 존재하지만 런타임에는 존재하지 않을 경우 TypeNotPresentException 예외 발생 가능

 

2.3 배열 매개변수를 받는 어노테이션 타입 선언

 

예외를 여러 개 명시하고 그 중 하나가 발생하면 성공하게 만들 수 있습니다.

@ExceptionTest 어노테이션의 매개변수 타입을 Class 객체의 배열로 수정하면 아래와 같습니다.

 

 

 

2.3.1 배열 매개변수를 받는 어노테이션을 사용하는 코드

단일 원소 배열에 최적화했지만 앞서의 @ExceptionTest들도 모두 수정 없이 수용합니다.

원소가 여럿인 배열을 지정할 때는 다음과 같이 원소들을 중괄호로 감싸고 쉼표로 구분해주기만 하면 됩니다.

 

 

 

2.3.2 배열 매개변수를 받는 어노테이션을 처리하는 프로그램

 

 

 

코드 부연 설명

  • Class<? extends Throwable>[] excTypes을 배열 형태로 받아 이미 지정한 Exception 클래스 중에 맞는 클래스가 있는 확인하는 코드

 

2.4 반복 가능한 어노테이션 @Repeatable

 

자바 9버전에는 여러 개의 값을 받는 어노테이션을 배열 매개변수를 사용하는 대신 어노테이션에 @Repeatable 메타 어노테이션을 다는 방식으로도 만들 수 있습니다.

@Repeatable 어노테이션은 하나의 메서드에 여러 개의 어노테이션을 지정할 수 있습니다.

  • @Repeatable을 단 어노테이션을 반환하는 컨테이너 어노테이션을 하나 더 정의
  • @Repeatable에 해당 컨테이너 어노테이션의 class 객체를 매개변수로 전달
  • 컨테이너 어노테이션은 내부 어노테이션 타입의 배열을 반환하는 value 메서드 정의
  • 컨테이너 어노테이션에는 @Retention과 @Target을 적절히 명시해야 하며 그렇지 않을 경우 컴파일 안됨

 

 

 

2.4.1 @Repeatable을 사용한 프로그램

 

 

2.4.2 @Repeatable을 처리하는 프로그램


 

코드 부연 설명

  • @Repeatable 처리 시 주의 필요
    • @Repeatable을 여러 개 달 경우 하나만 달았을 때와 구분하기 위해 해당 컨테이너 어노테이션 타입 적용
    • getAnnotationByType 메서드는 하나만 다는 경우와 여러 개 다는 경우를 구분하지 않아 @ExceptionTest와 @ExceptionTestContainer 모두 가져옴
    • 반면, isAnnotationPresent는 두 케이스를 구분
      • 만약 @ExceptionTest를 여러번 단 다음, isAnnotationPresent로 ExceptionTest를 검사하면 @ExceptionTestContainer로 인식하기 때문에 false'
      • 반대로 @ExceptionTest를 한 번 단 다음, isAnnotationPresent로 ExceptionTestContainer를 검사하면 false

 

  • 정리하면, @Repeatable을 사용한 경우 getAnnotationByType을 사용해 어노테이션 정보를 가져오는 것이 좋음
  • 같은 어노테이션을 여러 번 달 때 @Repeatable을 적용할 경우 코드 가독성이 높아지지만 이를 처리하는 부분에서는 코드 양이 늘어나며 처리 코드가 복잡해지고 오류를 야기할 가능성이 커짐

 

정리

어노테이션을 사용하는 것은 명명 패턴을 사용하는 것보다 확실히 더 나은 방법입니다.

따라서 어노테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유가 없습니다.

자바 프로그래머라면 자바에서 제공하는 어노테이션 타입을 사용하는 것을 권장하며 필요한 경우 커스텀 어노테이션을 정의하고 사용하는 능력을 갖추는 것이 좋습니다.

 

참고

이펙티브 자바

반응형