이 블로그는 개인의 공부 목적으로 작성된 블로그입니다. 왜곡된 정보가 포함되어 있을 수 있습니다
39 명명 패턴보다 에너테이션을 사용해라
명명 패턴: 전통적으로 도구, 프레임워크에서 특별히 다뤄야하는 경우 특벙 이름으로 이를 구별하는 것
getter, setter과 같은 것들이 명명 패턴이라고 할 수 있겠다.
이러한 명명 태펀을 사용하는경우 몇가지 단점이 존재하는데,
- 오타가 나면 안된다.
- 올바른 프로그램 요소에만 사용되리라 보증 할 방법이 없다.(해당 이름을 쓴다고 무조건 사용되는 것이 아니기 때문에, 클래스에 test를 붙인다고 해서 자동으로 테스트가 되는 것이 아니다.)
- 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.(명명 패턴을 이름으로 표현하는 것이기 떄문에)
이러한 명명 패턴이 문제가 되는 경우는 테스트코드를 작성할 떄 문제될 수 있는데, 아에 테스트하도록한 명명패턴이 작동하지 않으면 테스트 자체가 실행되지 않은 상태로 개발자는 해당 테스트가 실행되지 않았다는 것도 모른체 테스트가 성공했다고 오인할 수 있기 때문이다.
이러한 명명 패턴의 문제점을 해결하기 위해서 애노테이션이 등장한다.
Annotation
주석이자, 프로그램에 추가적인 정보를 제공하는 메타데이터이다.
@interface 형식으로 선언할 수 있다,
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
에노테이션 선언을 보면 다른 에노테이션을 볼 수 있다.
이러한 에노테이션을 메타에노테이션이라고 한다.
@Retention: 에노테이션의 범위를 설정한다.
RetentionPolicy.RUNTIME: 컴파일 이후 런타임 시기에 JVM에 의해 참조가능(런타임에 해당 어노테이션의 의미를 알 수 있다)
RetentionPolicy.CLASS: .class까지 에노테이션이 유지된다. 런타임에는 사라진다.
RetentionPolicy.SOURCE: .java 까지 에노테이션이 유지된다(@Getter, @Setter 가 여기에 해당한다)
RetentionPolicy.CLASS관련해서 재미있는 내용이 있어서 추가하면, .class까지 유지되는 것이 그렇게 의미있어보지않는다. Gradle로 다운 받는 jar 파일같은 경우 소스파일(.java)가 포함되지 않고 .class 파일만 존재하기 때문에 타입체크등 컴파일 단계에서 진행 할 수 없는 경우 이에 해당한다.(컴파일단계에서 추가하면 안될까? 타입체크 관련 코드를 컴파일 단계에서 어노테이션을 보고 추가하는게 왜 안되는지 아직 모르겠다.)
https://jeong-pro.tistory.com/234
@Target: 어노테이션 적용 위치를 설정한다.
ElementType:METHOD: 메소드
ElementType:FIELD: 필드
등등...
또한 적절한 에노테이션 처리기를 구현해야한다.
다음 코드는 테스트 메서드임을 선언하는 에노테이션이다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
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 에노테이션이 붙은 메소드만 Test해야한다.
에노테이션 해당 클래스에 직접적인 영향을 주지않고, 추가정보를 제공할 뿐이다.(직접적인 영향이라는 것이 모호한 느낌이 있는데 일단 기존 클래스정보에 삭제되는 것이 없다고 생각하겠다)
public class RunTests {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item12.Sample1");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(Test1.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);
}
}
리플렉션을 이용하여 @Test 에노테이션이 붙은 메소드를 읽어 invoke함수를 통해 함수를 호출한다 매개변수로 객체를 넣는데, null을 넣었으므로 정적 메소드만 실행가능하게 된다.
특정 예외를 던져야만 성공하는 테스트 에노테이션을 만들어 보자
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value(); // 매개변수
}
이전과 다르게 매개변수 value가 속성으로 추가되었다.
한정적 타입 토큰을 활용하여 Exception의 최상위 클래스인 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() { } // 실패해야 한다. (예외가 발생하지 않음)
}
어노테이션을 선언할때 매개변수을 선언하여 해당 Exception이 발생하는지 테스트 해보자
public class RunTests2 {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item12.Sample2");
for (Method m : testClass.getDeclaredMethods()) {
System.out.println("m.getName() = " + m.getName());
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);
}
}
여러가지 예외에 대해 성공여부를 테스트할 수도 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest2 {
Class<? extends Exception>[] value(); // Class 객체의 배열
}
매개변수로 배열을 value로 추가한다.
@ExceptionTest2({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() { // 성공해야 한다.
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
public class RunTests3 {
public static void main(String[] args) throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item12.Sample2");
for (Method m : testClass.getDeclaredMethods()) {
System.out.println("m.getName() = " + m.getName());
if (m.isAnnotationPresent(ExceptionTest2.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(ExceptionTest2.class).value();
for (Class<? extends Throwable> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
System.out.println( m.getName()+"test success");
break;
}
}
if (passed == oldPassed)
System.out.printf("테스트 %s 실패: %s %n", m, exc);
}
}
}
}
배열 대신 @Repeatable 메타 에노테이션을 사용해서 배열처럼 사용할 수 도 있다.
단 이때 @Repeatable 인자 값인 컨테이너 에노테이션을 하나더 정의해야하고, 컨테이너 에너테이션내부에서 배열을 반환하는 value 메서드를 정의해야한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class) //컨테이너 애너테이션 class 객체
public @interface ExceptionTest3 {
Class<? extends Throwable> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest3[] value(); // value 메서드 정의
}
@ExceptionTest3(IndexOutOfBoundsException.class)
@ExceptionTest3(NullPointerException.class)
public static void doublyBad2() {
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
반복 가능 에너테이션을 사용하면 위처럼 에노테이션을 중첩으로 선언하게 되는데 이때 기존에 사용하던 isAnnotationPresent()함수를 사용하게되면 각각의 에너테이션을 구분하여 에너테이션을 모두 검사하는데 한계가 있다.
에노테이션은 명명패턴대신 사용하자
그런데....
https://techblog.woowahan.com/2684/
40 @Override 에노테이션을 일관되계 사용하라
다음 코드를 보자
public class Bigram {
private final char first;
private final char second;
public Bigram(char first, char second) {
this.first = first;
this.second = second;
}
public boolean equals(Bigram b) {
return b.first == first && b.second == second;
}
public int hashCode() {
return 31 * first + second;
}
public static void main(String[] args) {
Set<Bigram> s = new HashSet<>();
for (int i = 0; i < 10; i++)
for (char ch = 'a'; ch <= 'z'; ch++)
s.add(new Bigram(ch, ch));
System.out.println(s.size());
}
}
실행결과를 예측해보자 알파벳 곗수가 26개 이기 떄문에 Set 자료구조의 특징에 의해 26개 밖에 남지 않는다.
위코드의 문제점을 자세히 보지 않으면 알 수 없는데, equals의 파라미커가 Object 타입이 아니여서 오버로딩이 되지 않는다.....
이런 오버로딩 실수는 빈번하게 발생하는데 @Override 에노테이션을 활용하면 쉽게 확인할 수 있다(일반적으로 idle에서 컴파일로 잡아주기 때문에)
@Override은 가장 많이 사용하는 어노테이션중 하나라고 생각하는데 의식적으로 사용하기 위해 노력하자
@Override public boolean equals(Object o) {
if (!(o instanceof Bigram))
return false;
Bigram b = (Bigram) o;
return b.first == first && b.second == second;
}
41 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
아무 메서드를 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 마커 인터페이스 라고 한다.
대표적으로 @Serializable 이 있는데 본인이 직렬화할 수 있다고 명시하는 것이다.
이러한 마커 에노테이션을 사용함에 따라 기존의 마커 인터페이스의 필요성에 대해 의구심이 들기 하는데,
결론적으로 마커 인터페이스가 마커 에노테이션보다 더 좋다는 주장이다.
첫번째로 마커 인터페이스는 구현 클래스의 인스턴스를 구분하는 용도로 사용할 수 있으나 마커 에노테이션은 그렇지 않다. 마커 에노테이션은 런타임에 되서야 오류를 발견할 수 있다.(에노테이션을 사용해서 컴파일 단계에서 오류가 확인되는 경우를 몇번 경험했었는데 이건 idle에서 해준건가?)
두번쨰로 마커 인터페이스는 적용대상을 더 정말하게 지정할 수 있다.
적용대상을 @Target 을 활용하여 @ElementType.TYPE선언하면 모든 타입에 달 수 있는 에노테이션을 달 수 있는데 이경우 더 세밀하게 제한하지 못한다는 것이다. @ElementType.METHOD @ElementType.CLASS 을 중첩으로 선언할 수 없기 때문에...
그러나 반대의 의견도 있는데, 마커 에노테이션이 거대한 에노테이션 시스템의 지원을 받고 있는다는 점에서 일관성을 지키기 유리하다는 것이다.
요약하자면 클래스와 인터페이스 외의 프로그램 요소를 마킹해야할때 에노테이션을 사용한다 만약 마킹이 된 객체를 매개변수로 받는 메서드로 작성할 일 있으면 당연히 인터페이스를 사용해야한다.
'Java' 카테고리의 다른 글
List.toArray() (0) | 2024.05.19 |
---|---|
[이펙티브 자바] 아이템 47, 48, 49 (2) | 2024.04.27 |
[이펙티브 자바] 아이템 10, 11 (1) | 2024.01.14 |
[이펙티브 자바] 아이템 7,8,9 (1) | 2024.01.13 |
[이펙티브 자바] 아이템 3,4,5,6 (0) | 2024.01.07 |