# Table of Contents

# 어노테이션

Java로 개발을 하다 보면 클래스, 메서드, 변수 앞에 @Override 같은 @ 표시를 많이 봤을 것이다. @어노테이션(Annotation)이라고 한다.

어노테이션은 컴파일 타임이나 런타임에 특정 처리를 하도록 컴파일러에게 정보를 제공한다. 이를 통해 소스코드 작성 단계에서 보일러코드를 제거하고 소스코드를 간결하게 작성할 수 있다. 컴파일 과정에서 어노테이션을 처리하여 데이터를 검증할 수 있으며, 소스코드를 추가하거나 변경할 수도 있기 때문이다.

어노테이션은 크게 세 가지 용도로 사용된다.

  1. 컴파일 과정에서 에러나 경고를 감지하도록 컴파일러에게 정보를 제공
  2. 컴파일 과정에서 특정 코드를 생성하도록 컴파일러에게 정보를 제공
  3. 런타임에서 특정 기능을 실행하도록 정보를 제공

# 어노테이션 사용법

어노테이션을 통해 데이터를 검증하는 예제를 살펴보자. 아래 코드는 두 인자의 합을 반환하는 함수다.

int sum(int a, int b) {
    return a+b;
}

int result = sum(30, 10)

두 인자의 범위를 제한하기 위해 어노테이션을 붙여보자. 자바 API에서 기본으로 제공하는 빌트인 어노테이션 @IntRange를 사용하고 있다.

int sum(@IntRange(from = 0, to = 10) int a, @IntRange(from = 0, to = 10) int b) {
    return a+b;
}

int result = sum(30, 10)    // 아래와 같은 에러가 발생하며 컴파일 자체가 되지 않는다.
// Value must be ≤ 10 (was 20)

어노테이션 덕분에 우리는 sum()함수 안에서 범위를 확인하는 코드를 작성하지 않아도 된다. 이처럼 어노테이션은 보일러 코드를 제거하고 데이터를 검증하는데 활용할 수 있다.

# 커스텀 어노테이션 만들기

커스텀 어노테이션을 만들 때는 키워드 @interface를 사용한다.

@interface CustomAnnotation {
    // ..
}

# 어노테이션 위치

어노테이션은 클래스에 붙일 수 있다.

@CustomAnnotation
class Person {
    // ..
}

어노테이션은 클래스의 멤버변수에도 붙일 수 있다.

class Person {
    
    @CustomAnnotation 
    String name;
    
    Person() {
        this.name ="";
    }
}

메소드에도 붙일 수 있다.

class Person {

    // ...

    @CustomAnnotation
    void printName() {
        // ...
    }
}

메소드 인자에도 어노테이션을 붙일 수 있다.

class Person {

    void printSomething(@CustomAnnotation String something) {
        // ...
    }
}

# 어노테이션은 어떻게 처리되는가?

다음 예제를 살펴보자

int sum(@IntRange(from = 0, to = 10) int a, @IntRange(from = 0, to = 10) int b) {
    return a+b;
}

int result = sum(30, 10)

어노테이션 @IntRange를 통해 데이터 유효성을 검증하고있다. 그렇다면 실제로 유효성을 검사하는 코드는 어디에 존재할까? 이를 이해하기 위해서는 어노테이션 프로세서에 대해 알아야한다.

# 어노테이션 프로세싱

어노테이션 프로세싱은 자바 컴파일러가 컴파일 단계에서 어노테이션을 분석하고 처리하는 기법이다.

자바 컴파일러는 자바 소스코드를 자바 바이트코드로 변환한다. 이때 어노테이션 프로세서(Annotation Processor)를 플러그인 형태로 자바 컴파일러에 등록하면 어노테이션을 처리하게 된다.

# 자바 컴파일러와 어노테이션 프로세서

자바 컴파일러는 등록된 어노테이션 프로세서가 모두 처리될 때까지 반복하여 실행한다. 이 때 어노테이션 프로세서에 구현된 로직에 따라 경고나 에러를 발생시키기도, 추가적인 자바코드를 추가하기도 한다. 자바 컴파일러는 이렇게 최종적으로 완성된 자바 소스코드를 자바 바이트코드로 변환한다.

# 설정

사용하는 IDE에 따라 어노테이션 프로세서를 등록하는 방법이 다르다. 이 포스트에서는 Android Studio를 기준으로 설명한다.

우선 다음과 같이 새로운 프로젝트를 생성하고 세 개의 모듈을 생성한다.

각 모듈에 의존성을 추가한다.

// app/build.gradle
implementation project(':annotation')
annotationProcessor project(':annotation_processor')
// annotation_processor/build.gradle
dependencies {
    implementation project(':annotation')
}

# 어노테이션 만들기

설정이 끝났다면 annotation모듈에 MyAnnotation.java파일을 생성한다.

package com.yologger.annotation;

public @interface MyAnnotation {
    // ...
}

# 어노테이션 프로세서 만들기

annotation_processor모듈에 다음과 같이 MyAnnotation.java파일을 생성하자.

어노테이션을 프로세서를 구현할 땐 AbstractProcessor를 상속해야한다. 또한 process()메소드를 구현해야한다. 이 메소드 안에서 실제로 어노테이션 처리를 하게된다.

public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

어노테이션을 사용하면 "Annotation Processing!!!"를 출력하도록 코드를 작성해보자.

