[Java] 어노테이션 (+커스텀 어노테이션 만들기)

송재호·2021년 4월 4일
16
post-thumbnail

서론

어노테이션에 대해 잘 모르는 사람도 이클립스 등의 IDE로 Java 개발을 하다 보면 @Override, @SuppressWarings 등의 어노테이션이 꽤 익숙할 것이다(자동으로 달아주니까).
혹은 Spring Framework를 사용해 보았다면 스프링이 제공하는 다양한 어노테이션도 접해보았을 것이다.

하지만 나처럼 아무 생각 없이 블랙박스 테스트 수행하듯 "이거 달면 이렇게 된다"는 식의 막연함만을 가지고 어노테이션을 사용했던 사람들도 있을 것이다. 스스로를 반성하는 의미에서, 이 어노테이션이라는 것에 대해 깊이 있게 알아보고자 글을 작성한다.

어노테이션이란 것이 무엇인지, 왜 사용하는지, 커스텀으로 만들어 사용하는 방법까지 정리해보려고 한다.

** Java에서 기본 제공하는 빌트인 어노테이션에 대해서는 따로 언급하지 않을 것이다.

어노테이션(Annotation)이란?

Java 5(1.5)부터 등장한 기능으로, 한 마디로 요약하면 프로그램에 추가적인 정보를 제공해주는 메타 데이터라고 볼 수 있다. 여기서 메타 데이터란 어플리케이션이 처리해야 할 데이터가 아니라 컴파일 과정과 런타임에서 코드를 어떻게 컴파일하고 처리할 것인지에 대한 정보를 말한다.

이 메타데이터를 잘 이용하면 비즈니스 로직과 분리하여 대상의 벨리데이션 체크, 값 주입, 역할 부여(기능 주입) 등을 수행할 수 있어 체계가 잡혀있는 깔끔한 코드를 작성할 수 있게 된다.

어노테이션은 옵션에 따라 컴파일 전까지만 유효하도록 처리될 수도 있고, 컴파일 시기에 처리(컴파일러가 클래스를 참조할 때까지)될 수도 있고, 런타임 시기에 처리될 수도 있다.

Java의 리플렉션(실행중인 자바 클래스의 정보를 가져오는 기능)을 사용하여 런타임 시기에 어노테이션의 정보를 바탕으로 다양한 기능을 수행할 수 있으므로 어노테이션은 AOP(관점지향 프로그래밍)을 구성하는 데에 많은 도움을 줄 수 있다.

어노테이션 구성과 동작원리 (스프링은 어떻게 사용하는가?)


(커스텀)어노테이션의 구성

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

@Target({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();
}

위의 커스텀 어노테이션을 JDK내장 javap 라는 역어셈블러를 사용해 확인해보면 java.lang.annotation.Annotation 클래스를 상속받음을 알 수 있고, 결과물은 다음과 같아진다.

public interface annotation.CustomAnnotation extends java.lang.annotation.Annotation {
	public abstract java.lang.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 어노테이션을 붙인 클래스를 하나 작성했다면,
이제 스프링은 구동 단계에서 ClassPathBeanDefinitionScanner.java 의 다음 메소드

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 "안녕하세용 ㅎ";
}

Person.java
어노테이션 값에 예의바르게 인사하는 인삿말을 전달

@PersonInfo(mention = "반가워요.")
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;
	}
}

Trash.java
어노테이션 값에 예의없게 대하는 인삿말을 전달

@PersonInfo(mention = "뭘 봐")
public class Trash {
	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과 Trash 인스턴스를 생성하여 런타임 시 어노테이션을 읽어 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 printTrash(Trash p) {
		Annotation[] annotations = Trash.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) 가 말합니다 : 반가워요.
park(28) 가 말합니다 : 뭘 봐
profile
식지 않는 감자

0개의 댓글