본문 바로가기
Java/Java Basic

[Java Basic] 41 - Java Annotation

by Rosmary 2022. 9. 15.
728x90
반응형

 

 

 

 

오늘 포스팅을 진행할 내용인 Annotation의 단어 정의는 아래와 같다.

 

 

 

사전적 의미의 Annotation은 주 문장에 작성하는 추가 노트 또는 설명인데, 한글로 가장 적당한 단어로 각주()라고 한다. Java에서는 주석을 통해 코드에 대한 부연 설명을 작성하여 다른 사람이 코드를 쉽게 이해할 수 있도록 만들고 있다. Java의 Documentation에 작성된 내용은 Java 컴파일러가 클래스의 소스 파일 내의 주석(/** ... */)과 Annotaion을 참조하여 Html 파일로 만든 것이다. 

 

하지만 중복되는 내용에 대해 일일이 주석을 달아넣는것 또한 번거롭기 때문에 공통적이면서 주요하게 알려야하는 내용은 @기호와 약속된 단어를 붙여 표시하며 이를 Annotation이라고 한다. 예를 들어, Object에 정의된 toString()은 오버라이드 매서드로 다른 클래스에서 사용이 가능한데, 해당 매서드가 오버라이드라는 매서드임을 표시하기 위해 매서드 선언 코드 위에 @Override라는 단어를 추가한다.

 

Java에서 정의된 Annotation은 표준 Annotation과 메타 Annotation으로 나뉘는데, 표준 Annotation은 Java에서 정의한 Annotation이며, 메타 Annotation은 사용자가 정의하려는 Annotation을 만들기 위한 일종의 도구라 보면 된다.

 

 

 

 

1. 표준 Annotation

 

표준 Annotation은 Java에서 기본적으로 제공하는 완전체 Annotation이다. 앞서 보았던 Override와 Deprecated 외에도, SuppressWarning, FunctionalInterface, SafeVarargs, Native 가 존재한다. 이 중 주요하게 사용되는 Annotation에 대해서만 소개를 진행하려 한다.

 

 

(1) @Override

 

Java의 대부분 클래스는 서로 상속관계를 이루며 존재하고 있다. 부모 클래스에 정의된 매서드를 자식 클래스에서 재정의하여 사용하는 오버라이딩(Overriding)도 매우 빈번한데, 직접 제작한 클래스를 상속받아 오버라이딩을 진행한 매서드의 경우, 다른 사람이 코드 확인 시 매서드 오버라이딩 여부를 확인하려면 시간이 소요될 수 밖에 없다.

 

 

 

 

또한 Child 클래스 파일을 수정하다가 오버라이딩 된 매서드의 이름을 "실수(코딩하다보면 모르는 사이에 다른 키를 누르는 경우가 종종 일어난다.. 필자만 그런가...)"로 변경하게 되는 경우, 이를 알아채기가 매우 어렵다. 

 

본격 숨은 그림 찾기다. 자식 클래스 매서드에서 이름이 변경되었는데 수많은 코드에서 바로 인식할 수 있을까?

 

 

따라서 오버라이딩 된 매서드의 경우, Annotation으로 각주를 달아주는 작업을 진행하는 것이 좋다. 잘못된 매서드에 @Override Annotation을 적용하면 다음과 같이 에러 문구가 발생한다.

 

 

 

에러 문구는 "이 매서드 오버라이드한다고 각주 달았는데, 부모 클래스에 이런 매서드 없으니 확인해라" 라는 내용이다. 다시 매서드 명을 정상값으로 바꿔보자.

 

 

 

 

(2) @Deprecated

 

프로그램을 제작하여 수정사항을 거치다보면, 기존 매서드를 개선하여 더 좋은 매서드를 만들어내는 경우가 있다. 그러나 기존 매서드가 다른 코드로 인해 삭제가 불가능한 상황이라면, 기존 코드는 퇴화(Deprecated)되었으니, 다른 프로그래머에게도 추후 기존 코드를 사용하지 못하도록 알려야 할 필요성이 있다. 지금까지 봤던 클래스 중 Date 클래스가 여기에 속한다.

 

 

Deprecated 된 클래스나 매서드는 Eclipse에서 호출하는 즉시 생성자와 매서드 이름에 줄이 쫙 그어진다. 즉, 더 이상 사용하지 않는 코드이니 확인하라는 의미다. 물론 이와 관련된 경고도 나타난다.

 

 

