JAVA

JAVA의 정석 정독하기 #12.3 - Annotation

dochi1017 2023. 11. 20. 23:40

어노테이션이란?

"프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것"

주석처럼 프로그래밍 언어에 영향을 끼치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다.

@Test
public void method() {...}

 

다음처럼 @ 뒤에 어노테이션 이름을 적으면 사용할 수 있다.

"@Test 어노테이션은 이 메서드를 테스트해야 한다." 라는 정보를 테스트 프로그램에 알리는 역할을 할 뿐,

메서드가 포함된 코드 자체에는 아무런 영향을 끼치지 않는다.

 

표준 어노테이션

자바 표준 어노테이션은 자바에서 기본적으로 제공하는 어노테이션이다.

대부분은 컴파일러에게 정보를 제공하는 역할을 한다.

 

@Override

@Override
public String toString() {...}

public String toStrint() {...}

메서드 앞에만 붙일 수 있는 어노테이션이다.

조상의 메서드를 오버라이딩하는 것이라는 정보를 컴파일러에게 알려준다.

 

두번째 코드와 같이 오버라이딩시에 조상 메서드의 이름을 잘못 써도

컴파일러는 이것이 잘못된 것인지 알지 못하고 새로운 이름의 메서드가 추가된 것으로 인식한다.

 

그러나 @Override 어노테이션을 붙이면,

컴파일러가 같은 이름의 메서드가 조상 클래스에 존재하는지 확인한 후,

없으면 에러메시지를 출력해준다.

 

필수는 아니지만, 알아내기 어려운 실수를 미연에 방지해주므로, 반드시 붙이자.

 

@Deprecated

새로운 버전의 JDK가 소개될 때, 새로운 기능이 추가되고, 기존의 기능을 개선하기도 한다.

(Date, Calendar 클래스의 문제를 해결하기 위해 java.util.time 패키지가 추가된 것처럼)

 

그러나 이 과정에서 이미 여러 곳에서 사용되었던 기존의 것들을 함부로 삭제할 순 없다.

제네릭이 로우 타입을 지원하는 것도 이전 코드와의 호환성을 위해서라고 저번 글에서 이야기했다.

 

그래서 나온 것이 @Deprecated 어노테이션이다.

더 이상 사용되지 않는 필드나 메서드에 이 어노테이션을 붙여 표시하는 용도로 사용한다.

 

@Deprecated 어노테이션이 붙은 메서드나 필드를 사용하면 컴파일 시에 경고 메세지를 보내준다.

해당 소스파일이 ‘deprecated’된 대상을 사용하고 있으니 확인해보라는 의미이다.

물론 에러를 띄우거나 코드가 동작하지 않는 것은 아니다.

단순히 사용하지 않기를 권장할뿐, 강제하지는 않는다.

 

@ FunctionalInterface

함수형 인터페이스를 선언할 때 이 어노테이션을 붙이면 컴파일러가 올바르게 선언했는지 확인해준다.

라는데 책 뒤에 내용에서 함수형 인터페이스에 관해 더 자세히 설명하는 내용이 나온다고 한다.

 

@SuppressWarnings

컴파일러가 보여주는 경고메세지가 나타나지 않게 억제해준다.

책에서는 컴파일러의 경고 메시지는 무시하고 넘어갈 수 있지만,

모두 확인하고 해결해서 컴파일 후에 어떠한 메세지도 나타나지 않게 해야 한다고 이야기한다.

 

그러나 경우에 따라서는 경고가 발생할 것을 알면서도 묵인해야할 때가 있는데,

이 경고를 그대로 놔두면 컴파일할 때마다 메세지가 나타나고 다른 경고들을 놓치기 쉽다.

 

따라서 묵인해야하는 경고가 발생하는 대상에 반드시 @SuppressWarnigs 어노테이션을 붙여

경고 메세지가 나타나지 않도록 해야 한다.

 

억제할 수 있는 경고 메세지의 종류는 여러 가지가 있는데 주로 사용되는 것은 다음과 같다.

  • deprecation : @Deprecated가 붙은 대상을 사용해서 발생한다.
  • unchecked : 제네릭스로 타입을 지정하지 않았을때 발생한다.
  • rawtypes : 제네릭을 사용하지 않아서 발생한다
  • varargs : 가변인자의 타입이 제네릭 타입일 때 발생한다.

