[Java] 55. 애노테이션

2025. 7. 1. 17:04·Java/Reflection, Annotation
애노테이션

애노테이션

#Java/adv2


/애노테이션이 필요한 이유

  • 리플렉션으로 해결하지 못했던 문제들
    • 요청 이름과 메서드 이름을 다르게 하고 싶다면?
    • / , /favicon.ico 와 같이 자바 메서드 이름으로 처리하기 어려운 URL은 어떻게 처리할까?
  • 해결 방안: 메서드에 추가 정보를 적어두고, 추가 정보를 통해 메서드를 리플렉션으로 찾고 호출해보자.
    • 프로그램 실행 중에 읽어서 사용할 수 있는 주석이 필요하다. 애노테이션

/애노테이션 예제

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

@Retention(RetentionPolicy.RUNTIME)
public @interface SimpleMapping {
    String value();
}
  • 애노테이션은 @interface 키워드를 사용해서 만든다.
  • @SimpleMapping 이라는 애노테이션을 하나 만든다. 내부에는 String value 라는 속성을 하나 가진다.
  • @Retention: 애노테이션이 유지되는 기간을 지정한다. 자세한 건 뒤에서… (Retention: 보유)

public class TestController {
    @SimpleMapping(value = "/")
    public void home() {
        System.out.println("TestController.home");
    }

    @SimpleMapping(value = "/site1")
    public void page1() {
        System.out.println("TestController.page1");
    }
}
  • 애노테이션을 사용할 때는 @ 기호로 시작한다.
  • 참고로 애노테이션은 프로그램 코드가 아니다. 예제에서 애노테이션이 붙어있는 home(), page1() 같은 코드를 호출해도 프로그램에는 아무런 영향을 주지 않는다. 마치 주석과 비슷하다고 이해하면 된다. 다만 일반적인 주석이 아니라, 리플렉션 같은 기술로 실행 시점에 읽어서 활용할 수 있는 특별한 주석이다.

애노테이션 메타 정보

public class TestControllerMain {
    public static void main(String[] args) {
        TestController testController = new TestController();
        Class<? extends TestController> aClass = testController.getClass();
        for (Method method : aClass.getDeclaredMethods()) {
            SimpleMapping simpleMapping = method.getAnnotation(SimpleMapping.class);
            if (simpleMapping != null) {
                System.out.println("[" + simpleMapping.value() + "] -> " + method);
            }
        }
    }
}
  • SimpleMapping simpleMapping = method.getAnnotation(SimpleMapping.class);
    • 리플렉션이 제공하는 getAnnotation() 메서드를 사용하면 붙어있는 애노테이션을 찾을 수 있다.
  • simpleMapping.value()를 사용해서 찾은 애노테이션에 지정된 값을 조회할 수 있다.

/애노테이션 정의

  • Annotation: 주속, 메모
  • 애노테이션은 코드에 추가적인 정보를 주석처럼 제공한다.
    • 하지만 일반 주석과 달리, 애노테이션은 컴파일러나 런타임에서 해석될 수 있는 메타데이터를 제공한다.
    • 애노테이션은 코드에 메모를 달아놓는 것처럼 특정 정보나 지시를 추가하는 도구로, 코드에 대한 메타데이터를 표현하는 방법이다.
      • 주석은 프로그램 동작에 아무런 영향을 주지 않지만, 애노테이션은 컴파일 시점까지 or 런타임 시점까지 살려둘 수 있다 리플렉션을 통해 애노테이션을 읽어서 동작을 지시할 수 있다.

@Retention(RetentionPolicy.RUNTIME)
public @interface AnnoElement {
    String value();
    int count() default 0;
    String[] tags() default {};

