영권's
12주차 과제: 애노테이션 본문
목표
자바의 애노테이션에 대해 학습하세요.
학습할 것 (필수)
- 애노테이션 정의하는 방법
- @retention
- @target
- @documented
- 애노테이션 프로세서
어노테이션(Annotation)
사전적 의미로 주석을 의미하며 프로그램에 대한 데이터를 제공하는 메타데이터의 한 형태 입니다.
어노테이션과 일반적인 주석은 뭐가 다른걸까?
- 어노테이션의 역할도 주석과 크게 다르지 않는다.
- 일반주석과 큰 차이점은 코드를 작성할 수 있다는 것이 다르다.
- 코드를 작성할 수 있다는 뜻은 어노테이션으로 뭔가를 할 수 있다는 뜻이 된다.
- 어노테이션도 enum과 마찬가지로 1.5에 등장했다고 한다.
- 메타 데이터(metadata) : 데이터에 대한 데이터. 즉, 다른 데이터를 설명해주는 데이터입니다.
- 애노테이션의 용도
- 컴파일러에 제공하는 정보 -> 컴파일러는 애노테이션을 사용하여 에러를 체크하거나 에러메시지를 억제할 수 있습니다. ex) @Override, @SuppressWarnings
- 컴파일 시간 및 배포 시간 처리 -> 소프트웨어 개발툴이 애노테이션 정보를 처리하여 코드, XML 파일등을 생성할 수 있습니다. (애노테이션을 사용한 곳에 특정 코드를 추가할 수 있습니다. ex) Lombok의 @Getter, @Setter)
- 런타임 처리 -> 일부 애너테이션은 런타임에 특정 기능을 실행하도록 정보를 제공합니다. (Java Reflection)
- 자바 리플렉션(Java Reflection) : 구체적인 클래스 타입을 알지 못해도 그 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API 입니다. 런타임 시에 클래스 이름만 알고있다면 클래스에 대한 정보를 가져오고 활용할 수 있게 해줍니다.
애노테이션 정의하는 방법
public @interface SocialUser {
}
'@interface'는 애노테이션 타입(annotation type)을 선언하는 키워드 입니다. 애노테이션 타입 선언을 일반적인 인터페이스(interface) 선언과 구분하기 위해 interface 앞에 기호 @를 붙입니다.
이렇게 정의하면 된다.
enum은 java.lang.Enum에 상속되어있다고 학습하였다.
그러면 어노테이션의 조상님은 과연 뭘까? 바이트 코드를 통해 확인해보자.
java.lang.annotation.Annotation을 implements하고 있다.
어노테이션은 인터페이스로 구성되었기 때문에 굳이 구현하려면 implements하던가 아니면 익명클래스로 만들어야 한다.
Annotation ANNOTATION = new Annotation() {
@Override
public Class<? extends Annotation> annotationType() {
return null;
}
};
어노테션은 리플렉션 기술을 이용해서 사용된다는 것을 짐작할 수 있다.
애노테이션 필드
애노테이션 타입 선언(@interface)은 문맥 자유 구문(Context-free grammer,CFG)으로부터 다음과 같은 제한을 가집니다.
애노테이션에 필드같은 요소들을 정의할 수 있습니다.
- 요소의 타입은 기본형, String, enum, 애노테이션, Class만 가질 수 있습니다.
- ()안에 매개변수를 선언할 수 없습니다.
- 예외를 선언할 수 없습니다.
- 요소를 타입 매개변수로 정의할 수 없습니다.
- 배열을 선언할 수 있습니다. ex) String[] arr();
- default값을 지정할 수 있습니다. ex) int number() default 500;
자바 8에서 추가된 애노테이션 사용처
위의 문서를 보면 자바 8에서는 다음과 같은곳에도 애노테이션을 사용할 수 있습니다.
- 클래스 인스턴스를 생성할 때
- new @Interned MyObject();
- 타입 캐스트
- myString = (@NonNull String) str;
- 인터페이스 구현(implements)
- class UnmodifiableList<T> implements @Readonly List<@Readonly T> { . . . }
- 예외를 throws 할 때
- void monitorTemperature() throws @Critical TemperatureException { . . . }
- 애너테이션 정의
요소는 위와 같이 선언하고 어노테이션을 적용하고 각각의 요소에 값을 주고 사용할 수 있습니다.
자바 리플렉션을 사용해서 MyClass의 testMethod()에 적용된 MyAnnotation의 요소값들을 가져 올 수 있습니다.
1. MyClass.class.getMethods()를 이용해서 MyClass에 정의된 모든 메서드들을 Method[] 배열에 넣습니다.
2. 각각의 메서드에 isAnnotationPresent(MyAnnotation.class)를 사용해서 MyAnnotation이 붙어 있는 메서드 인지 확인합니다.
3. MyAnnotation이 붙어있는 메서드라면 getDeclaredAnnotation(MyAnnotation.class)를 사용해서 어노테이션을 변수에 초기화 시킵니다.
4. myAnnotation변수를 사용해서 각각의 요소를 출력합니다.
자바 리플렉션 (Java Reflection)
모든 클래스 파일은 클래스로더(Classloader)에 의해 메모리에 올라갈 때, 클래스에 대한 정보가 담긴 객체를 생성하는데 이 객체를 클래스 객체라고 합니다. 이 객체를 참조할 때는 '클래스이름.class'의 형식을 사용합니다.
클래스이름.class 를 이용해서 클래스의 필드, 생성자, 메서드에 대한 정보를 얻을 수 있습니다.
메서드명 | 리턴타입 | 설명 |
getFields() | Field[] | 접근 제어자가 public인 필드들을 Field 배열로 반환 부모 클래스의 필드들로 함께 반환합니다. |
getConstructors() | Constructor[] | 접근 제어자가 public인 생성자들을 Constructor 배열로 반환, 부모 클래스의 생성자들도 함께 반환합니다. |
getMethods() | Method[] | 접근 제어자가 public 인 메서드들을 Method 배열로 반환, 부모 클래스의 메서드들도 함께 반환합니다. |
getDeclaredFields() | Field[] | 접근 제어자에 상관없이 모든 필드들을 Field 배열로 반환, 부모 클래스의 메서드들은 반환하지 않습니다. |
getDeclaredConstructors() | Constructor[] | 접근 제어자에 상관없이 모든 생성자들을 Constructor배열로 반환, 부모 클래스의 생성자들은 반환하지 않습니다. |
getDeclaredMethod() | Method[] | 접근 제어자에 상관없이 모든 메서드들을 Method배열로 반환, 부모 클래스의 메서드들은 반환하지 않습니다. |
애노테이션 정보를 얻기위한 메서드들
리턴타입 | 메소드명 / 설명 |
boolean | isAnnotationPresent(Class annotationClass) |
지정한 애노테이션이 적용되었는지 여부를 확인.Class에서 호출했을 경우 상위 클래스에 적용된 경우에도 true를 리턴. | |
Annotation | getAnnotation(Class annotationClass) |
지정한 애노테이션이 적용되어 있으면 애노테이션을 반환하고 그렇지 않은 경우 null을 반환합니다. Class에서 호출한 경우 상위 클래스에 적용된 애노테이션도 반환합니다. | |
Annotation[] | getAnnotations() |
적용된 모든 애노테이션을 반환합니다. Class에 사용됐을 경우 상위 클래스에 적용된 애노테이션까지 전부 포함해서 반환합니다. 애노테이션이 없을 경우 길이가 0인 배열을 반환합니다. | |
Annotation[] | getDeclaredAnnotation() |
직접 적용된 모든 애노테이션을 리턴합니다. Class에서 호출했을 경우 상위 클래스에 적용된 애노테이션은 포함되지 않습니다.(상위 클래스의 @Inherited가 붙은 애노테이션을 무시합니다.) |
메타 애너테이션
메타 데이터가 데이터에 대한 데이터, 즉 데이터를 위한 데이터인것처럼
메타 애노테이션은 '애노테이션을 위한 애노테이션'. 즉, 애노테이션을 정의하는데 사용하는 애노테이션 입니다.
- 애노테이션의 적용대상(target), 유지기간(retention)등을 지정하는데 사용됩니다.
- 메타 애노테이션은 java.lang.annotation 패키지에 포함되어 있습니다.
애너테이션 | 설명 |
@Target | 애노테이션이 적용가능한 대상을 지정하는데 사용합니다. |
@Documented | 애노테이션 정보가 javadoc으로 작성된 문서에 포함되게 합니다. |
@Inherited | 애노테이션이 자손 클래스에 상속되도록 합니다. |
@Retention | 애노테이션이 유지되는 범위를 지정하는데 사용합니다. |
@Repeatable | 애노테이션을 반복해서 적용할 수 있게 합니다. |
@Target
애노테이션이 적용가능한 대상을 지정하는데 사용합니다. 적용가능한 대상을 여러개 지정하고 싶다면 {} 배열을 사용하면 됩니다.
아래는 Target 애노테이션의 실제 코드 입니다. ElementType의 Enum 요소를 사용해서 애노테이션이 적용 가능한 대상을 결정합니다.
요소 타입이 ElementType[] 배열이기 때문에 적용가능한 대상을 여러 개의 값을 지정할 수 있습니다.
@Target으로 지정할 수 있는 애노테이션 적용대상의 종류
ElementType | 대상 타입 |
ANNOTATION_TYPE | 애노테이션 |
CONSTRUCTOR | 생성자 |
FIELD | 필드(멤버변수, enum상수) |
LOCAL_VARIABLE | 지역변수 |
METHOD | 메서드 |
PACKAGE | 패키지 |
PARAMETER | 매개변수 |
TYPE | 타입(클래스, 인터페이스, enum) |
TYPE_PARAMETER | 타입 매개변수 |
TYPE_USE | 타입이 사용되는 모든 곳(타입의 변수를 선언할 때) |
@Retention
애노테이션이 유지(retention)되는 기간을 지정하는데 사용됩니다. 즉, 어느 시점까지 애노테이션의 메모리를 가져갈지 설정합니다.
애노테이션의 유지 정책(retention policy)의 종류
유지 정책 | 의미 |
SOURCE | 소스 파일에만 존재. 클래스 파일에는 존재하지 않음 |
CLASS | 클래스 파일에 존재. 실행시에 사용 불가. Default 값 |
RUNTIME | 클래스 파일에 존재. 실행시에 사용가능. |
RetentionPolicy의 값을 넘겨주는 것으로 애노테이션의 유지 정책이 결정된다고 합니다.
RetentionPolicy는 enum(열거형)입니다.
SOURCE는 애노테이션을 사실상 주석처럼 사용한다고 보면 됩니다 컴파일러가 컴파일 할 때 해당 애노테이션의 메모리는 버립니다.'@Override'나 '@SuppressWarnings' 처럼 컴파일러가 사용하는 애노테이션은 유지 정책이 'SOURCE' 입니다. 컴파일러를 직접 만드는게 아니라면 주석처럼 사용됩니다.
CLASS(디폴트 값)는 컴파일러가 컴파일 시에는 애노테이션의 메모리를 가져가지만, 실질적으로 런타임시에는 사라지게 됩니다. 런타임시에 사라진다는 것은 리플렉션으로 선언된 애노테이션 데이터를 가져올 수 없게 됩니다.
즉, 실행 타임에서 애노테이션에 대한 데이터를 활용할 수 없다는것을 의미합니다. 예를들어 해당 클래스에 있는 애노테이션의 정보를 가져오고 싶을 때 유지정책이 SOURCE나 CLASS 라면 가져오는것이 불가능합니다.
(런타임시에 이미 애노테이션에 대한 메모리가 없기 때문에)
RUNTIME은 애노테이션을 런타임시에까지 사용할 수 있습니다. JVM이 자바 바이트코드가 담긴 class 파일에서 런타임 환경을 구성하고 런타임을 종료할 때까지 메모리는 살아있습니다.
RUNTIME, CLASS, SOURCE 3가지를 각각 사용하며 .class 파일을 열어보겠습니다.
Retention이 RetentionPolicy.RUNTIME으로 된 상태로 testMethod()에 사용하였습니다.
RUNTIME 유지정책이기 때문에 .class 파일에도 @MyAnnotation가 있습니다.
CLASS 유지정책 또한 @MyAnnotation이 남아있는것을 확인할 수 있습니다.
RUNTIME과 CLASS의 차이점을 보기위해 바이트코드를 열어보겠습니다.
testMethod() 밑 줄에 @LAnnotation/MyAnnotation;() 를 비교해 보면 CLASS 인 경우 invisible(보이지 않는)이 붙은 것을 알 수 있다.
이말은 즉, CLASS 유지정책은 바이트 코드에서만 확인할 수 있을 정도로만 정의해놓을 뿐 Runtime 환경에서는 역시나 버려지는 메모리라는것입니다.
마지막으로 SOURCE 입니다.
@MyAnnotation이 보이지 않습니다.
개발자가 프로그래밍을 할 때 사용하다가 버려지는 소스이기 때문에 그렇습니다. 대표적인 예로 @Override가 있습니다.
@Documented
애노테이션 정보가 javadoc으로 작성된 문서에 포함되도록 하는 메타 애노테이션 입니다.
자바에서 제공하는 기본 애노테이션 중에 '@Override'와 '@SuppressWarnings'를 제외하고는 모두 @Ducumented 메타 애너테이션이 붙어 있습니다.
보시면 @Override 와 @SuppressWarnings 에는 @Ducumented가 붙어있지 않습니다.
아래의 @Target, @Native, @Retention, 심지어 @Ducumented 자신까지 @Ducumented가 붙어있습니다.
intellij에서 javadoc 만드는 방법
: https://shinheechul.tistory.com/43
Generate javadoc을 통해 MyAnnotation을javadoc을 만들면
와 같이 만들어집니다.
@Inherited
애노테이션이 자손 클래스에 상속되도록 합니다. '@Inherited'가 붙은 애노테이션을 조상클래스에 붙이면, 자손 클래스도 이 애노테이션이 붙은 것과 같이 인식됩니다.
아래의 코드는 다음과 같습니다.
- @Inherited 애노테이션을 사용한 SomeAnnotation 애노테이션
- @SomeAnnotation을 사용한 Parent 클래스
- Parent클래스를 상속받은 Child 클래스
- Child 클래스의 인스턴스 getAnnotation() 메서드를 사용해서 SomeAnnotation이 있으면 반환받아서 출력합니다.
실행결과로 SomeAnnotation이 출력된것을 확인할 수 있습니다. 따라서 Child 클래스에도 @SomeAnnotation이 적용된것을 확인했습니다.
@Repeatable
보통은 하나의 대상에 한 종류의 애노테이션을 붙이지만 '@Repeatable'이 붙은 애노테이션은 여러 번 붙일 수 있습니다.
(하나의 애노테이션의 배열 요소에 여러개의 요소값을 넣어주는것과 다릅니다.)
일반적인 애노테이션과 달리 같은 이름의 애노테이션이 여러 개가 하나의 대상에 적용될 수 있기 때문에, 이 애노테이션들을 하나로 묶어서 다룰 수 있는 애노테이션도 추가로 정의해야 합니다.
-> 마치 애노테이션들을 담을 배열을 만든다고 생각하면 이해가 쉽습니다.
위와 같이 ToDo애노테이션을 담을 컨테이너 애노테이션 ToDos를 만들어야 합니다. 또한 ToDo 애노테이션 배열타입의 요소를 선언해줘야 하고 컨테이너 애노테이션안에 요소의 이름이 반드시 value 이어야 합니다.
표준 애노테이션
자바에서 기본적으로 제공하는 애노테이션들을 자바 표준 애노테이션이라고 합니다. 이안에는 메타 애노테이션도 포함됩니다.
애노테이션 | 설명 |
@Override | 컴파일러에게 오버라이딩하는 메서드라는 것을 알립니다. |
@Deprecated | 앞으로 사용하지 않을 것을 권장하는 대상에 붙입니다. |
@SuppressWarnings | 컴파일러의 특정 경고메시지가 나타나지 않게 해줍니다. |
@SafeVarargs | 지네릭스 타입의 가변인자에 사용합니다. |
@FunctionalInterface | 함수형 인터페이스라는 것을 알립니다. |
@Native | native메서드에서 참조되는 상수 앞에 붙입니다. |
@Target | 애노테이션이 적용가능한 대상을 지정하는데 사용합니다. |
@Documented | 애노테이션 정보가 javadoc으로 작성된 문서에 포함되게 합니다. |
@Inherited | 애노테이션이 자손 클래스에 상속되도록 합니다. |
@Retention | 애노테이션이 유지되는 범위를 지정하는데 사용합니다. |
@Repeatable | 애노테이션이 반복해서 적용할 수 있게 합니다. |
@Override
@Target이 METHOD로 메서드에만 붙일 수 있는 애노테이션으로, 조상의 메서드를 오버라이딩하는 것이라는걸 컴파일러에게 알려주는 역할을 합니다.
만약 @Override 애노테이션 없이 오버라이딩 하려고 할 때 조상메서드의 이름을 틀리게 입력하면 컴파일 에러 없이 넘어가게 됩니다. 그러면 컴파일러는 그냥 메서드가 추가된걸로 인식하지만 개발자 입장에서는 오류가 발생할 수 있는데 어디서 발생한 오류인지 쉽게 알아내기 어렵습니다.
이런 상황에서 @Override 애노테이션을 사용하면, 컴파일러가 같은 이름의 메서드가 조상에 있는지 확인하고 없으면, 에러메시지를 출력해줍니다.
어느 부분에서 발생했는지 알려준다.
메서드를 오버라이딩할 때 @Override를 붙이는 것은 필수가 아니지만, 알아내기 어려운 실수를 미연에 방지해주므로 붙이도록 하는것이 안전한 코드를 만드는 방법입니다.
@Deprecated
새로운 버전의 JDK가 소개될 때, 새로운 기능이 추가도리 뿐만 아니라 기존의 부족했던 기능들을 개선하기도 합니다.
이 과정에서 기존의 기능을 대체할 것들이 추가되어도, 이미 여러곳에서 사용할지도 모르기때문에 함부로 삭제했다간 일부 기능에 마비가 올 수 있습니다. 그래서 나온 애노테이션이 @Deprecated 입니다.
- 더이상 사용되지 않는 필드나 메서드에 사용합니다.
- 이 애노테이션이 붙은 대상은 다른 것으로 대체되었으니 더 이상 사용하지 않는것을 권장한다는 의미입니다.
- @Deprecated가 있는 대상은 추가로 개선된 기능이 있거나 심각한 오류를 발생시킬 수 있으니 사용하지 않아야 합니다.
만약 @Deorecated 가 있는 대상을 코드에 사용하여 컴파일 하면 다음과 같은 결과가 발생합니다.
여기서는 Thread 클래스의 stop() 메서드를 사용했습니다.
경고메시지의 내용은 해당 소스파일이 'deprecated' 된 대상을 사용하고 있으며, '-Xlint:deprecation'옵션을 붙여서 다시 컴파일하면 자세한 내용을 알 수 있다는 뜻입니다.
'-Xlint:deprecation' 옵션을 붙여서 컴파일하면 자세한 내용을 보여줍니다.
-Xlint:deprecation 옵션을 사용해서 경고 메시지의 자세한 내용을 확인할 수 있습니다.
@FunctionalInterface
'함수형 인터페이스(functional interface)' 를 선언할 때, '@FunctionalInterface'를 붙이면 컴파일러가 '함수형 인터페이스'를 올바르게 선언했는지 확인하고, 잘못된 경우, 에러를 발생시킵니다.
@Override 처럼 필수는 아니지만 붙이면 실수를 방지할 수 있으므로 '함수형 인터페이스'를 선언할 때는 이 애노테이션을 붙이는게 좋습니다.
대표적인 함수형 인터페이스로 Runnable 인터페이스가 있습니다.
다음은 @FuntionalInterface 애노테이션을 사용하고 메서드가 0개, 1개, 2개 일때의 결과입니다
만약 메서드 개수가 1개가 아니면 위와같은 에러메시지가 나옵니다.
@SuppressWarnings
컴파일러가 보여주는 경고 메시지를 나타나지 않게 억제해줍니다.
- 경고가 발생하면 해결하고 다시 컴파일해서 메시지를 나타나지 않게 하는것이 일반적이지만 특수한 경우에 경고가 발생할 것을 알면서도 묵인해야할 때가 있습니다.
- 이 경우 컴파일 때마다 경고 메시지가 나오기 때문에 '@SuppressWarnings'를 사용해서 경고 메시지가 나오지 않게 할 수 있습니다.
@SuppressWarnings의 토큰은 다음과 같이 있습니다.
토큰 | 설명 |
all | 모든 경고를 억제합니다. |
boxing | 오퍼레이션과 관련된 경고를 억제합니다. |
cast | 캐스트 오퍼레이션과 관련된 경고를 억제합니다. |
dep-ann | 권장되지 않는 애노테이션과 관련된 경고를 억제합니다. |
deprecation | 권장되지 않는 기능과 관련된 경고를 억제합니다. (@Deprecated) |
fallthrough | switch문에서 누락된 break 문과 관련된 경고를 억제합니다. |
finally | 리턴되지 않는 마지막 블록과 관련된 경고를 억제합니다. |
hiding | 변수를 숨기는 로컬과 관련된 경고를 억제합니다. |
incomplete-switch | switch 문에서 누락된 항목과 관련된 경고를 억제합니다.(enum case) |
javadoc | javadoc 경고와 관련된 경고를 억제합니다. |
null | 널(null) 분석과 관련된 경고를 억제합니다. |
rawtypes | 원시 유형 사용법과 관련된 경고를 억제합니다. |
restriction | 올바르지 않거나 금지된 참조 사용법과 관련된 경고를 억제합니다. |
resource | 닫기 가능 유형의 자원 사용에 관련된 경고 억제 |
serial | 직렬화 가능 클래스에 대한 누락된 serialVersionUID필드와 관련된 경고를 억제합니다. |
static-access | 잘못된 정적 액세스와 관련된 경고를 억제합니다. |
static-method | static으로 선언될 수 있는 메서드와 관련된 경고를 억제합니다. |
super | 슈퍼 호출을 사용하지 않는 메서드 오버라이딩과 관련된 경고를 억제합니다. |
synthetic-access | 내부 클래스로부터의 최적화되지 않은 액세스와 관련된 경고를 억제합니다. |
sync-override | 동기화된 메서드를 오버라이드하는 경우 누락된 동기화로 인한 경고 억제 |
unchecked | 미확인 오퍼레이션과 관련된 경고를 억제합니다. |
unqualified-field-access | 규정되지 않은 필드 액세스와 관련된 경고를 억제합니다. |
unused | 사용하지 않은 코드 및 불필요한 코드와 관련된 경고를 억제합니다. |
필요한 토큰은 경고 메시지를 보면 힌트가 있습니다.
경고메시지를 보면 -Xlint:deprecation 이라고 되있는 부분이 있습니다. 여기서 deprecation을 @SuppressWarnings의 토큰으로 사용하면 즉, @SuppressWarnings("deprecation") 으로 사용하면 해당 경고가 억제됩니다.
@SafeVarargs
메서드에 선언된 가변인자의 타입이 비 구체화 타입(non-reifiable type)일 경우, 해당 메서드를 선언하는 부분과 호출하는 부분에서 "unchecked" 경고가 발생합니다. 해당 코드에 문제가 없다면 이 경고를 억제하기 위해 @SafeVarargs를 사용해야 합니다.
- 비 구체화 타입(non-reifiable type) : 타입 소거자에 의해 컴파일 타임에 타입 정보가 사라지는 것(런타임에 구체화하지 않는것)
@SafeVarargs는 static이나 final이 붙은 메서드와 생성자에만 붙일 수 있습니다.
즉, 오버라이드될 수 있는 메서드에는 사용할 수 없다는 뜻입니다.
예를들어 java.util.Arrays의 asList()는 다음과 같이 정의되어 있으며, 이 메서드는 매개변수로 넘겨받은 값들로 배열을 만들어서 새로운 ArrayList 객체를 만들어서 반환하는데 이 과정에서 경고가 발생합니다.
- asList()의 매개변수가 가변인자인 동시에 제네릭 타입입니다.
- 메서드에 선언된 타입 T는 컴파일 과정에서 Object로 바뀝니다.
- 즉 Object[]가 됩니다.
- Object[]에는 모든 타입의 객체가 들어있을 수 있으므로
- 이 배열로 ArrayList<T>를 생성하는 것은 위험하다고 경고하는 것입니다.
- asList()가 호출되는 부분을 컴파일러가 체크해서 타입 T가 아닌 다른 타입이 들어가지 못하게 할 것이므로 위의 코드는 아무런 문제가 없습니다. 따라서 @SafeVarargs로 경고를 억제해줘야 합니다.
- @SafeVarargs를 사용하면 'unchecked' 경고도 억제할 수 있습니다. 또한 호출하는 곳에서 발생하는 경고도 억제됩니다. (SuppressWarnings("unchecked")는 선언 뿐만 아니라 호출되는 곳에도 애노테이션을 붙여야합니다.
마커 애노테이션(Marker Annotation)
값을 지정할 필요가 없는 경우, 애노테이션의 요소를 하나도 정의하지 않을 수 있습니다.
이런 애노테이션을 마커 애노테이션 이라고 합니다.
대표적인 마커 애노테이션으로 @Override가 있습니다.
애노테이션 프로세서
- 소스코드 레벨에서 소스코드에 붙어있는 애노테이션 정보를 읽어와
- 컴파일러가 컴파일 중에 새로운 소스코드를 생성하거나 기존의 코드 변경을 가능하게 합니다.
- (코드 변경은 비추)
- 클래스 즉, 바이트 코드도 생성가능
- 소스코드와 별개의 리소스도 생성가능
대표적인 예제
- 롬복(Lombok)
- @Getter, @Setter, @Builder
- AutoService : java.util.ServiceLoader용 파일 생성 유틸리티
- 리소스 파일을 만들어줍니다.
- META-INF 밑의 service 밑에 ServiceLoader용 레지스트리 파일을 만들어줍니다.
- @Override
- 컴파일러가 오버라이딩하는 메서드가 잘못된 대상임을 체크해주는것도 애노테이션 프로세서
- Dagger2 : 컴파일 타임 DI 제공 -> 런타임 비용이 없어집니다.
- 안드로이드 라이브러리
- ButterKnife : @BindView (뷰 아이디와 애노테이션 붙인 필드 바인딩)
- DeepLinkDispatcher : 특정 URI 링크를 Activity로 연결할 때 사용
애노테이션 프로세서의 장점
- 컴파일 시점에 조작하기 때문에 런타임 비용이 제로!
애노테이션 프로세서의 단점
- 기존의 코드를 고치는 방법 -> 현재로써는 public 한 api가 없습니다.
롬복(Lombok)의 동작원리
Lombok
- @Getter @Setter, @Builder 등의 애노테이션과
- 애노테이션 프로세서를 제공하여 표준적으로 작성해야 할 코드를 개발자 대신 생성해주는 라이브러리이다.
사용하기
- 의존성 추가
- IntelliJ Lombok 플로그인 설치
- Intellij Annotation Processing 옵션 활성화
동작원리
- 컴파일 시점에 "애노테이션 프로세서"를 사용하여 (자바가 제공하는 애노테이션 프로세서)
- 소스코드의 AST(Abstract Syntax Tree) 를 조작한다.
AST에 대한 참고 사이트
http://javaparser.org/inspecting-an-ast/
javax.annotation.processing || Interfaec Processor
⇒ 소스코드의 AST를 원래는 참조만 할 수 있다. // 수정하지 못한다. 그리고 하면 안된다!
⇒ 그러나 수정이 됬음을 알 수 있다.(컴파일 이후 바이트코드 확인)
⇒ 참조만 해야 하는 것을 내부 클래스를 사용하여 기존 코드를 조작하는 것임으로 "해킹" 이라고 얘기하기도 한다.
논란 거리
- 공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스 코드를 조작한다.
- 특히 이클립스의 경우에는 Java Agent를 사용하여 컴파일러 클래스까지 조작하여 사용한다.
- 해당 클래스들 역시 공개된 API가 아니다보니 버전 호환성에 문제가 생길 수도 있고 언제라도 그런 문제가 발생해도 이상하지 않다.
- 그럼에도 불구하고 엄청난 편리함 때문에 널리 쓰이고 있으며, 대안이 몇가지 있지만 롬복의 모든 기능과 편의성을 대체하지 못하는 상황이다.
- AutoValue
- Immutables
기존 Getter, Setter, equals, hasCode 등의 메소드를 생성하는 순간?
해당 클래스는 이미 방대해진 모습을 볼 수 있다.
해당 클래스를 위한 메소드들이 선언이 되어 있더라도 위 메소드들 사이에 파묻혀 있다면?
개발자 입장에서 놓칠 수도 있다. (그래서 boilerplat 코드라는 개념도 나온다.)
⇒ 롬복을 이용하여 쉽게, 그리고 가독성 높게 클래스를 구현할 수 있다.
package me.ssonsh.annotationprocessor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Member {
private String name;
private int age;
}
→ 컴파일 된 Member.class 파일
.java 에서 선언하지 않았던 getter, setter가 생성되어 있음을 확인할 수 있다.
롬복의 @Getter, @Setter 가 만들어낸 결과라 볼 수 있다.
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package me.ssonsh.annotationprocessor;
public class Member {
private String name;
private int age;
public Member() {
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
강의중 강조한 Point. 자바의 ServiceLoader
→ 스프링부트에서도 활용하고 있는 기능임으로 기본적 개념을 알고가는 것이 중요하다.
→ 스프링부트 이전에 자바 공부를 해야하는 이유.
→ 스프링부트의 autoconfigure
- autoconfigure → META-INF → spring.factories
나는 인터페이스만 제공하지만.
public interface HelloService{
String hello();
}
이 인터페이스의 구현체는 누군지 모른다.
이 구현체를 내가 지정하지 않고, Jar 파일만 바꿔끼면 동작하도록 만들 수 있다.
→ 이것이 Service Loader 이다.
https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html
Project : hello-service
//me.ssonsh.hello-service.HelloService
public interface HelloService{
String hello();
}
→ maven install → jar 파일 생성
Project : my-hello
- 위 hello-service의 의존성 주입
//me.ssonsh.my-hello.MyHello
public class MyHello implements HelloService{
@Override
public String hello(){
return "홧팅!";
}
}
- 패키징 전 서비스 로더 매커니즘을 사용하기 위해서
- resources 하위에 META-INF/services 디렉토리 생성
- **파일 생성 → 인터페이스의 풀패키지 경로로 파일을 생성한다.**
- 내용에는 구현체의 풀패키지 경로를 작성한다.
- **파일 생성 → 인터페이스의 풀패키지 경로로 파일을 생성한다.**
- 파일명 : resources/META-INF/services/me.ssonsh.hello-service.HelloService
- 파일 내용 : me.ssonsh.my-hello.MyHello
→ maven install → jar 파일 생성
Project : app
- 위 my-hello 의존성 주입
- my-hello는 hello-service 의존성을 주입하고 있음.
public static void main(String[] args){
ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
for(HelloService helloService : loader){
System.out.println(helloService.hello());
}
}
- HelloService 인터페이스의 구현체가 뭐가 있는지 모르는 상황에서 위와 같이 사용할 수 있다.
- loader는 얼마나 있을지도 모름으로 위와 같이 ServiceLoader<T> loader 형태로 반환된다.
- loop를 돌면서 위 예제에선 HelloService 인터페이스 구현체가 모두 처리된다.
애노테이션 프로세서를 이용해 Lombok Library를 직접 만들어 보자.
참고. 스터디 강의 중 maru 님 블로그. https://catch-me-java.tistory.com/49
출처
- 자바의정석 3판
https://b-programmer.tistory.com/264
https://parkadd.tistory.com/54
https://sas-study.tistory.com/329
https://www.notion.so/37d183f38389426d9700453f00253532
https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html
'스터디 > 백기선 라이브 스터디(자바)' 카테고리의 다른 글
14주차 과제: 제네릭 (0) | 2021.06.01 |
---|---|
13주차 과제: I/O (0) | 2021.05.31 |
11주차 과제: Enum (0) | 2021.03.05 |
10주차 과제: 멀티쓰레드 프로그래밍 (0) | 2021.02.25 |
9주차 과제: 예외 처리 (0) | 2021.02.23 |