자바 개발자, 특히 스프링 부트를 사용하는 개발자라면 숨 쉬듯이 어노테이션(Annotation)을 사용한다. @Controller, @Service, @Transactional 등 골뱅이(@) 하나만 붙이면 빈(Bean)이 등록되고 트랜잭션이 관리되는 마법 같은 일들이 일어난다.
하지만 많은 개발자가 어노테이션을 "그냥 붙이면 기능이 생기는 것" 정도로 이해하고 넘어간다. 도대체 이 텍스트 조각이 어떻게 코드를 변화시키는 것일까? 그리고 public @interface라는 선언은 인터페이스와 무슨 관계일까?
이번 글에서는 어노테이션의 본질인 메타데이터와 인터페이스로서의 정체, 그리고 이를 작동시키는 리플렉션(Reflection)의 원리를 파헤쳐본다.
1. 어노테이션의 정체: 메타데이터
사전적 의미로 어노테이션은 '주석'이다. 하지만 우리가 흔히 쓰는 // 나 /* */ 주석과는 목적이 완전히 다르다.
- 일반 주석(Comment): 사람(개발자)에게 정보를 제공하기 위함. 컴파일러는 이를 무시하고 버린다.
- 어노테이션(Annotation): 컴파일러나 프로그램(JVM, 프레임워크)에게 정보를 전달하기 위함.
즉, 어노테이션은 코드 로직에 직접적인 영향을 주지 않는다. 그저 "이 메서드는 특별한 처리가 필요하다"라는 메타데이터(Meta-Data), 즉 견출지(Post-it)를 붙여놓은 것과 같다.
2. 어노테이션은 특수한 인터페이스다
어노테이션을 직접 정의해 본 적이 있다면 키워드가 @interface라는 것을 알 것이다.
public @interface MyAnnotation {
String value();
}
이 코드는 컴파일(javac) 후 바이트코드 레벨에서 아래와 같은 형태의 인터페이스로 변환된다. 모든 어노테이션은 내부적으로 java.lang.annotation.Annotation 인터페이스를 상속받는다.
컴파일된 어노테이션의 실제 구조 (Decompiled)
// 어노테이션은 결국 인터페이스다.
public interface MyAnnotation extends java.lang.annotation.Annotation {
public abstract String value();
}
우리가 어노테이션에 값을 할당하는 행위(@MyAnnotation(value="A"))는, 런타임에 다이내믹 프록시(Dynamic Proxy)를 통해 이 인터페이스의 구현체를 생성하고 값을 반환하도록 메서드를 호출하는 과정과 유사하다.
java.lang.annotation.Annotation 소스 코드
그렇다면 모든 어노테이션의 부모인 java.lang.annotation.Annotation은 어떻게 생겼을까? JDK 소스를 확인해보면 다음과 같다.
/*
* Copyright (c) 2003, 2020, Oracle and/or its affiliates. All rights reserved.
* ...
*/
package java.lang.annotation;
/**
* The common interface extended by all annotation types.
* ...
*/
public interface Annotation {
/**
* Returns true if the specified object represents an annotation
* that is logically equivalent to this one.
*/
boolean equals(Object obj);
/**
* Returns the hash code of this annotation.
*/
int hashCode();
/**
* Returns a string representation of this annotation.
*/
String toString();
/**
* Returns the annotation type of this annotation.
*/
Class<? extends Annotation> annotationType();
}
출처: OpenJDK java.lang.annotation.Annotation
이처럼 어노테이션은 마법의 문법이 아니라, 자바 언어 스펙 내에서 정의된 특수한 인터페이스일 뿐이다.
3. 메타 어노테이션: 설정의 설정
어노테이션을 정의할 때는 "이 견출지를 어디에, 언제까지 붙여둘 것인가"를 정의해야 한다. 이를 설정하는 어노테이션을 메타 어노테이션(Meta-Annotation)이라 한다.
@Target
어노테이션을 부착할 수 있는 위치를 제한한다.
ElementType.TYPE: 클래스, 인터페이스ElementType.METHOD: 메서드ElementType.FIELD: 필드 (멤버 변수)
@Retention (가장 중요)
어노테이션이 유지되는 수명(Life Cycle)을 결정한다. 스프링이 작동하는 원리를 이해하려면 이 부분이 핵심이다.