    // MyLogger data(); // 다른 타입은 적용X
    Class<? extends MyLogger> annoData() default MyLogger.class; // 클래스 정보는 가능
}
  • 애노테이션은 @interface 키워드로 정의한다.
  • 애노테이션은 속성을 가질 수 있는데, 인터페이스와 비슷하게 정의한다.
  • /애노테이션 정의 규칙
    • 데이터 타입
      • 기본 타입 (int, float, boolean 등)
      • String
      • Class (메타데이터) 또는 인터페이스
        • 우리가 정의한 인터페이스가 아니라, 인터페이스의 정보가 Class에 들어갈 수 있다는 말임
      • enum
      • 다른 애노테이션 타입
      • 위의 타입들의 배열
      • 앞서 설명한 타입 외에는 정의할 수 없다. 쉽게 이야기해서 일반적인 클래스를 사용할 수 없다.
        • 예: Member, User, MyLogger
    • default 값
      • 요소에 default 값을 지정할 수 있다.
    • 요소 이름
      • 메서드 형태로 정의된다.
      • 괄호()를 포함하되 매개변수는 없어야 한다.
    • 반환 값: 없음
      • void를 반환 타입으로 사용할 수 없다. 반환 타입을 그냥 안적으면 됨.
    • 예외
      • 예외를 선언할 수 없다.
    • 특별한 요소 이름
      • value라는 이름의 요소를 하나만 가질 경우, 애노테이션 사용 시 요소 이름을 생략할 수 있다.
  • /애노테이션 사용
    • @AnnoElement(value = "data", count = 10, tags = {"t1", "t2"}) public class ~~
    • @AnnoElement(value = "data", tags = "t1")
      • default 항목은 생략 가능
      • 배열의 항목이 하나인 경우 {} 생략 가능
    • @AnnoElement("data")
      • 입력 요소가 하나인 경우 value 키워드 생략 가능
  • 애노테이션 값 조회
    • Class<ElementData1> annoClass = ElementData1.class;
    • AnnoElement annotation = annoClass.getAnnotation(AnnoElement.class);
    • String value = annotation.value(); 이거 말고도 count, tags 다 조회 가능.

/메타 애노테이션

  • 애노테이션을 정의하는데 사용하는 특별한 애노테이션을 메타 애노테이션이라고 함.
  • @Retention : 애노테이션의 생존 기간을 지정한다.
    • ex) @Retention(RetentionPolicy.RUNTIME)
      • RetentionPolicy.SOURCE : 소스 코드에만 남아있다. 컴파일 시점에 제거된다.
      • RetentionPolicy.CLASS : 컴파일 후 class 파일까지는 남아있지만 자바 실행 시점에 제거된다. (기본 값)
      • RetentionPolicy.RUNTIME : 자바 실행 중에도 남아있다. 대부분 이 설정을 사용한다.
    • 리플렉션 자체가 런타임중에 메타데이터를 읽는 기술이기 때문에, RetentionPolicy.RUNTIME을 대부분 사용한다. 애노테이션을 만들 때 RetentionPolicy를 꼭 지정해주자.
  • @Target : 애노테이션을 적용할 수 있는 위치를 지정한다.
    • ex) @Target(ElementType.ANNOTATION_TYPE)
      • @Target(ElementType.TYPE) 클래스(타입)에 붙일 수 있음
      • @Target(ElementType.METHOD) 메서드에 붙일 수 있음
      • 이거 말고도 FIELD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE, TYPE_PARAMETER, TYPE_USE, MODULE, RECORD_COMPONENT 요렇게 존재한다.
  • @Documented : 자바 API 문서를 만들 때 해당 애노테이션이 함께 포함되는지 지정한다. 보통 함께 사용한다.
  • @Inherited: 자식 클래스가 애노테이션을 상속 받을 수 있다.
  • /적용 예시
    • @Target({ElementType.METHOD, ElementType.TYPE}) public @interface Annometa { ...
      • Annometa 애노테이션을 필드에 적용하면 컴파일 오류가 발생한다.
    • @Retention(RetentionPolicy.CLASS)로 적용하고 리플렉션을 통해 애노테이션을 조회하면 결과는 null이다.

/애노테이션과 상속

  • 모든 애노테이션은 java.lang.annotation.Annotation 인터페이스를 묵시적으로 상속 받는다.
package java.lang.annotation;

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
}

