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를 붙이는 습관 을 들이세요.
@Deprecated
더 이상 사용을 권장하지 않는 API임을 표시합니다. Java 9+에서는 forRemoval과 since 속성이 추가되었습니다.
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 등 현대 자바 기술 스택의 핵심 기반이 바로 이 애너테이션 기능입니다!