public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        // 프로세싱에 필요한 기본적인 정보들을 processingEnvironment 부터 가져올 수 있다.
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // 프로세싱이 되는지 확인하기 위한 로그입
        System.out.println("Annotation Processing!!!");
        return false;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        // 지원되는 소스 버전을 리턴합니다.
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        // 어떤 애노테이션을 처리할 지를 Set에 추가한다.
        return new HashSet<String>() {
            {
                add(MyAnnotation.class.getCanonicalName());
            }
        };
    }
}
  • init(): 파일을 생성하기 위해 필요한 FileWriter, 디버깅에 필요한 Messager 등 각종 유틸리티 클래스들을 이 곳에서 사용할 수 있다.
  • process(): 프로세서의 핵심 부분이다. 이곳에서 클래스, 메소드, 필드 등에 추가한 어노테이션을 처리하고 처리에 대한 결과로 자바 파일을 생성할 수 있다.
  • getSupportedAnnotationType(): 어떤 어노테이션들을 처리할 지 집합(Set) 형태로 리턴하게 된다.
  • getSupportedSourceVersion(): 일반적으로 최신의 Java 버전을 리턴한다.

# 어노테이션 프로세서 등록하기

어노테이션 프로세서를 구현했다면 자바 컴파일러가 사용할 수 있도록 등록해야한다.

annotation_processor모듈에 다음과 같이 폴더를 생성한다.

annotation_processor/src/main/resources/META-INF/services

그 다음 파일을 하나 생성한다. 파일명은 반드시 다음과 같아야한다.

javax.annotation.processing.Processor

이제 파일을 열어 위에서 작성한 어노테이션 프로세서를 등록하자. 이때 반드시 프로젝트의 패키지명을 포함해서 작성해야한다.

com.yologger.annotation_processor.MyAnnotationProcessor

이제 app모듈을 빌드해보자. 어노테이션 프로세서가 정상적으로 작동되었다면, 다음과 같은 로그를 확인할 수 있다.

# auto-service 라이브러리

구글의 auto-service 라이브러리를 사용하면 위의 번거로운 등록 과정없이 자동으로 어노테이션 프로세서를 컴파일러에 등록해준다.

annotation_processeor모듈에 다음과 같이 auto-service 라이브러리 의존성을 추가한다.

// annotation_processor/build.gradle

dependencies {
    implementation 'com.google.auto.service:auto-service:1.0-rc5'
}

그 다음 어노테이션 프로세서 클래스에 다음과 같이 @AutoService(Processor.class)어노테이션을 추가하자.

@AutoService(Processor.class)
public class CharlesProcessor extends AbstractProcessor {
    ...
}

이제 app모듈을 다시 빌드하면 어노테이션 프로세서가 자동으로 등록되고 동작한다.

# 내장 어노테이션

Java API는 사전에 정의된 내장 어노테이션을 제공한다. 내장 어노테이션은 java.lang 또는 java.annotation 패키지에 포함된다. 자주 사용되는 내장 어노테이션은 다음과 같다.

# @Override

@Override는 부모 클래스의 메소드를 재정의했다는 것을 알려준다.

class Person {
    void work() {
        System.out.println("Work");
    }
}
class Musician extends Person {
    @Override
    void work() {
        System.out.println("Sing");
    }
}

# @Deprecated

@deprecated는 앞으로 사용하지 않을 코드임을 표시한다.

class Calculator {
    int plus(int a, int b) {
        return a+b;
    }

    @Deprecated
    int minus(int a, int b) {
        return a-b;
    }
}

public class App {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        calculator.minus();
    }
}

이 어노테이션이 붙은 코드를 사용하면 경고를 보여준다.

Warning: java: minux() has been deprecated

# @SuppressWarnings

경고를 무시하도록 컴파일러에게 지시한다.

# @FunctionalInterface

Java 8부터 추가되었으며, 함수형 인터페이스를 정의하는데 사용된다.

@FunctionalInterface
interface Lambda {
    void execute(String name);
}
Lambda printName = (String name) -> {
    System.out.println(name);
};

printName.execute("Paul");

# @Documented

javadoc 같은 문서화 도구로 코드를 문서화할 때 해당 어노테이션을 문서에 표시한다.

# @Target

어노테이션의 적용 범위를 지정할 수 있다. 예제를 살펴보자.

@Target(ElementType.FIELD)
public @interface First {}

커스텀 어노테이션 First를 정의하고 있다. 이 어노테이션은 필드에만 적용할 수 있다.

또 다른 예제를 살펴보자.

@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Second {}

이 어노테이션은 필드와 메소드에만 적용할 수 있게 된다.

인자로는 다음과 같은 enum ElementType이 올 수 있다.

이름 설명
TYPE class, interface, annotation, enum에 적용 가능
FIELD class, interface의 필드에만 적용 가능
METHOD 메소드에만 적용 가능
PARAMETER 함수의 파라미터에만 적용 가능
CONSTRUCTOR 생성자에만 적용 가능
LOCAL_VARIABLE 지역변수에만 적용 가능
ANNOTATION_TYPE 어노테이션에만 적용 가능
PACKAGE 패키지에만 적용 가능
TYPE_PARAMETER 어노테이션의 타입 파라미터에만 적용 가능

# @Retention

어노테이션의 유지 범위를 지정할 수 있으며, 세 가지의 enum RetentionPolicy 값이 올 수 있다.

이름 설명
SOURCE 어노테이션이 소스코드에서만 유지된다. 컴파일 시점에 어노테이션이 사라진다.
CLASS 어노테이션이 .class 파일에서도 유지된다. 런타임에서 실행될 때는 어노테이션이 사라진다.
RUNTIME 어노테이션이 JVM에서 실행될 때에도 유지된다.