java.lang.annotation.Annotation 인터페이스는 개발자가 직접 구현하거나 확장할 수 있는 것이 아니라, 자바 언어 자체에서 애노테이션을 위한 기반으로 사용된다. 이 인터페이스는 다음과 같은 메서드를 제공한다.

  • boolean equals(Object obj) : 두 애노테이션의 동일성을 비교한다.
  • int String hashCode() : 애노테이션의 해시코드를 반환한다.
  • toString() : 애노테이션의 문자열 표현을 반환한다.
  • Class<? extends Annotation> annotationType() : 애노테이션의 타입을 반환한다.

모든 애노테이션은 기본적으로 Annotation 인터페이스를 확장하며, 이로 인해 자바에서 애노테이션은 특별한 형태의 인터페이스로 간주된다. 하지만 자바에서 애노테이션을 정의할 때, 개발자가 명시적으로 Annotation 인터페이스를 상속하거나 구현할 필요는 없다. 애노테이션을 @interface 키워드를 통해 정의하면, 자바 컴파일러가 자동으로 Annotation 인터페이스를 확장하도록 처리해준다.


자바가 자동으로 처리

public interface MyCustomAnnotation extends java.lang.annotation.Annotation {}

애노테이션과 상속

  • 애노테이션은 다른 애노테이션이나 인터페이스를 직접 상속할 수 없다.
  • 오직 java.lang.annotation.Annotation 인터페이스만 상속한다.
  • 따라서 애노테이션 사이에는 상속이라는 개념이 존재하지 않는다.
  • /@Inherited: 애노테이션을 정의할 때 @Inherited 메타 애노테이션을 붙이면, 애노테이션을 적용한 클래스의 자식도 해당 애노테이션을 부여 받을 수 있다. 단, 이 기능은 클래스 상속에서만 작동하고, 인터페이스의 구현체에는 적용되지 않는다.
  • /@Inherited가 클래스 상속에만 적용되는 이유
    1. 논리적으로 인터페이스는 “구현”이고, 클래스 상속은 부모의 특성을 물려받는 개념이다. 인터페이스는 메서드의 시그니처만을 정의할 뿐, 상태나 행위를 가지지 않기 때문에, 인터페이스의 구현체가 애노테이션을 상속한다는 개념이 잘 맞지 않는다.
    2. 인터페이스와 다중 구현, 다이아몬드 문제: 인터페이스의 애노테이션을 구현 클래스에서 상속하게 되면 여러 인터페이스의 애노테이션 간의 충돌이나 모호한 상황이 발생할 수 있다.

/애노테이션 활용 - 검증기
검증 메서드

    private static void validateUser(User user) {
        if (user.getName() == null || user.getName().isEmpty()) {
            throw new RuntimeException("이름이 비어있습니다.");
        }
        if (user.getAge() < 1 || user.getAge() > 100) {
            throw new RuntimeException("나이는 1과 100 사이여야 합니다.");
        }
    }
    private static void validateTeam(Team team) {
        if (team.getName() == null || team.getName().isEmpty()) {
            throw new RuntimeException("이름이 비어있습니다.");
        }
        if (team.getMemberCount() < 1 || team.getMemberCount() > 999) {
            throw new RuntimeException("회원 수는 1과 999 사이여야 합니다.");
        }
    }

여기서는 값이 비었는지 검증하는 부분과 숫자의 범위를 검증하는 2가지 부분이 있다. 코드를 잘 보면 뭔가 비슷한 것 같으면서도 User, Team 이 서로 완전히 다른 클래스이기 때문에 재사용이 어렵다. 그리고 각각의 필드 이름도 서로 다르고, 오류 메시지도 다르다.


이후에 다른 객체들도 검증해야 한다면 비슷한 검증 기능을 계속 추가해야 한다.
이런 문제를 애노테이션을 사용해서 해결할 수 있다.