unchecked 경고와 rawtypes 경고는 비슷한 것 같은데 무슨 차이가 있을까?

 

unchecked 경고

unchecked 경고에 대해 구글에 검색해보니까

모든 블로그 글이 이펙티브 자바의 "무점검 경고를 제거하라" 파트를 정리한 글만 나온다.

 

그런데 정작 unchecked 경고가 어떤 상황에서 발생하는지에 대해선 다루는 글이 없었다.

 

unchecked 경고는 컴파일러가 타입 안전성을 검사하지 못할 때 발생하며, 프로그래머에게 형 변환 시에 경고를 주는 역할을 한다.

제네릭 타입 객체에서 raw type을 사용 시에는 예외를 발생시킬 가능성이 높기 때문에 사용하기만 해도 보통 발생한다.

 

그럼 rawtypes 경고는요?

다음과 같이 필드나 변수에 로우 타입을 사용할 경우에 발생한다고 이야기한다.

private List list = new ArrayList(); //rawtypes warning
list.add(1); //unchecked call warning
List<String> listString = (List<String>) list; //unchecked casting warning

System.out.println(list2.get(0)); //ClassCastException

 

그럼 unchecked 경고가 어떤 상황에서 발생하는지는 알았다.

그럼 왜 발생하는 걸까?

 

unchecked 경고는 제네릭의 타입 안정성을 해칠 가능성이 있는 코드에서 발생한다.

놀랍게도 위의 코드는 컴파일 시에는 문제가 없다!

 

아니 근데 로우 타입 컬렉션 객체를 캐스팅 하는 코드가 있다면 컴파일 에러를 띄워주면 되는거 아닌가요?

그럼 기존의 코드와 호환이 불가능하다. 기존의 코드를 재사용하려면 로우 타입 객체를 캐스팅 해야하는 상황이 생길 수 있다.

 

그래서 컴파일러는 unchecked 경고만 띄워주고 컴파일을 해준다.

그런데 저번 글에서 설명했듯이 제네릭은 컴파일 후에 소거된다.

 

List<String> 타입은 List<Object> 타입이 되며, 반환 메서드에 (String) o 로 형변환 하는 코드가 추가될 뿐이다.

그런데 엥 리스트 안에 Integer 타입이 들어가 있네?

Integer → String으로 형변환을 하려니까 ClassCastException이 발생하는 것이다.

 

이러한 상황을 힙 오염이라고 이야기한다.

힙 메모리에 의도하지 않은 불량 데이터가 들어가고 참조되는 것을 이야기한다.

 

다시 돌아와서 이러한 경고를 그냥 아무렇게나 무시할 경우 힙 오염과 런타임 에러가 마구마구 생겨버릴 수 있다.

 

그런데 왜 이펙티브 자바는 unchecked 경고를 지우라고 할까?

 

정확히는 지워도 되는 경고인지 확인 후에 지우라는 것이다.

타입 안정성이 확보 되는지 확인 후 왜 확보되는지까지 주석을 달고

@SuppressWarnings를 이용해 지우라는 의미다. 더 자세한 내용은 이펙티브 자바를 읽어보자.

 

본론으로 돌아와서 @SuppressWarnings 어노테이션은 이러한 경고를 나타나지 않게 억제해준다.

그러나 경고가 나타나는데에는 항상 이유가 있으므로 잘 확인하고 지우자.

 

사용법은 @Suppressed(”unchecked”)와 같이 무시할 경고의 종류를 괄호 안에 넣고

무시할 경고가 발생하는 코드 앞에 붙여주면 된다.

 

둘 이상의 경고가 발생할 경우 @Suppressed({”unchecked”, “rawtypes”}) 처럼 괄호를 추가로 사용하면 된다.

 

@SafeVarags

메서드에 선헌된 가변인자 타입이 non-reifiable 타입인 경우(제네릭 등 컴파일 후에 타입 정보가 제거될 경우)

선언부와 호출시 모두 unchecked 경고가 발생한다.

 

