이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › Java
애너테이션
애너테이션은 무엇이고, 붙이면 무슨 일이 생기는가
애너테이션은 @Run, @Override처럼 @로 시작하는, 코드에 붙이는 표시입니다. 이 실습에서는 @interface Run으로 표시를 직접 만들고 work 메서드 위에 @Run을 붙입니다. 그런데 표시는 그 자체로는 아무것도 실행하지 않습니다. 효과가 생기려면 그 표시를 읽고 무언가를 해 주는 쪽이 있어야 하고, 컴파일러나 스프링 같은 프레임워크, 또는 우리가 쓴 코드가 그 역할을 합니다. 여기서는 "Run이 붙은 메서드를 찾아 부르는" for 문을 우리가 직접 써서 그 관계를 보입니다. @Run을 지우고, 읽는 쪽을 지우고, @Retention을 지워 가며 같은 코드를 컴파일해 실행하고, 어느 메서드가 불리는지를 봅니다.
실행으로 대조 · JDK 21에서 열두 가지 조합을 javax.tools로 컴파일하고 실행해, 불린 메서드와 실행 중에 읽힌 표시까지 대조
이 실습에 나오는 개념
- 애너테이션
- 코드에 붙이는 메타데이터로, 그 자체로는 동작하지 않고 컴파일러나 프레임워크가 읽어서 처리한다. @Override처럼 컴파일러가 검사에 쓰는 것과 @Transactional, @Autowired처럼 프레임워크가 실행 중에 리플렉션으로 읽어 기능을 적용하는 것이 있다.
- @interface
- @interface는 애너테이션(@로 시작하는 표시)을 새로 만드는 선언이다. 일반 인터페이스 선언의 interface 앞에 @를 붙여 @interface Run { }처럼 쓰면 Run이라는 애너테이션이 생기고, 그 뒤로 클래스나 메서드 위에 @Run으로 붙일 수 있다. 중괄호 안이 비어 있으면 값을 담지 않는 표시(마커 애너테이션)이고, 메서드 모양의 선언을 두면 @Name(값)처럼 값을 담는 요소가 된다. 선언만으로는 실행되는 코드가 생기지 않으며, 표시를 읽고 일하는 쪽은 따로 있어야 한다. 직접 만든 애너테이션에 @Retention을 적지 않으면 실행 중에는 남지 않는다.
- 리플렉션
- 실행 중인 프로그램이 클래스의 정보(필드, 메서드, 생성자, 애너테이션)를 조사하고, 이름으로 객체를 생성하거나 메서드를 호출할 수 있는 기능이다. 스프링의 의존성 주입, JPA의 엔티티 처리, JSON 직렬화 라이브러리 등이 이 기능 위에서 동작한다. 일반 호출보다 느리고 컴파일 시점의 타입 검사를 받지 못한다.
- @Retention과 @Target
- 애너테이션을 새로 만들 때 그 애너테이션에 붙여 성질을 정하는 두 애너테이션이다. @Retention은 표시를 어디까지 남길지 정한다. SOURCE는 소스 코드까지만, CLASS는 class 파일까지, RUNTIME은 실행 중까지 남겨 리플렉션으로 읽을 수 있게 한다. 적지 않으면 CLASS가 되므로, 실행 중에 읽을 애너테이션에는 RUNTIME을 반드시 적어야 한다. @Target은 붙일 수 있는 곳(TYPE은 클래스 같은 타입 선언, METHOD는 메서드, FIELD는 필드 등)을 정하며, 맞지 않는 곳에 붙이면 컴파일 오류가 난다.
이렇게 바꿔 보세요
- 처음 설정으로 — @Run이 붙은 work만 불립니다. 부른 것은 표시가 아니라, 표시를 읽은 우리 코드입니다.
- work의 @Run을 지우면 — 읽는 코드는 그대로인데 work가 불리지 않습니다.
- rest에도 @Run을 붙이면 — 읽는 코드는 그대로인데 rest도 불립니다.
- 읽는 코드를 지우면 — @Run이 붙어 있어도 아무 일도 일어나지 않습니다.
- @Retention을 지우면 — @Run이 붙어 있어도 실행 중에는 보이지 않아 아무 메서드도 불리지 않습니다.
처음 설정으로 실행하면
- 컴파일러가 소스를 위에서부터 읽습니다. 먼저 @interface Run 선언을 읽고, Run이라는 표시를 새로 만듭니다. @로 시작하는 표시를 애너테이션이라고 부르고, @interface 뒤에 적은 Run이 그 표시의 이름이 됩니다. Run의 { } 안은 비어 있어서, 아무 값도 담지 않는 표시입니다. 그 위의 @Retention(RetentionPolicy.RUNTIME)은 이 표시를 어디까지 남길지 컴파일러에게 알립니다. RUNTIME은 class 파일에 기록하고 실행 중에도 리플렉션(실행 중에 클래스와 메서드를 조사하는 기능)으로 읽을 수 있게 남기라는 뜻입니다.
- 컴파일러가 Job 클래스를 읽습니다. work 메서드 위에 @Run이 붙어 있습니다. 컴파일러는 이 표시를 class 파일에 실행 중에 읽을 수 있는 표시로 적어 둘 뿐, 메서드를 실행하거나 부르지는 않습니다.
- 컴파일에 성공합니다. 컴파일러가 한 일은 소스를 확인하고 class 파일을 만드는 것까지입니다. work도 rest도 아직 한 번도 실행되지 않았습니다.
- java Main으로 실행합니다. main이 시작되고, new Job()이 Job 객체를 만듭니다. 이 줄에서 work와 rest는 실행되지 않았습니다. 메서드는 그것을 부르는 코드가 있어야 실행됩니다. 메서드 위에 붙은 @Run은 부르는 코드가 아니라 표시일 뿐입니다.
- for 문이 처음 돌면서 name은 "work"입니다. Job.class.getMethod("work")로 work 메서드를 꺼내고, m.isAnnotationPresent(Run.class)로 이 메서드에 Run이 붙어 있는지 실행 중에 묻습니다. 이렇게 묻는 것은 @Run이 아니라 우리가 쓴 이 코드입니다. 답은 true입니다. 소스에 붙인 @Run이 실행 중에 읽을 수 있는 상태로 남아 있기 때문입니다.
- true이므로 if 안으로 들어가 m.invoke(job)이 실행됩니다. invoke가 job 객체의 work 메서드를 부르고, work 메서드가 "work 실행"을 출력합니다. 이것을 출력하게 만든 것은 @Run 자체가 아니라, @Run을 보고 m.invoke(job)을 실행한 우리 코드입니다.
- for 문이 다시 돌면서 name은 "rest"입니다. Job.class.getMethod("rest")로 rest 메서드를 꺼내고, m.isAnnotationPresent(Run.class)로 이 메서드에 Run이 붙어 있는지 실행 중에 묻습니다. 답은 false입니다. 이 메서드에는 @Run이 붙어 있지 않습니다.
- false이므로 if 안으로 들어가지 않아 m.invoke(job)은 실행되지 않습니다. rest 메서드는 불리지 않았고, 아무것도 출력되지 않았습니다.
- for 문이 끝나고 main이 끝납니다. 불린 메서드는 1개이고, 출력은 "work 실행"입니다.
실행 결과
- 애너테이션은 @로 시작하는, 코드에 붙이는 표시입니다. @interface Run으로 표시를 만들고 work 메서드 위에 @Run을 붙였습니다. 그런데 표시는 그 자체로는 아무것도 실행하지 않습니다. work가 불린 것은 Run이 붙은 메서드를 찾아 m.invoke(job)으로 부르는 코드를 우리가 썼기 때문입니다.
- 스프링의 @RestController나 @GetMapping도 마찬가지로, 스프링이 그 표시를 읽어 주기 때문에 효과가 있는 것입니다. 스프링이 읽는 모습은 "컨트롤러" 기초 실습에서 봅니다.
코드
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
// 표시를 만든다. 실행 중에도 읽을 수 있게 남겨 두라고 적는다
@Retention(RetentionPolicy.RUNTIME)
@interface Run { }
class Job {
// 표시: work 메서드에 Run을 붙인다
@Run
public void work() {
System.out.println("work 실행");
}
public void rest() {
System.out.println("rest 실행");
}
}
public class Main {
public static void main(String[] args) throws Exception {
Job job = new Job();
// 읽는 쪽: Run이 붙은 메서드만 찾아 부른다
for (String name : new String[] { "work", "rest" }) {
Method m = Job.class.getMethod(name);
if (m.isAnnotationPresent(Run.class)) {
m.invoke(job);
}
}
}
}
이 모형의 전제
컴파일러와 실행 환경은 JDK 21.0.12.1(javac 21)입니다. 열두 가지 조합을 모두, 화면의 코드와 같은 소스를 javax.tools로 컴파일하고, 컴파일에 성공한 것은 클래스 파일을 새 클래스 로더로 읽어 Main.main을 실행해 표준 출력을 잡은 결과와 대조했습니다. 열두 조합 모두 컴파일에 성공했습니다. 대조한 것은 컴파일 성공 여부, 실행했을 때의 출력, 출력에서 읽은 불린 메서드, 그리고 "실행 중에 읽힌 표시"입니다. 마지막 것은 Main과 따로, 대조 프로그램이 같은 클래스 로더에서 Job 클래스의 work와 rest 메서드에 isAnnotationPresent(Run.class)를 직접 물어 잰 값이며, 읽는 코드가 없는 조합에서도 잽니다. 또 Job의 class 파일에 표시가 어떤 형태로 적혔는지(RuntimeVisibleAnnotations인지 RuntimeInvisibleAnnotations인지)도 확인했고, 화면의 단계 문장이 이 결과에 기댑니다. 문서는 직접 열어 확인했습니다. Java Language Specification(Java SE 21)의 9.6절(@interface로 애너테이션을 선언하고, 요소가 없으면 마커 애너테이션 인터페이스라는 것), 9.6.4.2절(@Retention이 없으면 CLASS로 보고, SOURCE는 class 파일에 남기지 않고, CLASS와 RUNTIME은 class 파일에 기록하며, RUNTIME은 리플렉션이 실행 중에 읽을 수 있게 해야 한다는 것), 9.7절(애너테이션은 프로그램 요소에 정보를 붙이는 표시이고 실행 중의 동작에는 영향을 주지 않는다는 것)과 9.7.2절(마커 애너테이션)을 따랐습니다. java.lang.annotation.Retention과 RetentionPolicy의 API 문서(기본값이 CLASS이고 RUNTIME만 리플렉션으로 읽을 수 있다는 것), java.lang.reflect.AnnotatedElement의 API 문서(isAnnotationPresent)도 확인했습니다. 컴파일러가 표시를 어떤 형태로 class 파일에 적는지는 명세가 정한 것이 아니라 이 javac에서 관찰한 것입니다. 화면의 코드는 이 실습의 설명을 위해 for 문으로 두 메서드를 차례로 보도록 했고, 실제 프레임워크가 표시를 읽는 방식과는 다릅니다. 스프링의 @RestController와 @GetMapping이 스프링이 읽어 주기 때문에 효과가 있다는 문장은 이 실습에서 실행하지 않았고, "컨트롤러" 기초 실습에서 확인한 내용을 가리킵니다. 애너테이션의 값을 담는 요소, @Target, @Override처럼 컴파일러가 읽는 표시, 실행 중에 읽는 프레임워크의 실제 구현은 다루지 않았습니다. 이 밖의 동작은 "세부 동작 실습: 애너테이션"에서 다룹니다.
앞 실습: 스트림 · 다음 실습: 스레드