필자는 Child 클래스 내에 get 매서드를 만들어 특정 위치에 저장된 값을 출력해보려 한다. 단, 이 매서드는 Deprecated를 적용하려 한다.

 

 

 

Child의 get() 클래스 호출 코드를 보면 Date와 마찬가지로 Deprecated가 된 매서드를 사용하였다는 의미로 매서드 명 위에 줄이 쳐져있는 것이 확인된다. 실제 이 코드를 Eclipse가 아닌 Window CMD에서 컴파일을 실행하면 다음과 같은 경고 문구가 나타난다.

 

 

 

cmd에서 컴파일 시, -Xlint:deprecation 을 옵션으로 지정하면, deprecation과 관련된 내용만 화면에 상세하게 출력된다.

 

 

 

 

(3) @SuppressWarnings(type) 

 

현재 필자가 작성한 코드에는 이런 저런 경고가 무진장 많이 뜨고 있다. 경고를 피하는 코드를 작성할 수도 있지만, 일일이 모든 경고에 대응하기는 불가능하다. 함부로 코드를 변경했다가 어떤 예상치 못한 오류가 발생할지는 모르기 때문이다. 그렇기 때문에 협업 과정이라면, 필자는 필자가 오류를 감안하고 이 코드를 작성했음을 다른 사람에게도 경고를 감안하고 코드를 작성했으니 무심결에 변경하는 일이 없도록 표시를 할 필요가 있다. 이 때 사용하는 Annotation이 SuppressWarning이다. 

 

SuppressWarning은 매개인자를 받는데, 이 매개인자는 CMD 창에서 -Xlint 옵션으로 나타난 결과의 대괄호[] 내의 값을 사용하면 된다.

 

 

 

 

여러 경고 중, rawtypes(원시타입)와 관련된 경고를 없애보자. rawtypes는 Generics로 정의된 클래스가 Generics 명시 없이 호출된 경우 나타난다. 따라서 Parent의 ArrayList 선언 및 초기화 부분 위에 @SuppressWarning을 적용한다.

 

기존에 존재하던 경고 아이콘이 사라진 것을 확인해보자.

 

다음으로 get() 매서드의 Iterator 선언 부분의 경고도 억제해보자. 이 경고도 역시 "rawtypes"로 지정되어 있다.

 

 

 

Iterator 부분의 rawtypes 경고가 모두 사라진 것이 확인된다. 그런데 일일이 모든 경고에 대응하여 Annotation을 달아주려니 영 귀찮다. 매서드가 아니라 클래스 단위에서 이 경고들을 무시하도록 Annotation을 적용하고 싶다. 이럴 때는 단순히 클래스 선언부 앞에 Annotation을 적용해주면 된다.

 

 

 

 

만약 둘 이상의 경고에 대해 SuppressWarning를 적용해야하는 경우, 매개 인자를 배열 형태로 대입해주며, 배열의 요소로 각 경고의 타입을 작성해주면 된다.

 

 

 

 

이번에는 Deprecated된 매서드 경고를 무시하는 Annotation을 적용해보자. deprecation 경고는 Main 함수에서 Child 클래스의 get 매서드를 호출하는 과정에서 발생하는데, 이 코드 위에 @SuppressWarnings("deprecation")을 적용하면 경고가 사라지는 것이 아니라 에러가 발생한다.

 

 

 

 

이유는 매우 단순한데, @SuppressWarnings Annotation은 메타 Annotation에 의해 사용될 수 있는 위치가 별도로 지정되어 있기 때문이다. 지금처럼 매서드를 호출하는 코드 위에는 @SuppressWarnings를 작성할 수 없도록 되어 있기 때문에, 컴파일러는 Annotation을 일반 코드로 인식하는 것이다(자세한 내용은 메타 Annotation에서 설명한다). 만약 Deprecation 경고를 출력하고 싶지 않다면, main 함수 선언부 앞에 Annotation을 적용해주면 된다.

 

unchecked 경고만 발생하는 것을 확인하자.

 

 

 

2. 메타 Annotation

 

메타 Annotation은 사용자 정의 Annotation을 생성할 때 사용하는 일종의 도구다. 이 말은 표준 Annotation 역시 메타 Annotation으로 만들어졌다는 것이다. Deprecated에 대한 내용을 Java Documentation에서 찾아보자.

 

 

 

 

내용을 보면 알겠지만 Annotation은 Interface의 일종이다. 에너테이션 선언은 위의 내용처럼 @interface "인터페이스명"으로 진행한다. 인터페이스를 정의하는 것과 동일하나, 인터페이스 앞에 @가 추가된다는 점만 차이가 있다.

 

 

 

