Home Item39 (명명 패턴보다 애너테이션을 사용하라)
Post
Cancel

Item39 (명명 패턴보다 애너테이션을 사용하라)

명명 패턴보다 애너테이션을 사용하라

명명 패턴이란 무엇일까?

1
2
3
4
5
6
7
8
9
10
int studnetAge;
String firstName;

public static final int MAX_HEIGHT = 150;

public void caculateScore() { ... }

public class UserAccount { }

package com.example.project;
  • 위와 같이 우리가 적용하고 있는 Camel Case, 클래스명, 패키지명의 네이밍 방법 및 규칙들이 포함된다.
  • 또 예시로는 JUnit은 버전 3까지 테스트 메서드 이름을 test로 시작하게끔 했다.
    • 이러한 일종의 규약도 명명 패턴에 포함된다.

이러한 명명 패턴은 가독성 및 유지보수성에 큰 도움이 되지만 단점이 있다.

  • 오타가 나면 안된다.
  • 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
    • ex) 메서드가 아닌 클래스 이름을 TestSafetyMechanisms로 JUnit에 던졌는데, JUnit은 클래스 이름에 관심이 없다. 메서드 이름이 아니기 때문에 그냥 무시하고 테스트를 수행하지 않는다.
  • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
    • ex) 특정 예외를 던져야만 성공하는 테스트 메서드가 있다. 기대하는 예외 타입을 테스트에 매개변수로 전달해야 한다. 예외의 이름을 테스트 메서드 이름에 붙일 수도 있지만 가독성이 좋지 않다.

이러한 부분을 애노테이션이 해결해줄 수 있다.

애노테이션을 써보자.

1
2
3
4
5
//마커 애노테이션 타입 선언
@Retenstion(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
  • @Test 애노테이션 타입 선언 자체에도 두 가지의 다른 애노테이션들이 달려있다.
    • @Retension: @Test가 런타임에도 유지되어야 한다는 표시. 이 애노테이션을 생략하면 테스트 도구는 @Test 애노테이션을 인식할 수 없다.
    • @Target: @Test가 반드시 메서드 선언에서만 사용되어야 한다는 것을 알려준다. 따라서 클래스 선언, 필드 선언 등 다른 프로그램 요소에는 달 수 없다.
  • 이와 같이 아무 매개변수 없이 단순히 대상에 마킹한다는 뜻을 가진 애노테이션을 마커 애노테이션이라고 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class SampleInstance {
	@Test
	public static void test1() { }
	
	public static void test2() { }
	
	@Test
	public static void test3() {
		throw new RuntimeException("실패");
	}

	@Test
	public void test4() { } // 정적 메서드가 아니기 때문에
  • @Test 애노테이션을 붙인 메서드는 테스트 도구가 테스트한다.
    • 애노테이션을 붙이지 않은 메서드는 테스트 도구가 무시한다.
  • 인스턴스 메서드인 test4()는 잘못 사용한 경우이다.
    • 보통 테스트 프레임워크가 테스트 메서드를 정적 메서드로 제한한다.
    • 상태를 가지지 않아 테스트 간의 간섭을 피하고 독립성을 보장한다.
    • 정적 메서드는 인스턴스를 생성하지 않아도 되기 때문에 테스트 프레임워크의 구현을 단순화한다.
  • 애노테이션이 SampleInstance 클래스에 직접적인 형향을 끼치지 않는다. 단순히 이 애노테이션에게 관심있는 프로그램에게 추가 정보를 제공할 뿐이다.

물론 이렇게 애노테이션만 선언하고 사용한다고 해서 원하는 동작이 이루어지지는 않는다.

해당 애노테이션을 어떻게 사용할지의 로직을 갖고있는 프로그램이 필요하다. (ex - 테스트 프레임워크)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.lang.reflect.*; //자바 리플렉션 API 사용

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); //정적 메서드인 경우 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);
	}
}			
  • 우리가 사용하는 테스트 프레임워크는 이와 비슷하게 동작한다.
    • 물론 실제로는 더 많은 기능과 복잡한 로직이 있다.
  • 리플렉션을 사용해 클래스와 메서드, 애노테이션 정보를 가져와 애노테이션이 붙은 메서드를 호출했다.