/애노테이션 기반 검증기
@NotEmpty - 빈 값을 검증하는데 사용

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmpty {
    String message() default "값이 비어있습니다.";
}

@Range - 숫자의 범위를 검증하는데 사용

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Range {
    int min();
    int max();
    String message() default "범위를 넘었습니다.";
}

애노테이션 추가

public class User {
    @NotEmpty(message = "이름이 비어있습니다.")
    private String name;

    @Range(min = 1, max = 100, message = "나이는 1과 100 사이여야 합니다.")
    private int age;
	
	// 생략
}

public class Team {
    @NotEmpty(message = "이름이 비어있습니다.")
    private String name;

    @Range(min = 1, max = 999, message = "회원 수는 1과 999 사이여야 합니다.")
    private int memberCount;

	// 생략
}

검증기

public class Validator {
    public static void validate(Object obj) throws Exception {
        Field[] fields = obj.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);

            if (field.isAnnotationPresent(NotEmpty.class)) {
                String value = (String) field.get(obj);
                NotEmpty annotation = field.getAnnotation(NotEmpty.class);
                if (value == null || value.isEmpty()) {
                    throw new RuntimeException(annotation.message());
                }
            }

            if (field.isAnnotationPresent(Range.class)) {
                long value = field.getLong(obj);
                Range annotation = field.getAnnotation(Range.class);
                if (value < annotation.min() || value > annotation.max()) {
                    throw new RuntimeException(annotation.message());
                }
            }
        }
    }
}
  • 전달된 객체에 선언된 필드를 모두 찾아서 @NotEmpty, @Range 애노테이션이 붙어있는지 확인한다.
    • isAnnotationPresent()
  • 애노테이션이 있는 경우 각 애노테이션의 속성을 기반으로 검증 로직을 수행하고, 검증에 실패하면 애노테이션에 적용한 메시지를 예외에 담아 던진다.

개선된 검증 로직

User user = new User("user1", 0);
Team team = new Team("", 0);
try {
    log("== user 검증 ==");
    Validator.validate(user);
} catch (Exception e) {
    log(e);
}
try {
    log("== team 검증 ==");
    Validator.validate(team);
} catch (Exception e) {
    log(e);
}

  • 검증용 애노테이션과 검증기를 사용한 덕분에, 어떤 객체든지 애노테이션으로 간단하게 검증할 수 있게 되었다.
  • 다양한 클래스에서 공통된 검증 로직을 재사용할 수 있게 되었다
  • 새로 추가되는 클래스나 필드에 대해서도 복잡한 로직을 별도로 구현할 필요 없이 적절한 애노테이션을 추가하는 것만으로 검증 로직을 쉽게 확장할 수 있다