이때 선언하는 곳에 @SafeVarargs를 붙이면 호출하는 곳에서 발생하는 경고도 억제된다.

그러나 이름과 다르게 unchecked 경고를 억제할 뿐 varargs 경고는 억제하지 못하므로

@SuppressWarnings(”varargs”)를 보통 같이 사용한다.

 

사용자 정의 어노테이션

위와 같이 이미 존재하는 어노테이션 외에도 사용자가 직접 어노테이션을 추가해서 사용할 수 있다.

@Target(ElementType.TYPE_USE)
****@****Retention****(****RententionPolicy.RUNTIME)
public @interface MyAnnotation{
  String value();
}

@MyAnnotation("hi")
public void MyMethod();

다음과 같이 @interface를 이용해 선언한다.

어노테이션 안에 멤버 함수를 선언함으로서, 어노테이션 사용시에 값을 받아올 수 있으며 이러한 값을 엘리먼트라고 한다.

만약 value라는 이름의 엘리먼트 하나만 존재한다면, 사용시에 생략이 가능하다.

 

선언부에 @Target, @Retention과 같은 어노테이션이 붙어있는데 이러한 어노테이션은 메타 어노테이션이라고 한다.

 

메타 어노테이션이란?

메타 어노테이션은 사용자 정의 어노테이션에 붙이는 어노테이션이다.

어노테이션 선언 시 메타 어노테이션을 사용해서 해당 어노테이션에 기능이나 정보를 추가할 수 있다.

 

@Target

어노테이션의 사용범위를 지정하는데 사용한다.

@Target(ElementType.ANNOTATION_TYPE)과 같이 사용한다.

사용할 수 있는 옵션이 java.lang.annotation.ElementType에 정의되어 있다.

  • ElementType.ANNOTATION_TYPE : 어노테이션
  • ElementType.CONSTRUCTOR : 생성자
  • ElementType.FIELD : 필드(멤버 변수, Enum 상수)
  • ElementType.LOCALVARIABLE : 지역변수
  • ElementType.METHOD : 메서드
  • ElementType.PACKAGE : 패키지
  • ElementType.PARAMETER : 매개변수(파라미터)
  • ElementType.TYPE : 클래스, 인터페이스, Enum
  • ElementType.TYPE_PARAMETER : 타입 매개변수(제네릭과 같은 매개변수)
  • ElementType.TYPE_USE: 모든 대상

@Documented

어노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함하도록 하는 어노테이션 설정이다.

 

javadoc이란?

JDK와 함께 패키지로 제공되는 도구이다.

JAVA 소스코드의 코드 문서를 생성하는데 도움을 주는 도구이다.

여기서 설명하기엔 너무 길어질 것 같아서, 잘 정리해놓은 글을 가져왔다.

Javadoc이란? Javadoc 사용방법

 

Javadoc이란? Javadoc 사용방법

Javadoc이란? Javadoc은 JDK와 함께 패키지로 제공되는 도구 입니다. JDK가 설치 되어있다면 Javadoc을 사용할 수 있으며, Java 소스 코드의 코드 문서를 생성하는데 도움을 주는 도구 입니다. Javadoc의 사

agileryuhaeul.tistory.com

 

@Inherited

하위 클래스도 해당 어노테이션을 상속받도록 하는 어노테이션 설정이다.

즉 @Inherited 어노테이션을 상위 클래스에 붙이면, 하위 클래스에까지도 동일하게 적용된다.

 

@Retention

어노테이션의 지속 시간을 결정한다.

3가지의 옵션을 설정할 수 있다. 이를 유지 정책이라고 부른다.

  • RententionPolicy.SOURCE : 자바 소스에만 존재한다. 즉 컴파일시에 사라지게 된다.
  • RententionPolicy.CLASS (기본값) : 클래스 파일까지만 존재한다. 런타임시 실행시에 사용하지 않는다.
  • RententionPolicy.RUNTIME : 클래스 파일에 남아있으며 실행 시에도 사용한다.

아무 옵션도 사용하지 않을 시 기본값인 CLASS로 설정되지만, 책에서는 실제로 사용할때에는 보통 SOURCE 또는 RUNTIME만 사용한다고 이야기한다.

 

