본문 바로가기
Java

[Java] 21. 어노테이션

by byeongoo 2019. 4. 20.

1. 어노테이션이란?

어노테이션은 클래스나 메소드 등의 선언시에 @를 사용하는 것을 말합니다. 어노테이션은 영어로 Annotation이며, 메타데이터 라고 불리기도 합니다. 메타데이터는 데이터에 대한 설명을 의미하는 데이터를 말합니다.(데이터에대한 데이터) 

 

● 컴파일러에게 정보를 알려줌

 

● 컴파일할 때와 설치시의 작업을 지정

 

● 실행할 때 별도의 처리가 필요할 때

 

이런 경우에 사용합니다. 이와 같이 매우 다양한 용도로 사용할 수 있는 어노테이션은 클래스, 메소드, 변수 등 모든 요소에 선언할 수 있습니다. 

 

2. 미리 정해져 있는 어노테이션

 메타 어노테이션은 선언을 위해서 존재하기 때문에 일반적으로 사용 가능한 어노테이션은 다음의 5개입니다. (자바 1.8기준)

 

● @Override

 

● @Deprecated

 

● @SupressWarnings

 

● @SafeVarargs

 

● @FunctionalInterface

@Override

해당 메소드가 부모 클래스에 있는 메소드를 Override 했다는 것을 명시적으로 선언합니다. Override를 할 때에는 부모 클래스에 있는 메소드의 이름과 매개 변수들을 동일하게 가져갑니다. 그런데, 만약 자식 클래스에 여러 개의 메소드가 있을 때, 어떤 메소드가 Override 되었는지 쉽게 알 수 없을 수도 있고, 제대로 메소드를 Override 했다고 생각했는데, 매개 변수가 하나 빠졌을 수도 있습니다. 따라서, 명확하게 이 메소드는 Override 된 것이니까 만약 잘못 코딩 했다면 컴파일러 너가 알려주라고 지정해주는 것 입니다.

@Deprecated

미리 만들어져 있는 클래스나 메소드가 더 이상 사용되지 않는 경우가 있습니다. 그런 것을 deprecaed라고 하는데, 컴파일러에게 얘는 더이상 사용하지 않으니까 그렇게 알아줘. 그리고 나중에 누가 이거 쓰면 경고 한번 주라고 일러 주는 것이라고 생각하면 됩니다.

@SuppressWarnings

코딩을 하다보면 컴파일러에서 경고를 알리는 경우가 있습니다. 그럴 때 컴파일러에게 "얘는 내가 일부로 이렇게 코딩한 거니까 너가 경고를 해 줄 필요가 없어라고 이야기 해주는 것 입니다. 

@SafeVarargs

Java7 부터 지원하며, 제너릭 같은 가변인자의 매개변수를 사용할 때의 경고를 무시합니다.

@FunctionalInterface

Java8 부터 지원하며, 함수형 인터페이스를 지정하는 어노테이션입니다. 만약 메서드가 존재하지 않거나, 1개 이상의 메서드(default 메서드 제외)가 존재할 경우 컴파일 오류를 발생 시킵니다.

 

 

이제 예제를 통해서 살펴봅시다. 먼저 Override부터 알아보겠습니다. 다음과 같이 Parent 클래스를 확장하는 AnnotationChild라는 클래스를 만듭시다. 그리고, Parent 클래스에 선언되어 있는 printName() 메소드를 Override 하자.

 

package c.inheritance;
    
public class Parent {
    
    public Parent(){
    	System.out.println("Parent Constructor");
    }
        
    public Parent(String name) {
        System.out.println("Parent(String) Constructor");
    }
        
    public void printName() {
        System.out.println("printName() - Parent");
    }    
}
package c.annotation;

import c.inheritance.Parent;

public class AnnotationChild extends Parent{
	@Override
	public void printName() {
		System.out.println("AnnotationChild");
	}
}

 

printName() 이라는 Parent 클래스에 선언된 메소드를 AnnotationChild 클래스에서 Override 했습니다. 만약 AnnotatonChild 클래스에 아주 많은 메소드들이 선언되어 있다면, 어떤 메소드가 부모 클래스에 있는 것을 Override 했는지 알아보기 힘들 것입니다. 따라서, 이렇게 @Override 어노테이션을 추가해주면 쉽게 확인이 가능합니다. 이 AnnotationChild 클래스를 컴파일해도 큰 문제 없어 컴파일이 되는 것을 볼 수 있습니다. 여기서 printName() 메소드에 String arg라는 매개 변수를 추가해봅시다. 

 

package c.annotation;

import c.inheritance.Parent;

public class AnnotationChild extends Parent{
	@Override
	public void printName(String arg) {
		System.out.println("AnnotationChild");
	}
}