/자바 기본 애노테이션

  • /@Override: 메서드 재정의가 정확하게 잘 되었는지 컴파일러가 체크하는데 사용한다.
    package java.lang;
    import java.lang.annotation.*;
    
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE) // 컴파일 할 때만 사용함.
    public @interface Override {
    }
    
    • @Override를 붙였는데, 부모 클래스나 인터페이스에 해당 메서드가 존재하지 않으면 컴파일 오류를 발생시켜준다.
    • RetentionPolicy.SOURCE
      • @Override 는 컴파일 시점에만 사용하는 애노테이션이다. 런타임에는 필요하지 않으므로 이렇게 설정되어 있다.

  • /@Deprecated: 더 이상 사용되지 않는다는 뜻이다. 이 애노테이션이 적용된 기능은 사용을 권장하지 않는다
    package java.lang;
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
    public @interface Deprecated {}
    
    • @Deprecated 만 있는 코드를 사용할 경우 IDE에서 경고를 나타낸다.
    • @Deprecated + forRemoval 이 있는 경우 IDE는 빨간색으로 심각한 경고를 나타낸다.
      • forRemoval: 향후에 사라질 기능이다~.

  • /@SuppressWarnings: 경고를 억제하는 애노테이션. 자바 컴파일러가 문제를 경고하지만, 개발자가 해당 문제를 잘 알고 있기 때문에, 더는 경고하지 말라고 지시하는 애노테이션이다.
    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SuppressWarnings {
        String[] value();
    }
    
    • @SuppressWarnings 에 사용하는 대표적인 값들
      • all: 모든 경고를 억제
      • deprecation: 사용이 권장되지 않는(deprecated) 코드를 사용할 때 발생하는 경고를 억제
      • unchecked: 제네릭 타입과 관련된 unchecked 경고를 억제
      • serial: Serializable 인터페이스를 구현할 때 serialVersionUID 필드를 선언하지 않은 경우 발생하는 경고를 억제
      • rawtypes: 제네릭 타입이 명시되지 않은(raw) 타입을 사용할 때 발생하는 경고를 억제
      • unused: 사용되지 않는 변수, 메서드, 필드 등을 선언했을 때 발생하는 경고를 억제

  • 앞서 설명한 @Retention, @Target 도 자바 언어가 기본으로 제공하는 애노테이션이지만, 이것은 애노테이션 자체를 정의하기 위한 메타 애노테이션이다.

Ref) 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션 강의 | 김영한 - 인프런

'Java > Reflection, Annotation' 카테고리의 다른 글

[Java] 56. HTTP 서버에 Reflection & Annotation 활용  (0) 2025.07.01
[Java] 54. 리플렉션(Reflection)  (0) 2025.07.01
'Java/Reflection, Annotation' 카테고리의 다른 글
  • [Java] 56. HTTP 서버에 Reflection & Annotation 활용
  • [Java] 54. 리플렉션(Reflection)
lumana
lumana
배움을 나누는 공간 https://github.com/bebeis
  • lumana
    Brute force Study
    lumana
  • 전체
    오늘
    어제
    • 분류 전체보기 (456)
      • Software Development (27)
        • Performance (0)
        • TroubleShooting (1)
        • Refactoring (0)
        • Test (8)
        • Code Style, Convetion (0)
        • DDD (0)
        • Software Engineering (18)
      • Java (71)
        • Basic (5)
        • Core (21)
        • Collection (7)
        • 멀티스레드&동시성 (13)
        • IO, Network (8)
        • Reflection, Annotation (3)
        • Modern Java(8~) (12)
        • JVM (2)
      • Spring (53)
        • Framework (12)
        • MVC (23)
        • Transaction (3)
        • AOP (11)
        • Boot (0)
        • AI (0)
      • DB Access (1)
        • Jdbc (1)
        • JdbcTemplate (0)
        • JPA (14)
        • Spring Data JPA (0)
        • QueryDSL (0)
      • Computer Science (129)
        • Data Structure (27)
        • OS (14)
        • Database (10)
        • Network (21)
        • 컴퓨터구조 (5)
        • 시스템 프로그래밍 (23)
        • Algorithm (29)
      • HTTP (8)
      • Infra (1)
        • Docker (1)
      • 프로그래밍언어론 (15)
      • Programming Language(Sub) (77)
        • Kotlin (1)
        • Python (25)
        • C++ (51)
        • JavaScript (0)
      • FE (11)
        • HTML (1)
        • CSS (9)
        • React (0)
        • Application (1)
      • Unix_Linux (0)
        • Common (0)
      • PS (13)
        • BOJ (7)
        • Tip (3)
        • 프로그래머스 (0)
        • CodeForce (0)
      • Book Review (4)
        • Clean Code (4)
      • Math (3)
        • Linear Algebra (3)
      • AI (7)
        • DL (0)
        • ML (0)
        • DA (0)
        • Concepts (7)
      • 프리코스 (4)
      • Project Review (6)
      • LegacyPosts (11)
      • 모니터 (0)
      • Diary (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
lumana
[Java] 55. 애노테이션
상단으로

티스토리툴바