@Interface에 메타 Annotation인 @Documented, @Retention, @Target 등이 적용되면 사용자가 정의한 Annotation이 된다. 따라서 Annotation을 정의하기 위해서 메타 Annotation의 구성 및 기능에 대해 알아야 한다.

 

참고로 메타 Annotation은 java.lang.annotation 패키지에 정의되어 있기 때문에 메타 Annotation 사용을 위해 해당 패키지를 Import 해주어야 한다.

 

 

 

(1) @Target

 

Target은 생성하려는 Annotation이 코드의 어느 위치(대상 타입)에 적용될 수 있는지를 정의한다. Java Documentation의 Deprecated를 보면 Target이 CONSTRUCTION, FIELD 등등으로 정의되어 있는데, 대상 타입의 종류와 의미는 다음과 같다.

 

 

 

위의 내용은 java.lang.annotation.ElementType의 Documentation 일부다. ElementType은 Enum으로 정의되어 있으며, @Target은 ElementType 요소를 인자로 사용한다. 

 

사용자 지정 Annotation 위에 @Target의 인자를 Method로만 지정한다면, 이 Annotation은 매서드에만 적용할 수 있다는 이야기다. @Target을 적용해보자. 

 

 

 

* 필자가 import static에 대한 내용은 작성하지 않았는데, static으로 패키지 Import를 진행하면 클래스나 필드명만 코드에 작성하더라고 컴파일러가 인식할 수 있게 된다. 위의 예시에서는 ElementType.METHOD라고 적지 않고 단순히 METHOD라고 적기 위해 static을 적용했다는 말이다.

 

이제 필자가 만든 Annotation은 매서드에만 적용이 가능하다. Test라는 클래스와 printInfo() 매서드 위에 이 Annotation을 동시에 적용해보자.

 

 

 

클래스에 적용된 Annotation은 "이 위치에 Annotation을 적용하는 것이 허용되지 않음" 이라는 에러를 발생시킨다. 만약 클래스 레벨에서도 Annotation을 적용시키고자 한다면 Target의 매개인자에 TYPE을 추가해주면 된다.

 

 

 

 

ElementType에서 FIELD는 클래스 내 기본 자료형 멤버 변수(static, 인스턴스 변수 모두) 및 Enum의 상수 요소를 일컫는다. LOCAL_VARIABLE과 헷갈리기 쉬운 개념이니 주의하자. 또한 TYPE은 클래스와 ENUM의 선언부에 사용할 수 있지만 TYPE_USE는 이들의 인스턴스 호출 등 해당 클래스 타입에 대해 변수를 선언할 때 적용할 수 있다.

 

 

 

(2) @Retention

 

Retention은 Annotation의 유지(Retention)와 관련된 메타 Annotation이다. 인자로 SOURCE, CLASS(기본값), RUNTIME 세 가지를 가질 수 있으며, 이들 인자는 java.lang.annotation 패키지의 RetentionPolicy라는이름의 열거형 내에 존재한다. 

 

아무 코드가 작성되지 않은 Annotation을 마커 Annotation이라 한다.

 

 

Retention은 첫 눈에 이해하기가 어려운 개념인데, 만든 Annotation이 단순히 각주를 추가한 것인지, 아니면 코드 실행 시에 코드 내 이상 여부를 확인하는지를 설정하는 개념이라 보면 이해가 쉽다. 각 인자값의 의미는 다음과 같다.

 

* SOURCE:  Annotation이 소스 파일(실제 코드. .java 파일 내)에만 존재함.

* CLASS    : Annotation이 클래스 파일에도 존재(컴파일 이후 .class 파일 생성 시 내부에 존재). 기본값.

* RUNTIME: Annotation이 클래스 파일에 존재하며, 실행 시 JVM에 로딩됨.

 

SOURCE는 말 그대로 실제 코드를 보는 누군가에게 "이 코드는 이러이러한 특성을 가지고 있습니다" 라고 알려주는 역할을 한다. 그렇기 때문에 SOURCE가 RetentionPolicy로 설정된 Annotation은 컴파일 시 컴파일러가 무시하고 넘어간다.

 

CLASS의 경우, 컴파일러가 소스 파일의 Annotation을 인지하여 .class 파일에 annotation에 대한 내용을 컴파일하긴 하지만, 실행 시 JVM에 로딩을 하지 않기 때문에 사실상 SOURCE와 비슷하게 동작한다. 실행 시 Annotation에 대한 정보가 나타나지 않기 때문에 기본값임에도 불구하고 잘 사용하지 않는다.

 

