본문으로 건너뛰기

12.4 애너테이션 (Annotation)

애너테이션(Annotation)은 프로그램의 소스 코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것입니다. 보통 프로그램 실행에 영향을 주지 않고, 컴파일러나 특정 프레임워크가 코드를 분석하거나 런타임에 처리할 수 있도록 메타데이터(Metadata) 를 제공합니다.

기본적으로 주석(// 또는 /* */)과 동일하게 동작하지만, 애너테이션은 기계(컴파일러, 프레임워크 등)도 읽고 처리할 수 있다는 차이점이 있습니다.

1. 애너테이션이란?

// 일반 주석 - 컴파일러와 런타임이 무시
// 이 메서드는 부모 클래스의 메서드를 오버라이드합니다

// 애너테이션 - 컴파일러가 실제로 검사
@Override // 부모 클래스에 동일한 메서드가 없으면 컴파일 에러 발생!
public String toString() {
return "MyClass";
}

애너테이션은 크게 세 가지 용도로 사용됩니다:

용도설명예시
컴파일러 힌트컴파일 시 경고/오류 제어@Override, @SuppressWarnings
빌드 도구 처리코드 자동 생성 등Lombok의 @Getter, @Setter
런타임 처리프레임워크가 동작 방식 결정Spring의 @Component, @Autowired

2. 표준 애너테이션 (Built-in Annotations)

자바에서 기본적으로 제공하는 대표적인 표준 애너테이션들입니다.

@Override

부모 클래스나 인터페이스의 메서드를 오버라이드함을 명시합니다.

class Animal {
public String speak() {
return "...";
}
}

class Dog extends Animal {
@Override
public String speak() { // 부모 메서드 오버라이드 - 컴파일러가 검증
return "멍멍!";
}

// @Override
// public String speek() { // 오타! 컴파일 에러로 즉시 발견
// return "오류";
// }
}
@Override를 꼭 붙여야 하는 이유

@Override 없이도 오버라이드는 되지만, 메서드 이름을 오타냈을 때 새 메서드가 만들어져 버그가 생깁니다. 항상 @Override를 붙이는 습관 을 들이세요.

@Deprecated

더 이상 사용을 권장하지 않는 API임을 표시합니다. Java 9+에서는 forRemovalsince 속성이 추가되었습니다.

public class LegacyService {

@Deprecated(since = "2.0", forRemoval = true)
public void oldMethod() {
System.out.println("구버전 메서드 - 곧 삭제 예정");
}

public void newMethod() {
System.out.println("새 메서드 - 이것을 사용하세요");
}
}

public class DeprecatedExample {
public static void main(String[] args) {
LegacyService service = new LegacyService();
service.oldMethod(); // 컴파일러 경고 발생! "deprecated"
service.newMethod(); // 경고 없음
}
}

@SuppressWarnings

특정 컴파일러 경고를 억제합니다.

import java.util.ArrayList;

public class SuppressWarningsExample {

@SuppressWarnings("unchecked") // 제네릭 미사용 경고 억제
public void rawTypeExample() {
ArrayList list = new ArrayList(); // Raw type 경고 억제
list.add("값");
}

@SuppressWarnings({"unchecked", "deprecation"}) // 여러 경고 억제
public void multipleWarnings() {
// 경고가 발생하는 코드들...
}

// 자주 쓰는 경고 종류:
// "unchecked" - 제네릭 타입 미사용
// "deprecation"- @Deprecated 요소 사용
// "unused" - 미사용 변수/메서드
// "rawtypes" - Raw 타입 사용
// "all" - 모든 경고 억제 (권장하지 않음)
}

@FunctionalInterface

함수형 인터페이스임을 명시합니다. 추상 메서드가 2개 이상이면 컴파일 에러를 발생시킵니다.

@FunctionalInterface
public interface MyFunction<T, R> {
R apply(T t);

// 이 메서드를 추가하면 컴파일 에러: 추상 메서드 2개
// R apply2(T t);

// default와 static 메서드는 추가 가능
default MyFunction<T, R> andThen(MyFunction<R, R> after) {
return t -> after.apply(this.apply(t));
}
}

public class FunctionalInterfaceExample {
public static void main(String[] args) {
MyFunction<String, Integer> getLength = s -> s.length();
System.out.println(getLength.apply("Hello")); // 5

// 람다로 사용 가능 (함수형 인터페이스이므로)
MyFunction<Integer, String> intToStr = n -> "숫자: " + n;
System.out.println(intToStr.apply(42)); // 숫자: 42
}
}

@SafeVarargs

제네릭 가변인자(varargs) 메서드에서 힙 오염(Heap Pollution) 경고를 억제합니다.

import java.util.List;
import java.util.Arrays;

public class SafeVarargsExample {

@SafeVarargs // 타입 안전성을 개발자가 보장
@SuppressWarnings("varargs")
public static <T> List<T> combine(List<T>... lists) {
List<T> result = new java.util.ArrayList<>();
for (List<T> list : lists) {
result.addAll(list);
}
return result;
}

public static void main(String[] args) {
List<String> combined = combine(
List.of("a", "b"),
List.of("c", "d"),
List.of("e")
);
System.out.println(combined); // [a, b, c, d, e]
}
}

3. 메타 애너테이션 (Meta Annotations)

메타 애너테이션은 애너테이션을 위한 애너테이션 으로, 커스텀 애너테이션을 정의할 때 그 애너테이션의 적용 대상이나 유지 기간 등을 지정합니다.

@Target

애너테이션이 적용될 수 있는 대상을 지정합니다.

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

@Target({ElementType.METHOD, ElementType.FIELD}) // 메서드와 필드에만 적용 가능
public @interface MyAnnotation {}

// ElementType 종류:
// TYPE - 클래스, 인터페이스, 열거형
// FIELD - 필드(멤버변수), enum 상수
// METHOD - 메서드
// PARAMETER - 메서드 파라미터
// CONSTRUCTOR - 생성자
// LOCAL_VARIABLE - 지역 변수
// ANNOTATION_TYPE- 애너테이션 타입
// PACKAGE - 패키지
// TYPE_PARAMETER - 타입 파라미터 (Java 8+)
// TYPE_USE - 타입이 사용되는 모든 곳 (Java 8+)

@Retention

애너테이션 정보가 유지되는 기간을 지정합니다.

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

// SOURCE: 소스 파일(.java)까지만 존재. 컴파일 시 제거됨
// → @Override, @SuppressWarnings
@Retention(RetentionPolicy.SOURCE)
public @interface SourceOnly {}

// CLASS: 클래스 파일(.class)까지 유지. JVM 로딩 시 제거됨 (기본값)
// → 바이트코드 분석 도구에서 사용
@Retention(RetentionPolicy.CLASS)
public @interface ClassOnly {}

// RUNTIME: 런타임에도 유지. Reflection으로 읽기 가능
// → Spring @Component, @Autowired 등 대부분의 프레임워크 애너테이션
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimeAnnotation {}

@Documented

JavaDoc 문서에 애너테이션 정보가 포함되도록 합니다.

@Inherited

부모 클래스의 애너테이션이 자식 클래스에 상속됩니다.

import java.lang.annotation.*;

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface InheritedAnnotation {
String value() default "상속됨";
}

@InheritedAnnotation("부모에 적용")
class Parent {}

class Child extends Parent {} // @InheritedAnnotation을 상속받음

public class InheritedExample {
public static void main(String[] args) {
// Child 클래스에서도 @InheritedAnnotation을 찾을 수 있음
InheritedAnnotation anno = Child.class.getAnnotation(InheritedAnnotation.class);
System.out.println(anno != null ? anno.value() : "없음"); // 부모에 적용
}
}

4. 커스텀 애너테이션 만들기

개발자가 직접 애너테이션을 만들어 프레임워크나 Reflection API와 결합해 특정한 동작을 구현할 수 있습니다.

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyCustomAnnotation {
// 애너테이션 요소(속성) 선언
String value() default "Default Value"; // 기본값 설정
int count() default 1;
String[] tags() default {};
boolean enabled() default true;
}

애너테이션 요소 타입 제약

애너테이션 요소로 사용 가능한 타입은 제한됩니다:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TypeConstraints {
// 가능한 타입들:
int intValue() default 0; // 기본 타입 (byte, short, int, long, float, double, boolean, char)
String strValue() default ""; // String
Class<?> classValue() default Void.class; // Class
MyEnum enumValue() default MyEnum.A; // Enum
Override annoValue(); // Annotation (다른 애너테이션)
int[] arrayValue() default {}; // 배열 (위 타입들의 배열)

// 불가능한 타입:
// Object objValue(); // 에러!
// List<String> listValue(); // 에러!
}

enum MyEnum { A, B, C }

5. 리플렉션(Reflection)으로 애너테이션 읽기

@Retention(RetentionPolicy.RUNTIME) 으로 선언한 애너테이션은 런타임에 리플렉션으로 읽을 수 있습니다.

import java.lang.annotation.*;
import java.lang.reflect.Method;

// 커스텀 애너테이션 정의
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@interface Description {
String value();
String author() default "Unknown";
String version() default "1.0";
}

@Description(value = "테스트 클래스", author = "김개발", version = "2.0")
public class ReflectionAnnotationExample {

@Description("일반 메서드")
public void ordinaryMethod() {}

@Description(value = "중요 메서드", author = "이시니어")
public void importantMethod() {}

public static void main(String[] args) throws Exception {
Class<?> clazz = ReflectionAnnotationExample.class;

// 클래스에 적용된 애너테이션 읽기
Description classDesc = clazz.getAnnotation(Description.class);
if (classDesc != null) {
System.out.println("=== 클래스 정보 ===");
System.out.println("설명: " + classDesc.value());
System.out.println("작성자: " + classDesc.author());
System.out.println("버전: " + classDesc.version());
}

// 모든 메서드의 애너테이션 읽기
System.out.println("\n=== 메서드 정보 ===");
for (Method method : clazz.getDeclaredMethods()) {
Description methodDesc = method.getAnnotation(Description.class);
if (methodDesc != null) {
System.out.printf("메서드: %s()%n", method.getName());
System.out.printf(" 설명: %s, 작성자: %s%n", methodDesc.value(), methodDesc.author());
}
}

// 클래스의 모든 애너테이션 목록
System.out.println("\n=== 전체 애너테이션 ===");
for (Annotation anno : clazz.getAnnotations()) {
System.out.println(" " + anno.annotationType().getSimpleName());
}
}
}

6. 실전 예제: @NotNull 커스텀 유효성 검사

프레임워크처럼 애너테이션 기반 유효성 검사 시스템을 직접 구현해봅니다.

import java.lang.annotation.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

// 유효성 검사 애너테이션 정의
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface NotNull {
String message() default "값이 null이면 안 됩니다";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface MinLength {
int value();
String message() default "최소 길이 조건을 만족하지 않습니다";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Range {
int min();
int max();
String message() default "값의 범위가 유효하지 않습니다";
}

// 유효성 검사기
class Validator {
public static List<String> validate(Object obj) throws IllegalAccessException {
List<String> errors = new ArrayList<>();
Class<?> clazz = obj.getClass();

for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true); // private 필드 접근 허용
Object value = field.get(obj);

// @NotNull 검사
NotNull notNull = field.getAnnotation(NotNull.class);
if (notNull != null && value == null) {
errors.add("[" + field.getName() + "] " + notNull.message());
}

// @MinLength 검사
MinLength minLength = field.getAnnotation(MinLength.class);
if (minLength != null && value instanceof String s) {
if (s.length() < minLength.value()) {
errors.add("[" + field.getName() + "] " + minLength.message() +
" (최소 " + minLength.value() + "자, 현재 " + s.length() + "자)");
}
}

// @Range 검사
Range range = field.getAnnotation(Range.class);
if (range != null && value instanceof Integer n) {
if (n < range.min() || n > range.max()) {
errors.add("[" + field.getName() + "] " + range.message() +
" (" + range.min() + "~" + range.max() + ", 현재 " + n + ")");
}
}
}

return errors;
}
}

// 사용자 입력 데이터 모델
class UserInput {
@NotNull(message = "이름은 필수입니다")
@MinLength(value = 2, message = "이름은 2자 이상이어야 합니다")
String name;

@NotNull(message = "이메일은 필수입니다")
String email;

@Range(min = 1, max = 150, message = "나이는 1~150 사이여야 합니다")
int age;

public UserInput(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
}

public class ValidationExample {
public static void main(String[] args) throws Exception {
System.out.println("=== 유효한 입력 ===");
UserInput validUser = new UserInput("김철수", "kim@example.com", 25);
List<String> errors1 = Validator.validate(validUser);
if (errors1.isEmpty()) {
System.out.println("검증 통과!");
}

System.out.println("\n=== 유효하지 않은 입력 ===");
UserInput invalidUser = new UserInput("김", null, 200); // 이름 짧음, 이메일 null, 나이 범위 초과
List<String> errors2 = Validator.validate(invalidUser);
if (!errors2.isEmpty()) {
System.out.println("검증 실패:");
errors2.forEach(e -> System.out.println(" - " + e));
}
}
}

출력 결과:

=== 유효한 입력 ===
검증 통과!

=== 유효하지 않은 입력 ===
검증 실패:
- [name] 이름은 2자 이상이어야 합니다 (최소 2자, 현재 1자)
- [email] 이메일은 필수입니다
- [age] 나이는 1~150 사이여야 합니다 (1~150, 현재 200)

7. 스프링에서의 애너테이션 활용 미리보기

스프링 프레임워크는 애너테이션 기반으로 동작합니다. 위에서 배운 개념이 실제로 어떻게 활용되는지 미리 살펴봅니다.

// 스프링 핵심 애너테이션들 (내부적으로 리플렉션으로 동작)

@Component // 이 클래스를 스프링 Bean으로 등록
class MyService {

@Autowired // 스프링 컨테이너가 의존성을 자동으로 주입
private MyRepository repository;

@Transactional // 이 메서드를 트랜잭션으로 실행
public void saveData() { ... }
}

@RestController // HTTP 요청을 처리하는 컨트롤러
@RequestMapping("/api")
class MyController {

@GetMapping("/users") // GET /api/users 요청 처리
public List<User> getUsers() { ... }

@PostMapping("/users") // POST /api/users 요청 처리
public User createUser(@RequestBody User user) { ... }

@PathVariable // URL 경로 변수 바인딩
@RequestParam // 쿼리 파라미터 바인딩
}
스프링은 왜 애너테이션 기반인가?

스프링은 애플리케이션 시작 시 모든 클래스를 스캔하여 @Component 등이 붙은 클래스를 찾고, 리플렉션으로 인스턴스를 만들고, @Autowired가 붙은 필드에 자동으로 의존성을 주입합니다. 이것이 스프링의 IoC(Inversion of Control) 컨테이너가 동작하는 방식입니다.

고수 팁: 애너테이션 사용 시 주의사항

import java.lang.annotation.*;

// 반복 가능한 애너테이션 (Java 8+)
@Repeatable(Roles.class) // 반복 사용 가능하게 설정
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Role {
String value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Roles {
Role[] value(); // 컨테이너 애너테이션
}

class RoleExample {
@Role("ADMIN")
@Role("USER") // 같은 애너테이션 반복 사용 가능 (Java 8+)
@Role("MODERATOR")
public void multiRoleMethod() {}

public static void main(String[] args) throws Exception {
var method = RoleExample.class.getMethod("multiRoleMethod");
Role[] roles = method.getAnnotationsByType(Role.class);
for (Role role : roles) {
System.out.println("역할: " + role.value());
}
// 역할: ADMIN
// 역할: USER
// 역할: MODERATOR
}
}

스프링 프레임워크나 JPA 등 현대 자바 기술 스택의 핵심 기반이 바로 이 애너테이션 기능입니다!