Java annotations 이란? 동작 원리 설명 + 활용 (커스텀 어노테이션)

DongHyun Kim·2023년 8월 13일
2

이 글의 목표 🚩
annotation 이 무엇인지, 어떻게 작동하는지 이해
annotation 속성 공부
annotation 커스텀 해보기

Annotation 을 이해해야 하는 이유 📖


Java Annotation 을 처음 배울 때는 @Override 같이 소스 코드에 영향을 주지 않는 메타데이터 정보를 추가하는 정도라 생각했다.

하지만, 스프링 공부를 할 수록 Annotation 만 붙이면 Validation 검사나 WebServlet 자동 매핑 getter, setter 자동 생성 등 보일러 플레이트를 획기적으로 줄여주는 역할을 해주면서, 마법같은 일이 벌어졌다.

원리를 알기 위해 정의한 파일에 들어가 코드를 봐도 은닉된 코드가 많았기 때문에 해석하기에 난해했다. 그래서 컴파일 시간에 무슨 일이 일어나는지 대충 알고 넘어갔다.

하지만 자바 공부를 할 수록 더 다양한 어노테이션이 등장했고, 심지어 @Benchmark 같이 성능 측정하는 곳에서도 다양하게 쓰이는 것을 보고, CS 지식도 중요하지만, 내 코드가 뒤에서 어떻게 작동하는지 이해하는 것이 더 필요하다 생각하여, 이번 기회에 정리하고 지나가고자 한다.

Annotation 이해와 작동 원리


자바 어노테이션은 JDK5에 나온 문법으로, 메타데이터 (information)를 우리의 소스 코드 (클래스나 메서드 등) 에 붙이는 용도로 XML 주석이나 Marker Interface 를 대체하는 역할이다.

  • Annotation은 ‘@’ 로 시작한다
  • Annotation은 컴파일된 프로그램을 바꾸지 않는다
  • Annotation은 클래스 이름 앞에 @interface를 붙여서 생성할 수 있다. Example: {접근제어자} @interface {클래스 이름} {} 으로 생성 가능하다

Annotation 을 정의해서 클래스, 메서드, 필드 등에 붙이면, getClass() 로 런타임 클래스를 가져와 리플렉션을 이용해 Annotation이 있는지 없는지에 따라 처리하는 로직을 생성해서 Annotation을 처리한다.
무슨 말인지 밑에 커스텀 어노테이션 부분에서 느낌을 받아보자.

Annotation 카테고리

  1. Marker Annotations
  2. Single value Annotations
  3. Full Annotations

Category 1: Marker Annotations

매개변수가 없이 마크 표시만 하는 Annotation을 의미한다. 필요한 곳에 적기만 하면 충분하다. @Override 가 예시이다

Category 2: Single value Annotations

하나의 멤버를 가지는 Annotation을 의미한다. 사용할 때 멤버에 값을 넣어줘야 한다. 값을 넣을 멤버 이름은 생략 가능하다.

Example: @TestAnnotation("testing");

Category 3: Full Annotations

여러 개의 멤버를 가지는 Annotation을 의미한다

Example: @TestAnnotation(owner="kim", value="developer");

Annotation 속성 공부

위 그림에서 Meta Annotations 은 어노테이션을 만들기 위한 어노테이션이다.

@Target