마지막으로 RUNTIME은 소스파일의 Annotation을 컴파일러가 인식하여 .class 파일에 유지시키기도 하지만, .class 파일 실행 시 Annotation 정보가 JVM에 올라가 동작하도록 만든다. 기본 Annotation에서는 @Override와 @SuppressWarnings가 이 타입에 속한다. 예시로 든 두 Annotation은 .class 파일 실행 시, 상속 클래스에 실제 매서드 명이 존재하는지, 혹은 경고가 존재하는지 확인해야하기 때문에 RUNTIME으로 지정된다.

 

 

(3) @Documented

 

Annotation 정보가 javadoc으로 작성한 문서에 포함되도록 하는 메타 Annotation이다. 자세한 내용은 추후 필자가 javadoc 사용과 관련된 내용을 포스팅하게 되면 다시 언급하려 한다.

 

 

 

(4) @Inherited

 

사용자가 생성한 Annotation에 @Inherited 가 적용되면, 클래스에 적용된 사용자 지정 Annotation은 자동으로 해당 클래스의 자손에게도 적용된다.

 

 

 

 

3. Annotation 정의 및 사용

 

이제 메타 Annotation과 @Interface를 사용하여 직접 Annotation을 제작해보려한다. @SuppressWarnings와 같이 Annotation 사용 시 매개인자를 넣는 종류도 존재함을 위에서 확인했다. 그럼 Annotation을 어떻게 정의를 했기에 매개변수를 대입하는 것이 가능할까?

 

아래와 같이 int 형 변수 하나를 Annotation에 정의해보자.

 

 

Annotation은 인터페이스와 내부 코드 작성 방식이 상이한데, Annotation의 인자로 받을 자료형을 마치 변수 선언하듯이 작성하면 된다. 하지만 변수명 뒤에는 소괄호()를 추가해준다는 차이점이 있다. 얼핏보면 추상매서드로 명시한 자료형을 돌려주는 것과 비슷해보인다.

 

이제 Annotation에 인자를 추가하고 클래스에 적용해보자. 인자 작성 시에는 Annotation 내부 코드의 매서드명을 반드시 명시한 뒤 값을 =로 대입해주어야 한다. 아래와 같이.

 

 

 

여기서 다시 의문이 생긴다. "@SuppressWarning이나 @Target의 인자는 리터럴 값만 작성했지, 매서드 이름은 작성하지 않았는데 어떻게 한 건가요?" 

 

직접 생성한 Annotation의 인자 매서드명이 value이고, 인자가 하나만 존재하는 경우 리터럴 값만 매개 인자에 대입하는 것이 가능해진다. 

 

 

 

이제 인자 개수와 타입을 늘려보자.

 

 

 

위의 예시를 보면 참조 객체인 String도 Annotation의 인자로 지정하는 것이 가능함을 확인할 수 있다. 여기서 유추해보면 다른 클래스 객체 역시 Annotation 인자로 사용하는 것이 가능할 듯 하다. 조금 더 내용을 추가해보자.

 

 

일차원 배열은 물론이고 다른 Annotation에 정의된 내용을 토대로 생성한 Annotation을 적용하는 것이 가능함이 확인된다. 그러나 Java에 정의된 클래스를 객체로 사용하면 다음과 같은 문구와 함께 컴파일 에러가 발생함을 확인할 수 있다. 필자가 Integer 타입의 인자를 받기 위해 다음과 같이 코드를 작성해보았다.

 

 

Annotation 내에서 받아들일 수 있는 인자 타입으로는 기본 타입(Primitive), String, Annotation, 열거형과 이들의 1차원 배열 뿐이다. 에러문에는 clas도 포함이 되는 것으로 나오나 Class를 직접 만들어서 진행해봐도 Class 객체타입은 매개변수로 지정할 수 없음을 확인했다.

 

만약, Annotation 적용 시 매개인자가 입력되지 않는 경우, 기본값을 지정하고 싶다면 Annotation 내부 매서드 뒤에 default 값을 입력해주면 된다.

 

 

 

 

 

4. java.lang.annotation.Annotation

 

모든 Annotation은 java.lang.annotation 패키지 내에 존재하는 Annotation이라는 클래스를 조상으로 지정한다. 따라서 모든 Annotation의 선언부는 Annotation을 extends로 상속할 것이라 생각하지만 Annotation은 상속이 허용되지 않으므로 extends Annotation 키워드를 추가하면 에러가 발생한다.

 

 