스프링 프레임워크가 서버 기동 중에 클래스를 스캔해서 빈을 등록하려면, 해당 어노테이션은 반드시 RetentionPolicy.RUNTIME 이어야 한다.
4. 작동 원리: 리플렉션 (Reflection)
어노테이션 자체는 아무런 기능이 없는 메타데이터라고 했다. 그렇다면 실제 기능은 누가 수행하는가?
바로 리플렉션(Reflection) API가 그 역할을 담당한다.
리플렉션은 구체적인 클래스 타입을 알지 못해도 런타임에 그 클래스의 메서드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API다.
스프링 컨테이너의 동작 과정 예시
- 애플리케이션이 실행되면 스프링은 컴포넌트 스캔을 수행한다.
- 리플렉션을 통해 클래스 파일들을 읽어들인다 (
Class<?> clazz = ...). - 클래스나 메서드에 특정 어노테이션(
@Component,@Transactional)이 붙어있는지 검사한다 (isAnnotationPresent). - 붙어있다면 그에 맞는 처리(객체 생성, 프록시 생성 등)를 수행한다.
5. 커스텀 어노테이션 구현 실습
이론을 확인하기 위해, 메서드 실행 시 로그를 출력하는 어노테이션을 직접 만들고 리플렉션으로 처리해보자.
1) 어노테이션 정의
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 살아있어야 리플렉션이 읽을 수 있다.
public @interface PrintLog {
String value() default "기본 로그";
}
2) 비즈니스 로직 적용
public class MyService {
@PrintLog("메서드 실행됨!")
public void doSomething() {
System.out.println(">>> 비즈니스 로직 수행");
}
}
3) 리플렉션 프로세서 구현 (프레임워크의 역할)
import java.lang.reflect.Method;
public class MyFramework {
public static void main(String[] args) throws Exception {
MyService service = new MyService();
// 1. 리플렉션으로 클래스 정보 획득
Class<?> clazz = service.getClass();
// 2. 모든 메서드 순회
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
// 3. 특정 어노테이션이 붙어있는지 확인
if (method.isAnnotationPresent(PrintLog.class)) {
// 4. 어노테이션 정보 추출
PrintLog annotation = method.getAnnotation(PrintLog.class);
System.out.println("[LOG] " + annotation.value());
// 5. 실제 메서드 실행
method.invoke(service);
}
}
}
}
실행 결과
[LOG] 메서드 실행됨!
>>> 비즈니스 로직 수행
MyService 코드 내부에는 출력문([LOG]...)이 없지만, 외부의 MyFramework가 리플렉션을 통해 어노테이션을 감지하고 기능을 주입했다. 이것이 스프링 AOP와 어노테이션 기반 개발의 핵심 원리다.
6. 결론
- 어노테이션은
java.lang.annotation.Annotation을 상속받는 인터페이스다. - 어노테이션 그 자체는 기능이 없는 메타데이터(견출지)일 뿐이다.
- 실제 기능은 리플렉션(Reflection) 기술을 이용해 런타임에 어노테이션을 읽고 처리하는 제3자(프레임워크)가 수행한다.
그러므로 우리가 @SpringBootApplication이나 @Autowired를 사용할 때, 단순히 '마법'이라고 생각하기보다 "스프링이 리플렉션을 통해 이 표시를 읽고, 빈을 주입하거나 설정을 구성하겠구나"라고 이해하는 것이 정확하다.
Reference
'공부 > JAVA' 카테고리의 다른 글
| [JAVA] 배열(Array) vs ArrayList vs Vector (0) | 2026.02.06 |
|---|---|
| [Java] Record는 무엇인가? (0) | 2025.12.05 |
| [Java] 프로세스와 스레드 (0) | 2025.11.27 |
| [Spring]서블릿(Servlet)과 Spring MVC (0) | 2025.11.17 |