자바 리플렉션 API

  • 런타임에 클래스, 메서드, 필드, 애노테이션 등에 대한 정보를 동적으로 검사하고 조작할 수 있는 기능을 제공한다.
    • 자바 프로그램이 자기 자신을 검사하고 수정할 수 있다.
    • 프레임워크나 라이브러리 개발에서 쓰인다.

간단하게 몇 가지 기능만 확인해보자.

1
Class<?> clazz = Class.forName("com.example.MyClass");
  • 클래스의 이름, 구현된 인터페이스 등등을 가져올 수 있다.
1
2
Constructor<?> constructor = clazz.getConstructor(); 
Object instance = constructor.newInstance();
  • 클래스의 생성자를 가져올 수 있다.
  • 그 생성자를 사용해 객체를 생성할 수 있다.
1
2
Method method = clazz.getMethod("myMethod"); 
method.invoke(instance);
  • 클래스의 메서드 정보를 가져올 수 있다.
  • 이를 사용해 메서드를 호출할 수도 있다.
1
2
Field field = clazz.getField("myField"); 
field.set(instance, "newValue");
  • 클래스의 필드 정보를 가져올 수 있다.
  • 필드 값을 세팅할 수도 있다.
1
2
3
if (method.isAnnotationPresent(MyAnnotation.class)) { 
	MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); 
}
  • 클래스나 메서드, 필드 등에 선언된 애노테이션을 가져올 수 있다.

리플렉션의 장단점

  • 장점
    • 동적으로 객체를 생성하고 조작할 수 있다.
    • 클래스, 메서드, 필드 등의 구조를 분석하고 조작할 수 있어 코드 분석 도구나 프레임워크 개발에 유용하다.
  • 단점
    • 리플렉션을 사용하면 일반 메서드 호출보다 느리다. 메서드, 필드 등에 동적으로 접근하는 데에 추가적인 비용이 발생한다. (성능 문제)
    • 리플렉션을 사용하면 컴파일 타임에 타입 검사를 피할 수 있다. (안전성 문제)

매개변수를 받는 애노테이션 사용법

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElemenetType.METHOD)
public @interface ExceptionTest {
	Class<? extends Throwable> value();
}
  • Class<? extends Throwable> value()
    • 이 메서드의 선언으로 애노테이션에 매개변수를 받을 수 있다.
    • 매개변수는 value 메서드의 매개변수로 전달된다.
1
m.getAnnotation(ExceptionTest.class).value();
  • 테스트 프레임워크에서는 위와 같은 방식으로 매개변수 값을 받아와 사용할 수 있는 것이다.
1
Class<? extends Throwable>[] value();
  • 위와 같이 애노테이션에 배열을 매개변수로 받도록 할 수 있다.

정리

애노테이션을 만들 때는 적절한 보존 정책(@Retenstion)과 적용 대상(@Target)을 명시해야 한다.

애노테이션을 통해 소스 코드에 추가 정보를 제공할 수 있다. 명명 패턴도 가능한 일이지만 오타의 가능성이 문제고, 활용도가 떨어진다. 따라서 애노테이션으로 해결할 수 있는 일이라면 애노테이션을 사용해보자.

물론 도구 제작자를 제외하고는 일반 프로그래머가 애노테이션 타입을 직접 정의하는 일은 거의 없다. 하지만 만들어놓은, 제공되는 애노테이션들을 적극 활용하자. (스프링 AOP, Test 등등)

This post is licensed under CC BY 4.0 by the author.

Item38 (확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라)

Item40 (@Override 애노테이션을 일관되게 사용하라)