실제 스프링 프로젝트에서 @Login 어노테이션으로 Argument Resolver를 정의할때 사용했던 코드이다.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Login{
}

 

해당 어노테이션은 다음과 같이 사용됐다.

@GetMapping("/admin")
public String newHome(@Login User findUser, Model model){
	...
}

컨트롤러의 메서드 파라미터에 넘어온 값을 가공하고 처리하는 Argument Resolver가

@Login 어노테이션이 붙어있는지 확인해서 붙어있다면 설정해놓은 로직을 수행해준다.

 

프로그램이 실행 중에 메서드에 어노테이션이 붙어있는지 확인하고 로직을 수행해야 하기 때문에

유지 정책을 RUNTIME으로 설정했다.

 

@Repeatable

해당 어노테이션을 반복해서 사용할 수 있도록 하는 어노테이션이다.

@Repeatable(Colors.class)
public @interface Color{
  String value();
}

public @interface Colors{
  Color[] value();
}

@Color("red")
@Color("blue")
@Color("green")
public class RGBColor {...}

위와 같이 정의해서 사용하면 같은 어노테이션을 여러번 사용할 수 있게 된다.

정의할때에는 실제로 사용할 어노테이션과 그 어노테이션 묶음 값을 관리하는

컨테이너 어노테이션을 같이 작성해줘야 한다.

 

어노테이션의 동작 원리

이렇게 사용자 정의 어노테이션을 만들기만 하면 도대체 어떻게 써먹는 것인지 궁금해졌다.

어노테이션이 단순한 주석과 같이 표시하는 것이 아닌 특별한 기능을 가지도록 하려면 어떻게 해야할까?

 

바로 Reflection을 사용하면 된다.

Class 클래스 안에는 다음과 같은 메서드들이 존재한다.

Field[] getFields() //필드 정보를 배열로 리턴
Constructor[] getConstructors() //생성자 정보를 배열로 리턴
Method[] getMethods()//메소드 정보를 배열로 리턴

그리고 이렇게 해서 얻은 필드, 생성자, 메서드 정보를 담은 클래스 혹은 Class 클래스에서

어노테이션 정보를 읽어오는 것이 가능하다.

boolean isAnnotationPresent(Class<? Extends Annotation> annotationClass)
Annotation getAnnotation(Class<T> annotationClass)
Annotation[] getAnnotations()
Annotation[] getDeclaredAnnotations()

따라서 클래스나 메서드에 어노테이션이 붙어있는지 확인 후 특정 로직을 수행하게 할 수 있다.

 

@Retention(RetentionPolicy.RUNTIME)
@interface PrintAnnotation{
}

@PrintAnnotation
class MyClass{}

public static void main(String[] args) {
  MyClass m = new MyClass();

  if(MyClass.class.isAnnotationPresent(PrintAnnotation.class)){
    System.out.println("hi");
  }
  ...
}

스프링에서 스프링 빈을 자동으로 등록해주는 @Component 어노테이션도 이런 방식으로 작동한다.

스프링 어플리케이션을 구동하면 ClassPathBeanDefinitionScanner의 scan() 메서드에서

ClassPath에 있는 모든 패키지의 클래스들을 읽은 후

@Component 어노테이션이 존재하는 클래스들을 찾아 빈으로 등록하는 로직을 수행한다.

 

또한 직접 어노테이션을 만든 후 AOP를 이용해

해당 어노테이션이 붙어있는 메서드 실행 시 추가적인 공통 로직을 수행하는 식으로 제작할 수도 있다.

(구글링 하니까 실행 시간 분석, 로그 남기기 등을 하는 예제가 많이 보였다)

 

Lombok의 경우도 reflection을 이용한다고 볼 수 있지만 앞선 예재와는 달리 컴파일 시점에 어노테이션이 있는지 찾는다.

컴파일 명령 → AST 트리 생성 → Annotation Processor 가 어노테이션 확인 → AST 트리 변경 → 컴파일

순서로 진행된다는데 지금 단계에서 짚어보기엔 내용이 너무 많아서 나중에 더 공부해보고 따로 글을 써봐야겠다.

 

자바의 정석 정독이 끝나면 Reflection, Annotation Processor에 대해서 더 공부해봐야겠다.