이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
공부 › Java › 제네릭, 열거형, 애너테이션
애너테이션
@를 붙이기만 하면 정말 무슨 일이 일어나나
애너테이션(@Override, @Timed처럼 @로 시작하는 표시)은 코드에 붙여 두는 꼬리표입니다. 꼬리표 자체는 아무 일도 하지 않습니다. 그것을 읽는 쪽, 곧 컴파일러나 실행 중에 리플렉션으로 읽는 프레임워크가 있어야 의미가 생깁니다. 그리고 실행 중에 읽으려면 꼬리표가 실행 중까지 남아 있어야 합니다.
실행으로 대조 · JDK 21에서 스물여덟 가지 조합을 실제로 컴파일하고 실행해 대조 (class 파일의 내용과 리플렉션 결과 포함)
이 실습에 나오는 개념
- 애너테이션
- 코드에 붙이는 메타데이터로, 그 자체로는 동작하지 않고 컴파일러나 프레임워크가 읽어서 처리한다. @Override처럼 컴파일러가 검사에 쓰는 것과 @Transactional, @Autowired처럼 프레임워크가 실행 중에 리플렉션으로 읽어 기능을 적용하는 것이 있다.
- 리플렉션
- 실행 중인 프로그램이 클래스의 정보(필드, 메서드, 생성자, 애너테이션)를 조사하고, 이름으로 객체를 생성하거나 메서드를 호출할 수 있는 기능이다. 스프링의 의존성 주입, JPA의 엔티티 처리, JSON 직렬화 라이브러리 등이 이 기능 위에서 동작한다. 일반 호출보다 느리고 컴파일 시점의 타입 검사를 받지 못한다.
- @Retention과 @Target
- 애너테이션을 새로 만들 때 그 애너테이션에 붙여 성질을 정하는 두 애너테이션이다. @Retention은 표시를 어디까지 남길지 정한다. SOURCE는 소스 코드까지만, CLASS는 class 파일까지, RUNTIME은 실행 중까지 남겨 리플렉션으로 읽을 수 있게 한다. 적지 않으면 CLASS가 되므로, 실행 중에 읽을 애너테이션에는 RUNTIME을 반드시 적어야 한다. @Target은 붙일 수 있는 곳(TYPE은 클래스 같은 타입 선언, METHOD는 메서드, FIELD는 필드 등)을 정하며, 맞지 않는 곳에 붙이면 컴파일 오류가 난다.
- @Override
- 바로 아래 메서드가 상위 클래스나 인터페이스의 메서드를 재정의한 것임을 나타내는 애너테이션이다. 컴파일러가 이 표시를 읽고 실제로 재정의가 맞는지 확인하며, 아니면 컴파일 오류를 낸다. 메서드 이름이나 매개변수를 잘못 적어 재정의가 아닌 새 메서드를 만들어 버리는 실수를 잡아 준다. 붙이지 않아도 재정의는 되고 프로그램의 동작도 달라지지 않지만, 실수를 막기 위해 재정의하는 메서드에는 항상 붙인다. 소스 코드까지만 남는 애너테이션이라 class 파일에는 적히지 않는다.
- 오버라이딩
- 상위 클래스(또는 인터페이스)의 메서드를 하위 클래스에서 같은 시그니처로 다시 정의하는 것이다. 실행 시점에 실제 객체의 타입에 맞는 메서드가 호출되어 다형성을 구현한다. 접근 범위를 더 좁힐 수 없고, static·private·final 메서드는 오버라이딩할 수 없다.
이렇게 바꿔 보세요
- 처음 설정으로 — @Timed를 붙였고 읽는 코드도 있는데, 아무 일도 일어나지 않습니다.
- RUNTIME으로 남기면 — 읽는 코드가 표시를 찾아 work 앞뒤에 일을 덧붙입니다.
- 읽는 코드가 없으면 — 표시가 실행 중까지 남아 있어도 아무 일도 일어나지 않습니다.
- SOURCE로 하면 — 표시가 class 파일에도 남지 않습니다.
- 붙일 수 없는 곳에 붙이면 — @Target이 TYPE인데 메서드에 붙였습니다. 컴파일 오류입니다.
- @Override 없이 이름을 틀리면 — 오류 없이 컴파일되고, 부모의 메서드가 대신 실행됩니다.
- @Override가 잡아 준다 — 같은 실수를 컴파일러가 오류로 알려 줍니다.
처음 설정으로 실행하면
- @interface Timed 선언을 봅니다: Timed라는 애너테이션을 새로 만드는 문법입니다. 중괄호 안에 실행할 코드가 한 줄도 없습니다. 애너테이션은 "여기에 이런 표시를 붙여 둔다"는 정보일 뿐, 스스로 무엇을 하지는 않습니다.
- @Retention을 적지 않았습니다: @Retention은 표시를 어디까지 남길지 정하는 설정입니다. 적지 않으면 기본값인 CLASS가 적용됩니다. CLASS는 "class 파일까지는 남기되, 실행 중에는 보여 주지 않는다"는 뜻입니다.
- 컴파일러가 work 메서드 위의 @Timed를 검사합니다: @Target(ElementType.METHOD)은 "메서드에만 붙일 수 있다"는 뜻이고, 붙인 곳이 메서드이므로 통과합니다. @Target을 읽고 지키게 하는 쪽은 컴파일러입니다.
- 컴파일해서 Service.class를 만듭니다: @Timed 표시가 class 파일에 적힙니다. 다만 "실행 중에는 보여 주지 않는 칸"(RuntimeInvisibleAnnotations)에 적힙니다.
- Service.class.getDeclaredMethod("work") 실행: 리플렉션으로 work 메서드의 정보를 꺼냅니다. 리플렉션은 실행 중에 클래스가 가진 메서드, 필드, 애너테이션 같은 정보를 조사하는 기능으로, 프레임워크가 애너테이션을 읽을 때 쓰는 방법입니다.
- m.isAnnotationPresent(Timed.class) 실행: work에 @Timed가 붙어 있는지 묻습니다. 결과는 false입니다. 표시가 class 파일에는 있지만, JVM은 CLASS로 남긴 표시를 리플렉션에 보여 주지 않습니다. @Retention을 적지 않은 것이 원인입니다.
- found가 false이므로 읽는 쪽은 아무것도 덧붙이지 않습니다: m.invoke(service)로 work만 실행합니다. 소스 코드에는 @Timed가 분명히 붙어 있는데도, 읽는 쪽에게는 표시가 없는 메서드와 같습니다. 오류도 나지 않습니다.
실행 결과
- 애너테이션을 붙였고 읽는 코드도 있는데 아무 일도 일어나지 않습니다. 표시가 실행 중까지 남지 않았기 때문입니다. 실행 중에 리플렉션으로 읽을 애너테이션을 만들 때는 @Retention(RetentionPolicy.RUNTIME)을 반드시 적습니다. 적지 않으면 기본값이 CLASS라서 오류 없이 조용히 무시됩니다.
- CLASS는 class 파일을 읽어 처리하는 도구를 위한 것으로, 실행 중인 프로그램이 스스로 읽을 수는 없습니다.
코드
@Target(ElementType.METHOD)
@interface Timed { }
class Service {
@Timed
void work() {
System.out.println("work 실행");
}
}
// 여기부터는 main 안의 코드: 애너테이션을 읽는 쪽
Service service = new Service();
Method m = Service.class.getDeclaredMethod("work");
boolean found = m.isAnnotationPresent(Timed.class);
System.out.println("@Timed가 보이는가: " + found);
if (found) System.out.println("[시간 측정 시작]");
m.invoke(service);
if (found) System.out.println("[시간 측정 끝]");
이 모형의 전제
스물여덟 가지 조합(@Timed 쪽 24가지, @Override 쪽 4가지)을 이 PC의 JDK 21에서 화면의 코드로 실제로 컴파일하고 실행해 대조했습니다. 컴파일 여부와 오류가 난 줄, 컴파일러 메시지, 출력뿐 아니라, 만들어진 class 파일의 바이트에 애너테이션이 적혀 있는지(적혀 있다면 RuntimeVisibleAnnotations와 RuntimeInvisibleAnnotations 가운데 어느 칸인지)와 리플렉션의 isAnnotationPresent 결과도 조합마다 직접 확인했습니다. 화면에서 숨겨진 설정은 결과에 영향을 주지 않습니다. 화면의 코드에는 import 문(java.lang.annotation의 것들과 java.lang.reflect.Method)과 main 메서드의 선언이 생략되어 있습니다. @Target(ElementType.TYPE)인 애너테이션을 클래스에 붙이면 컴파일된다는 것도 확인했습니다. 스프링 같은 프레임워크가 애너테이션을 읽는 방식은 이 모형의 "읽는 쪽" 코드와 원리가 같다는 설명이며, 프레임워크를 실제로 실행해 확인한 것은 아닙니다. 컴파일할 때 애너테이션을 읽어 코드를 만들어 내는 도구(애너테이션 프로세서)는 다루지 않았습니다.
앞 실습: 열거형(enum) · 다음 실습: ArrayList와 LinkedList