Annotation 인터페이스 내부에 코드가 없는 추상 매서드가 정의되어 있는데, 이들 매서드는 Annotation으로부터 직접 호출하는 것이 아니라, 하위 인터페이스인 개별 Annotation에 정의된 매서드를 사용한다. 

 

 

 

5. 일반 클래스에 적용된 Annotation 정보 확인하기

 

이제 각 클래스에 적용된 Annotation의 정보에 대해 확인해보자. 시작하기 전에, 코드 실행 후 적용된 Annotation의 정보가 나타나도록 만들기 위해서는 반드시 적용된 RetentionPolicy가 RUNTIME 타입이어야 한다는 것을 상기하자(본 포스팅의  @Retention) 내용을 참고하자.

 

 

 

각각의 클래스들은 class라는 필드값에 자기 자신의 Class 타입으로 정보를 저장한다. 이 Class 정보는 실행 시, ClassLoader에 의해 JVM에 적재되며, 클래스 이름 뿐만 아니라 내부에 정의된 매서드나 필드, Annotation 정보도 저장하고 있다.

 

 

 

Annotation 정보 역시 매서드 및 필드 정보를 얻는 방법과 다르지 않다. getAnnotations()라는 매서드로 클래스 레벨에 적용된 모든 Annotation 정보를 배열 형태로 반환받으며, for 문을 통해 개별 Annotation에 대한 정보를 출력하면 된다.

 

 

 

Annotation 인터페이스에 정의된 매서드들은 배열 내에 추출된 개별 Annotation을 통해 호출하여 사용이 가능하다. toString()의 경우, 실제 Annotation이 적용된 형태 그대로 값이 반환된다. 

 

클래스 내부 매서드에 적용된 @Deprecated와 @Override도 출력이 되어야 하는 것이 아닌가라는 의문이 있겠지만, 이들은 클래스 레벨에 적용된 것이 아니라 매서드 레벨에 적용되어 있기 때문에 결과에 나타나지 않는다. @Override는 매서드 레벨에서만 가능하니 제외하고 @Deprecated만 클래스 레벨로 적용하면  Annotation에 대한 결과도 다르게 출력된다.

 

Deprecated도 사실 forRemoval과 since를 통해 인자를 받는다. 모두 기본값 설정이 되어 인자를 넣지 않았던 것 뿐이다.

 

클래스에 적용된 개별 Annotation에 대한 정보 역시 추출이 가능하다. getAnnotation() 매서드는 매개 인자로 클래스에 적용된 Annotation의 클래스 정보를 받는데, 클래스에 매개인자 Annotaion이 적용되어 있다면 값을 반환하고, 없으면 아무것도 출력하지 않는다. 

 

 

개별 Annotation의 객체 타입은  Annotation 자체 인터페이스이기 때문에 getAnnotation() 매서드로 반환받은 값을 통해 내부의 인자에 직접적인 접근이 가능하다. 위의 예시에서도 MyAnnotation의 세부 정보를 인터페이스 내부에 정의된 매서드를 통해 키에 해당하는 값을 추출한 것이 보인다. 

 

getAnnotations()로도 반환받은 값을 특정 Annotation 인터페이스 객체로 형변환이 가능한 경우, 위의 값을 동일하게 출력하는 것도 가능하다.

 

Deprecated 역시 동일한 방법으로 각 키의 값만 추출하여 정리하는 것이 가능하다.

 

 


 

 

Java 의 Annotation은 마커 Annotation과 같이 단순히 각주를 입력하는 것에 그치지 않는다. 작성한 클래스에 대한 정보를 전달하기 위해 Javadoc을 사용하여 html 문서로 변환하는 기능은 물론이거니와, 각주 역할에 지니지 않는 Annotation을 활용하여 Java에서 처리한 데이터를 DB에 효율적으로 저장하도록 활용하는 것도 가능하다. 필자가 되도록이면 DB 부분도 테스트와 포스팅을 진행하고 싶지만, Linux를 내려놓은지 한참 되어 추후 기회가 된다면 다른 포스팅에서 다루려고 한다.자세한 내용은 영문 위키 백과의 내용을 참고하자.

 

 

다음 포스팅에서는 하나의 프로그램을 여러 분기로 나누는 쓰레드(Thread)에 대해 알아보려 한다.

 

 

Fin.

반응형

댓글