Annotation이 어디에 위치할 수 있는지 제한해준다 (class, interface, constructor, etc.

@Target(ElementType.TYPE)
@interface CustomAnnotation {}

위의 경우 CustomAnnotation 은 Class, Interface, Enumeration에만 붙일 수 있다.

package com.example.annotation.lombok;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface ClassAnnotation {
    String value() default "Can annotate a class";
}

@Target({ElementType.METHOD, ElementType.TYPE,
        ElementType.ANNOTATION_TYPE,
        ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
@interface MultipleElementTypeAnnotation {

    String value() default "Can annotate a class, method, "
            + "annotation, or constructor";
}

@ClassAnnotation
public class LombokStudy {
    
    public static void main(String[] args) throws Exception {
        LombokStudy obj = new LombokStudy();
        
        Annotation a[] = obj.getClass().getAnnotations();

        System.out.println(a[0]);

        Class<?> className = Class.forName("com.example.annotation.lombok.LombokStudy");
        Annotation b[] = className.getMethod("myMethod")
                .getAnnotations();

        System.out.println(b[0]);
    }

// @ClassAnnotation 붙일 시 컴파일 오류 (빨간색 밑줄) 발생
    @MultipleElementTypeAnnotation
    public void myMethod() {
    }
}

@Retention

코드를 실행 할 때 언제 이 Annotation 이 없앨 지 결정하는 meta-annotation 이다.

  • @Retention(RetentionPolicy.SOURCE) runtime 때 없앤다
  • @Retention(RetentionPolicy.CLASS) .class 파일엔 적혀있지만 runtime 때 없앤다. 특별히 Retention을 지정하지 않았다면 이게 Default
  • @Retention(RetentionPolicy.RUNTIME) runtime 때까지 접근할 수 있다. 위에 예시 코드에서 getAnnotations() 로 접근할 수 있는 이유이다

자주 사용하는 @Getter, @RequiredArgsConstructor 모두 SOURCE 에 해당한다.

그래서 다음과 같이 Customer 클래스가 선언됐을 때, build/classes 아래에 컴파일 된 .class 파일을 보면 Annotation 대신 추가된 코드가 적혀있는 것을 확인할 수 있다!

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@interface RetentionClassAnnotation{
    String value() default "Can not access during runtime";
}

public class LombokStudy {

    public static void main(String[] args) throws Exception {

		Class<?> className = Class.forName("com.example.annotation.lombok.LombokStudy");

        Annotation c[] = className.getMethod("myMethod2")
                .getAnnotations();

        System.out.println(c.length); // 0 출력
    }

    @RetentionClassAnnotation
    public void myMethod2(){

    }
}

위와 같이 @RetentionClassAnnotation 을 만들었다면 .class 파일에선 @RetentionClassAnnotation 이 보이지만 runtime 때 사라져서 getAnnotations() 로 가져올 시 길이 0 을 출력한다 (반환 0개)

@SuppressWarnins

컴파일러의 경고를 억제하도록 알려주는 Annotation 이다.

IBM @SuppressWarnings 문서

@Documented

Javadoc이 만들어질 때 Annotation 이 표시되도록 만드는 Annotation 이다.

geeksforgeeks @Documentation 예시

@Inherited

subclass 가 @Inherited를 붙인 Annotation을 상속받을 수 있도록 표시하는 Annotation 이다.

geeksforgeeks @Inherited 예시

Annotation 커스텀 해보기


만들어 볼 것

@LogExecutionTime 이라는 커스텀 어노테이션의 역할은, 이 어노테이션을 붙인 메서드의 실행시간을 측정하는 기능이다. 메서드의 실행 시점에 따라 처리가 필요하므로 Aspect 를 이용했다.

다음 절차대로 만들어보자

  1. Meta Annotation 을 이용해 Custom Annotation 만들자
  2. @Aspect 를 이용해 @LogExecutionTime 를 붙인 메서드의 로직을 처리하자
  3. @LogExecutionTime 을 붙인 메서드를 만들기 위해 스프링 빈을 생성하자
  4. Custom Annotation을 붙인 메서드를 실행시키기 위해 ApplicationContext 를 이용해서 빈을 가져오자
// LogExecutionTime.java

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}

@LogExecution 은 메서드에 붙이는 Annotation 으로 런타임 때 getAnnotation() 으로 접근할 수 있도록 Retention 설정

// ExampleAspect.java

@Aspect
@Component
@Slf4j
public class ExampleAspect {

    @Around("@annotation(com.example.annotation.aop.LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {

        log.info("Call LogExecutionTime");

        long start = System.currentTimeMillis();

        Object proceed = joinPoint.proceed();

        long executionTime = System.currentTimeMillis() - start;

        log.info(joinPoint.getSignature() + " executed in " + executionTime + "ms");

        return proceed;
    }
}

ExampleAspect 클래스는 @LogExecution 을 붙인 메서드를 AOP 를 이용해 로직을 처리하는 클래스이다. @Component 를 붙여서 빈으로 등록되도록 했다

// LombokStudy.java

// AOP 가 Spring Bean 에서 작동하기 때문에 Service 를 붙여서 빈으로 등록되도록 했다
@Service 
public class LombokStudy {

    @LogExecutionTime
    public void checkExecutionTime() throws InterruptedException {
        Thread.sleep(2000);
    }

LombokStudy 클래스는 @LogExecutionTime 을 사용해보는 메서드를 가지는 클래스이다.

// AnnotationApplicationTests.java

@SpringBootTest
class AnnotationApplicationTests {

    @Autowired
    ApplicationContext ac; 

    @Test
    void test1() throws Exception{
        LombokStudy lombokStudy = ac.getBean("lombokStudy", LombokStudy.class);
        lombokStudy.checkExecutionTime();
    }

}

Test 해보기 위해 Test 클래스에서 진행했다.

테스트 결과

여담

AOP 에서 HttpServletRequest, Response 를 쓰고싶으면 다음 방식을 이용하자

HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();

HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();

출처: AOP 에서 HTTP 이용하기

진행하면서 겪었던 문제⚠️ 들을 정리해보고자 한다.

첫 째로, 처음 LombokStudy 로 메서드를 체크하려 했을 때 new LombokStudy() 로 인스턴스를 만들어서 checkExecutionTime() 메서드를 실행했는데 아무 반응이 없었다.
이유는 스프링 컨테이너의 빈은 싱글톤 패턴 이기 때문에 내가 new 로 생성한 LombokStudy 는 스프링 빈이 아니었기 때문에 @Aspect 가 정상 작동을 안 했던 것이다.

두 번째로, main 메서드에서 ApplicationContext 로 lombokStudy 빈을 가져오려 하니

Exception in thread "main" java.lang.IllegalStateException: org.springframework.context.annotation.AnnotationConfigApplicationContext@427a12b6 has not been refreshed yet 예외가 발생했다
이 오류의 원인은 Application Context 를 Autowired로 빈을 가져오지 않았기 때문에 발생한 문제였다.
자세한 ApplicationContext 의 refresh 과정은
망나니개발자 SpringBoot 소스 코드 분석 이 곳에 잘 설명돼있다.

profile
do programming yourself

0개의 댓글