이렇게 변경된 클래스를 컴파일하면 에러 메시지가 발생할 것입니다. 어노테이션으로 Override 한다고 했는데 그런 메소드가 부모 클래스에 없기 때문입니다. Parent 클래스에는 String을 매개 변수로 갖는 printName() 이라는 메소드를 갖고 있지 않습니다. 그런데, @Override라는 어노테이션으로 메소드를 Override 한다고 선언했으니, 당연히 에러가 발생할 수 밖에 없습니다 이렇게 제대로 Override했는지 확인 하는 수단으로 사용할 수 있습니다.

 

@Deprecated라는 어노테이션은 더 이상 사용하지 않는 클래스나 메소드를 선언하는 데 사용됩니다. 이것도 예제를 통해 살펴봅시다. Annotation 클래스에 noMoreUse라는 Deprecated 된 메소드를 만들어 봅시다. 

 

package c.annotation;

import c.inheritance.Parent;

public class AnnotationChild extends Parent{
	@Override
	public void printName() {
		System.out.println("AnnotationChild");
	}
	
	@Deprecated
	public void noMoreUse() {
		
	}
}

 

그리고 다음과 같이 AnnotationSample이라는 클래스를 만들고, 이 메소드를 호출하는 useDeprecated()라는 메소드를 추가합시다. 

 

package c.annotation;

public class AnnotationSample {
	public void useDeprecated() {
		AnnotationChild child = new AnnotationChild();
		child.noMoreUse();
	}
}

 

Deprecated 된 메소드를 컴파일 하면, Deprecated API를 사용하는 메소드가 있으니 javac 수행시 "-Xlint:deprecation" 이라는 옵션을 추가하여 컴파일을 다시 해서 확인하라는 메시지가 나옵니다. 그러면 noMoreUse()라는 메소드는 deprecated 되었으니 조심하라고 경고가 나타납니다. 컴파일 결과에는 경고가 있을 뿐이지 에러는 없습니다 따라서, 컴파일을 완료되었기 떄문에 클래스 파일은 생성되어 있을 것입니다. 그냥 지워버리면 되지 왜 이렇게 Deprecated라고 표시를 해야 하는거지라고 생각할 수도 있습니다. 만약 지워버린다면 지워버린 메소드나 클래스를 참조하는 다른 개발자가 만든 프로그램이 변경된 사항을 모르고 있다면, 컴파일할 때 에러가 발생할 것입니다. 그러므로, 하위 호환성을 위해서 Deprecated로 선언하는 것이 꼭 필요합니다. 

 

마지막으로 @SuperWarnings를 살펴봅시다. 방금 컴파일할 때 처럼 경고 메시지가 나타날 수 있습니다. @SupressWarnings 어노테이션은 이러한 경고가 발생할 때, "나도 알고 있으니까 그냥 눈 감아줘"라고 컴파일러에게 이야기해 주는 것입니다. 방금 만든 useDeprecated라는 메소드에 어노테이션을 추가해봅시다.

 

package c.annotation;

public class AnnotationSample {
	
	public static void main(String[] args) {
		AnnotationSample anno = new AnnotationSample();
		anno.useDeprecated();
	}
	
	@SuppressWarnings("deprecation")
	public void useDeprecated() {
		AnnotationChild child = new AnnotationChild();
		child.noMoreUse();
	}
}

 

이 어노테이션은 지금까지 살펴본 다른 어노테이션과는 다르게 소괄호 속에 문자열을 넘겨 주었습니다. 이렇게 어노테이션의 종류에 따라서, 속성값을 지정하는 것도 존재합니다. @SuppressWarnings 어노테이션을 지정해 준 다음에는 컴파일을 해도 아무런 메시지가 나타나지 않습니다. 하지만, 이 어노테이션을 너무 남용할 경우 Deprecated된 메소드를 사용해도 모르고 넘어갈 수도 있으니 유의하기 바랍니다.

 

3. 어노테이션을 선언하기 위한 메타 어노테이션

메타 어노테이션이라는 것은 어노테이션을 여러분들이 선언할 때 사용합니다. 메타 어노테이션은 다음과 같이 4개가 존재합니다.  

 

● @Target

 

● @Retention

 

● @Documented

 

● @Inherited

 

어노테이션을 선언할 때 꼭 이 4개를 모두 사용해야 하는 것은 아니지만, 4개가 있다는 것은 알아두면 좋습니다. 

 

@Target

어노테이션을 어떤 것에 적용할지를 선언할 때 사용합니다. 적용 가능한 대상은 다음과 같습니다.

요소타입 대 상
CONSTRUCTOR 생성자 선언 시
FIELD enum 상수를 포함한 필드값 선언 시
LOCAL_VARIABLE 지역변수 선언 시
METHOD 메소드 선언 시
PACKAGE 패키지 선언 시
PARAMETER 매개 변수 선언 시
TYPE 클래스, 인터페이스, enum 등 선언 시

 

