(7) Spring Boot Custom Annotation

728x90

어노테이션 구성과 동작 원리

커스텀 어노테이션은 메타 어노테이션을 사용하여 다음과 같은 구조를 가진다. 메타 어노테이션이란 커스텀 어노테이션을 구성할 때 시점, 위치 등을 지정하기 위한 어노테이션이다. 어노테이션의 필드 타입은 enum, String 이나 기본 자료형, 기본 자료형의 배열만 사용할 수 있다.

@Targer({ElementType.$적용대상})
@Retention(RetentionPolicy.$정보유지되는대상)
public @interface $어노테이션명{
	public 타입 elementName() $default값
    ....
}

메타 어노테이션의 종류는 다음과 같다.

@Retention : 컴파일러가 어노테이션을 다루는 방법을 기술, 어느 시점까지 영향을 미치는지를 결정
RetentionPolicy.SOURCE : 컴파일 전까지만 유효
RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유효
RetentionPolicy.RUNTIME : 컴파일 이후 런타임 시기에도 JVM에 의해 참조가 가능(리플렉션)

 

@Target : 어노테이션 적용할 위치 선택
ElementType.PACKAGE : 패키지 선언
ElementType.TYPE : 타입 선언
ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언
ElementType.CONSTRUCTOR : 생성자 선언
ElementType.FIELD : 멤버 변수 선언
ElementType.LOCAL_VARIABLE : 지역 변수 선언
ElementType.METHOD : 메서드 선언
ElementType.PARAMETER : 전달인자 선언
ElementType.TYPE_PARAMETER : 전달인자 타입 선언
ElementType.TYPE_USE : 타입 선언

 

@Documented : 해당 어노테이션을 Javadoc에 포함시킴

@Inherited : 어노테이션의 상속을 가능하게 함
@Repeatable : Java8 부터 지원하며, 연속적으로 어노테이션을 선언할 수 있게 함


어노테이션 동작 원리

어노테이션 타입 선언은 특별한 종류의 인터페이스로 친다. 일반적인 인터페이스와 구분하기 위해 @기호 + interface 를 붙여 선언한다. 기술적으로 이 둘은 공백으로 분리가 가능하지만 스타일 문제로 분리하지 않는 것을 권장한다고 한다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = ElementType.METHOD)
public @interface CustomAnnotation {
  public String value();
}

@interface 는 자동으로 Annotation 클래스를 상속(확장) 하며, 내부의 메소드들은 abstract 키워드가 자동으로 붙게 된다. 따라서 어노테이션 인터페이스는 extends 절을 가질 수 없으며, 추가적으로 다음과 같은 제약이 존재한다.

  • 어노테이션 타입 선언은 제너릭일 수 없다.
  • 메소드는 매개변수를 가질 수 없다.
  • 메소드는 타입 매개변수를 가질 수 없다.
  • 메소드 선언은 throws 절을 가질 수 없다.

어노테이션 그 자체로는 아무것도 해주는 일이 없다. 어노테이션은 추가적인 정보를 제공해주는 메타데이터이다.

어노테이션의 역할은 정보를 가짐으로써 끝났다고 할 수 있다. 

이제 이 정보를 이용하는 역할을 하는 다른 누군가가 필요한 것이다.

 

정보를 이용하려면 일단 어노테이션에 대한 접근이 필요하다.

클래스 메소드와 필드에 관한 어노테이션 정보를 얻고 싶으면, 리플렉션을 이용해서 얻어야 한다.

리플렉션을 이용해 가져온 정보를 토대로 다양한 작업을 수행할 수 있게 되는 것이다.


Spring Framework 의 어노테이션

스프링은 @Component, @Service, @Controller, @Repository 등등 수많은 어노테이션을 지원한다.

@Component 어노테이션의 구조를 살펴보자

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
    String value() default "";
}

위에서 만든 커스텀 어노테이션과 비슷한 모습을 볼 수 있다.

@Component 어노테이션을 붙인 클래스를 하나 작성했다면, 스프링은 구동 단계에서 protected Set<BeanDefinitionHolder> doScan(String... basePackages){...} 를 사용해 ClassPath 내에 있는 패키지의 모든 클래스를 읽어서, 어노테이션이 붙은 클래스에 대해 컨테이너에 빈 등록 등의 작업을 수행하는 것이고, 이제 런타임 시에 자동으로 의존성 주입 등에 사용될 수 있는 것이다.


커스텀 어노테이션 만들기

이제 커스텀 어노테이션을 생성하고, 이를 적용시켜 리플렉션을 통해 정보를 가져와 응용하는 간단한 샘플을 만들어 보려고 한다.


@Target의 값을 ({ElementType.TYPE}) 으로 사용하는 예시
실제로 이런식으로는 사용 안 할 것 같지만 한 번 해본다.

PersonInfo.java
클래스 생성시 mention이라는 인삿말을 넣어주기 위한 어노테이션 생성

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PersonInfo {

	String mention() default "HI";
}

Person.java
어노테이션 값에 인사하는 인삿말을 전달

@PersonInfo(mention = "HELLO!")
public class Person {
	private String name;
	private int age;
	
	public Person(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

Answer.java
어노테이션 값에 인삿말을 전달

@PersonInfo(mention = "안녕!")
public class Answer {
	private String name;
	private int age;
	
	public Trash(String name, int age) {
		super();
		this.name = name;
		this.age = age;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
}

PersonService.java
Person과 Answer 인스턴스를 생성하여 런타임 시 어노테이션을 읽어 mention을 출력

import java.lang.annotation.Annotation;

public class PersonService {

	public static void main(String[] args) {
		PersonService p = new PersonService();
		p.printPerson(new Person("kim", 20));
		p.printTrash(new Trash("park", 28));
	}
	
	public void printPerson(Person p) {
		Annotation[] annotations = Person.class.getDeclaredAnnotations();
		for(Annotation annotation : annotations) {
			if (annotation instanceof PersonInfo) {
				PersonInfo personInfo = (PersonInfo) annotation;
				System.out.println(p.getName() + "(" + p.getAge() + ") 가 말합니다 : " + personInfo.mention());
			}
		}
	}
	public void printAnswer(Answer p) {
		Annotation[] annotations = Answer.class.getDeclaredAnnotations();
		for(Annotation annotation : annotations) {
			if (annotation instanceof PersonInfo) {
				PersonInfo personInfo = (PersonInfo) annotation;
				System.out.println(p.getName() + "(" + p.getAge() + ") 가 말합니다 : " + personInfo.mention());
			}
		}
	}
}

PersonService.java의 main메소드 실행 결과

kim(20) 가 말합니다 : HELLO!
park(28) 가 말합니다 : 안녕!

 

728x90