@Retention

얼마나 오래 어노테이션 정보가 유지되는지를 선언합니다. 

  대 상
SOURCE 어노테이션 정보가 컴파일시 사라짐
CLASS 클래스 파일에 있는 어노테이션 정보가 컴파일러에 의해서 참조 가능함. 하지만, 가상 머신에서는 사라짐
RUNTIME 실행시 어노테이션 정보가 가상 머신에 의해서 참조 가능

 

@Documented

해당 어노테이션에 대한 정보가 Javadocs(API) 문서에 포함된다는 것을 선언합니다.

 

@inherited

모든 자식 클래스에서 부모 클래스의 어노테이션을 사용할 수 있다는 것을 선언합니다. 

 

4. 어노테이션 선언

예제를 통해서 어떤 식으로 어노테이션을 선언하는지 살펴봅시다. 

 

package c.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserAnnotation {
	public int number();
	public String text() default "This is first annotation";
}

 

가장 먼저 import 한 클래스들을 봅시다. 필자가 별도의 이야기는 안했지만, 어노테이션 관련 클래스들은 java.lang.annotation 패키지에 선언되어 있습니다. 어노테이션 선언부를 봅시다.

 

● @Target은 해당 어노테이션 사용 대상을 지정한다. 여기서는 ElementTYPE. METHOD를 소괄호 안에 넣어 줌으로써 이 어노테이션은 메소드에 사용할 수 있다고 지정된 것이다,

 

● @Retention은 어노테이션 유지 정보를 지정하는 데 사용한다. 소괄호 안에 RetentionPolicy.RUNTIME으로 지정하면 실행시에 이 어노테이션을 참조하게 된다.

 

● 어노테이션 선언 안에는 number()라는 메소드와 text()라는 메소드가 있다. number()의 리턴 타입은 int이며, text()의 리턴 타입은 String 이다. 이렇게 메소드처럼 어노테이션 안에 선언해 놓으면, 이 어노테이션을 사용할 때 해당 항목에 대한 타입으로 값을 지정해 주어야만 한다

 

● text()를 보면 default라는 예약어를 쓴 뒤 문자열이 지정되어 있는 것을 볼 수 있다. 이처럼 default 예약어를 사용할 경우에, default 뒤에 있는 값이 이 어노테이션을 사용할 때 별도로 값을 지정해 주지 않을 때의 값이 된다.

 

그러면 만든 @UserAnnotation을 사용하는 클래스 예제를 봅시다.

 

package c.annotation;

public class UserAnnotationSample {
	@UserAnnotation(number=0)
	public static void main(String[] args) {
		UserAnnotationSample sample = new UserAnnotationSample();
	}
	
	@UserAnnotation(number = 1)
	public void annotationSample() {
		
	}
	
	@UserAnnotation(number=2, text="second")
	public void annotationSample2() {
		
	}
	
	@UserAnnotation(number=3, text="third")
	public void annotationSample3() {
		
	}
}

 

이렇게 메소드를 선언할 때 직접 만든 어노테이션을 사용할 수 있습니다. 어노테이션 선언 클래스에 지정해 놓은 각 메소드의 이름에 해당하는 값을 소괄호 안에 넣어 주어야만 합니다. 즉, number() 와 text()에 해당하는 어노테이션 값들을 지정해 주면 됩니다. 추가로 text()의 경우 default를 사용하여 기본값을 지정해 주었기 때문에 별도로 값을 지정해 주지 않아도 컴파일하는 데 전혀 문제가 없습니다. 하지만, number()는 기본값이 지정되어 있지 않으므로 반드시 값을 정해주어야만 합니다. 추가로 한가지 더 살펴 봅시다. 방금 살펴본 UserAnnotation은 대상이 메소드뿐입니다. 그런데, 만약 생성자나 클래스에서도 사용할 수 있도록 하려면 어떻게 해야 할까요? UserAnnotation에서 사용한 @Target 어노테이션을 다시 한번 봅시다.

 

@Target(ElementType.METHOD)

 

만약 두개 이상의 어노테이션을 선언할 때에는 중괄호를 한 후 쉼표로 구분해 주면 됩니다. 만약 클래스 선언시와 메소드 선언시에 어노테이션을 사용할 수 있도록 하려면, 다음과 같이 지정하면 됩니다.

 

@Target({ElementType.METHOD, ElementType.TYPE})

 

이렇게 선언해 두면, 메소드와 클래스에 해당 어노테이션을 사용할 수 있습니다. 만약 이 범위를 벗어나서 선언하게 되면 컴파일 시 에러가 발생합니다. 예를 들어 대상을 메소드로만 한정해 놓고, UserAnnotationSample 클래스의 선언부에 어노테이션을 다음과 같이 지정하면

 

package c.annotation

@UserAnnotation(number=0)
public class UserAnnotationSample{
//중간 생략
}

 

컴파일할 때 에러 메시지와 함께 컴파일이 되지 않습니다. 다음 예제를 위해 방금 추가한 클래스 선언시 사용한 어노테이션은 지우도록 하겠습니다.

 

5. 어노테이션에 선언한 값은 어떻게 확인할까요?

구현한 어노테이션의 값은 어떻게 확인할 수 있을까요? 일반적으로는 여러분들이 어노테이션을 만들 일은 없습니다. 따라서, 어노테이션에서 선언한 값을 확인하는 코드를 만들 일도 거의 없을 것입니다. 하지만, 한번 확인은 해 봅시다. UserAnnotationSample 클래스에 다음의 메소드를 추가합시다.

 

package c.annotation;

import java.lang.reflect.Method;

public class UserAnnotationSample {
	@UserAnnotation(number=0)
	public static void main(String[] args) {
		UserAnnotationSample sample = new UserAnnotationSample();
		sample.checkAnnotations(UserAnnotationSample.class);
	}
	
	@UserAnnotation(number = 1)
	public void annotationSample() {
		
	}
	
	@UserAnnotation(number=2, text="second")
	public void annotationSample2() {
		
	}
	
	@UserAnnotation(number=3, text="third")
	public void annotationSample3() {
		
	}
	
	public void checkAnnotations(Class useClass) {
		Method[] methods = useClass.getDeclaredMethods();
		for(Method tempMethod:methods) {
			UserAnnotation annotation = 
					tempMethod.getAnnotation(UserAnnotation.class);
			if(annotation != null) {
				int number = annotation.number();
				String text = annotation.text();
				System.out.println(tempMethod.getName()
					+"() : number=" +number+ " text="+text);
			} else {
				System.out.println(tempMethod.getName()+"(): annotation is null.");
			}
		}
	}
}

여기서 Class, Method라는 것은 자바의 리플렉션이라는 API에서 제공하는 클래스들입니다. Class라는 클래스는 클래스의 정보를 확인하고, Method라는 클래스는 메소드의 정보를 확인하는 데 사용합니다. 여기서 코드의 주요 부분을 살펴봅시다.

 

● Class 클래스에 선언되어 있는 getDeclaredMethods() 메소드를 호출하면, 해당 클래스에 선언되어 있는 메소드들의 목록을 배열로 리턴합니다. 

 

● Method 클래스에 선언되어 있는 getAnnotation()이라는 메소드를 호출하면, 해당 메소드에 선언되어 있는 매개 변수로 넘겨준 어노테이션이 있는지 확인하고, 있을 경우 그 어노테이션의 객체를 리턴해 줍니다.

 

● 어노테이션에 선언된 메소드를 호출하면, 그 값을 리턴해줍니다.

 

실행결과를 보면 다음과 같이 출력될 것입니다.

 

main() : number=0 text=This is first annotation
checkAnnotations(): annotation is null.
annotationSample() : number=1 text=This is first annotation
annotationSample2() : number=2 text=second
annotationSample3() : number=3 text=third

 

이렇게 리플렉션 API를 사용하면 여러분들이 선언하여 사용하는 어노테이션에 대한 정보를 확인할 수 있습니다.

 

6. 어노테이션은 상속 X

자바의 상속은 많은 이점을 제공합니다. 앞서 살펴봄 enum 클래스가 상속을 지원하지 않듯이, 어노테이션을 선언할 때에도 미리 만들어 놓은 어노테이션을 확장하는 것이 불가능합니다. 즉, extends라는 예약어를 사용할 수가 없습니다. 어노테이션을 여러분들이 선언하여 사용할 일은 그리 많지 않습니다. 

 

* 정리

이번 장에서는 어노테이션에 대해 알아 보았습니다. 어노테이션을 직접 선언하여 개발할 일은 거의 없습니다. 이번장에서는 @Override, @SuppressWarnings, @Deprecated 어노테이션은 꼭 기억해 둡시다. 단순히 어노테이션을 사용하는 방법만 알면 실력이 늘지 않습니다. 어노테이션을 지정하면 코드가 내부적으로 어떻게 변환되는지에 대해서 살펴보는 버릇을 가지는 것이 좋습니다.

'Java' 카테고리의 다른 글

[Java] compareTo() 문자열 비교 함수  (0) 2019.11.05
[Java] 22. JAR (Java ARchive Files) 파일  (0) 2019.06.04
[Java] 20. 예외 처리(3)  (0) 2019.04.18
[Java] 20. 예외 처리(2)  (0) 2019.04.18
[Java] 20. 예외 처리(1)  (0) 2019.04.17