프로그래밍 개발을 하면 주석은 필수에요. 프로그래밍에 영향을 주지 않으면서 필수적인 기록들을 하고 이후에 저 자신이나 같은 일하는 개발자들이 코드를 이해하는데 큰 도움이 되요.

// 단칸 주석
/*
여러 칸을
기록할 수 있는 주석
*/

이 뿐만이 아니라, 자바의 경우 javadoc 프로그램을 통해 HTML 형식으로 작성된 주석을 읽어들여 API로 제공하는 인터페이스나 메서드를 문서 자동화를 해줘요.

지금 정리할 애너테이션annotation은 주석(실은 주석이 영어로 annotation이긴 해요 ㅎㅎ)처럼 프로그래밍에 영향을 미치지 않으면서 프로그램에게 유용한 정보를 제공해주어요. 이 뿐만 아니라 프로그래밍 하는데 있어 여러 편리한 기능도 제공해요.

표준 애너테이션

자바에서 기본적으로 제공하는 애너테이션은 많아요. 이 중 자주 사용되는 부분만 정리할게요.

@Override

상속된 메서드임을 프로그램에 알려주는 애너테이션이에요. 다형성을 활용하기 위해선 상속하려는 메서드 명을 정확하게 일치해서 작성해야 해요. 그렇지 않으면 의도하지 않은 결과가 발생해요. @Override를 선언하지 않아도 상속은 가능하나, 메서드 명이 정확하게 작성된 것인지 확인해야 하죠. 메서드가 정확하게 작성되었는지를 컴파일링 단계에서 확인할 수 있어요.


class A {
	void polymorphismMethod() {}
}

class AEx extends A {
	// 실수로 M을 m으로 작성했어요
    // 이 때 Override 애너테이션을 통해 상속을 한 건지 확인 가능해요.
    // 해당 메서드는 컴파일 에러가 떠요
    @Override
	void polymorphismmethod() {}
}

@Deprecated

프로그램에게 해당 필드나 메서드는 더 이상 사용되지 않음을 알려줘요. 프로그램을 업데이트하는 과정에서 불필요하거나, 개선하는 과정에서 다른 메서드를 사용해 기존 로직을 사용하지 않게 해야 하지만, 기존에 자주 사용한 코드를 삭제기엔 호환성이 떨어질 수 있어요.

그래서 @Deprecated를 선언함으로써 동작은 하지만 더이상 쓰이지 않게될 예정인 필드나 메서드를 애너테이션 선언해서 컴파일 과정에서 경고를 날려서 알려주는 거에요.

// 이렇게 사용하지 않을 예정인 변수나 메서드, 클래스 등에 선언해주면 되요.
// 주의할 점은 변수를 완전히 사용하지 않을 예정이라면 getter, setter인 메서드에도 같이 해줘야 해요.
@Deprecated
public static int i = 1;
@Deprecated
public static int getI() {
    return i;
}
public static void main(String[] args) {
    Main.i = 1;
    Main.getI();
}   

@FunctionalInterface

함수형 인터페이스임을 알려주는 애너테이션이에요. 인터페이스interface에서만 사용하고, 추상 메서드가 오직 하나여야 해요. 왜냐하면 함수형 인터페이스는 추상 메서드가 하나여야만 해요. 이를 선언함으로써 다른 추상 메서드를 선언하는 것을 방지해줘요.

람다식은 함수가 한개에요. 함수형 인터페이스는 보통 람다식과 대응하는 클래스를 선언해서 사용할 때 제작해요. 오직 하나의 추상 메서드만 만들 수 있다는 점에서 둘이 1:1로 연결될 수 있는거죠. 하지만 static 메서드나 defualt 메서드는 얼마든지 만들 수 있어요.

@FunctionalInterface
interface MyMath {
    public static int max(int a, int b) {
        return a >= b ? a : b;
    }

    default int mult(int a, int b) {
        return a * b;
    }

    default int div(int a, int b) {
        return a / b;
    }

    public int min(int a, int b);
}

// 이렇게 선언하거나
MyMath myMath = new MyMath() {
    @Override
    public int min(int a , int b){
        return a <= b ? a : b;
    }
};

// 아예 람다식을 넣어서 할 수 있어요. 
// 함수형 인터페이스의 조건인 추상 메서드가 하나이니까 가능한 거에요.
MyMath myMath2 = (a, b) -> { return a < b ? a : b; };

MyMath.max(1, 2);
myMath.min(1, 2);
myMath.mult(1, 2);
myMath.min(1, 2);

@SuppressWarnings

@Deprecated가 경고를 날려준다면 @SuppressWarnings는 이 경고를 나타나지 않게 해줘요. 프로그래밍 과정에서 사용되지 않을 예정인 필드나 메서드를 사용해야하는 경우가 있어요. 문제가 없이 작동되고 있음을 알지만 수시로 나오는 경고 메시지는 오히려 이후에 있을 문제에 대응하는데 어려움이 있어요. 그래서 불필요한 경고 메시지를 무시하기 위해 선언해요.

무시 종류

@SuppressWarings가 무시할 경고 메시지 유형은 다양해요. @SuppressWarning({무시할 경고 메시지 문자열})을 통해 선언할 수 있어요. JDK 버전이 올라가면서 다양해지겠지만 이 중 자주 사용되는 것을 나열하자면,

  1. "deprecation" : @Deprecated가 선언된 경고 무시
  2. "unchecked" : 지네릭스 타입을 지정하지 않았을 때 발생하는 경고 무시
    3."rawtypes" : 지네릭스 타입을 사용하지 않아서 발생하는 경고 무시
  3. "varargs" : 가변인자의 타입이 지네릭 타입일 때 발생하는 경고 무시

경고 무시를 두 개이상 하고 싶다면 @SupressWarnings({"rawtypes", "varargs"}) 형식으로 선언해주면 되요.

문제가 될 수 있는 코드를 컴파일 시, 경고 메시지 상세 내용을 보고 싶으시면 javac -Xlint {*.java}를 입력하시면 되요.

@Deprecated
static int i = 1;

@SuppressWarnings("rawtypes")
public static void print(List arr) {
    for(var a : arr){
        System.out.println(a);
    }
}

public static void main(String[] args) {
    Main.i = 2;

    @SuppressWarnings("rawtypes")
    ArrayList list = new ArrayList(10);

    @SuppressWarnings("rawtypes")
    ArrayList<Integer> list2 = new ArrayList();

    print(list);
}

애너테이션이 있는 부분을 모두 주석 처리하고 컴파일 시 아래와 같은 결과가 나와요. 애너테이션이 선언되어 있으면 해당 부분 경고 메시지는 더이상 표시되지 않아요.

@SafeVarArgs

메서드에 선언된 가변인자 타입이 non-refiable, 즉 컴파일 후에 제거되는 타입일 경우 unchecked 경고가 발생해요. 지네릭스에 대한 내용을 공부하셨다면 지네릭 타입은 컴파일 이후에 정해져서 제거된다는 사실을 아신다면 이에 대한 경고 무시를 해주는 애너테이션이라고 이해하시면 되요.

매개변수가 가변인자인 동시에 지네릭 타입이라면 이는 컴파일 과정에서 Object 타입으로 바뀌고, 가변인자니까 Object[]로 되요. Object[]에는 모든 타입이 들어가니까 위험하다고 판단되어 경고를 날려요. 하지만, 논리상 맞다면 이를 무시해주게 하는게 @SafeVarArgs에요.

// 누가 봐도 무해한 지네릭 타입 가변인자!
// 따라서 애너테이션 선언해서 경고를 무시해줘요.
@SafeVarargs
public static <T>void print(T... arr){
    for(T a : arr){
        System.out.println(a);
    }
}    

여기서 @SafeVarArgs"unchecked" 경고를 억제할 수 있지만, "varargs" 경고는 억제할 수가 없어요. 그래서 @SafeVarArgs 사용하려는 경우 @SuppressWarnings("varargs")를 같이 사용해요.

@SafeVarargs
@SuppressWarnings("varargs")
public static <T>void print(T... arr){
    for(T a : arr){
        System.out.println(a);
    }
}    

메타 애너테이션(Meta Annotation)

메타 애너테이션Meta annotation은 '애너테이션을 위한 애너테이션', 즉 애너테이션에 붙이는 애너테이션이에요. 애너테이션을 정의할 때 적용 대상Target이나 유지기간Retention 등을 지정할 때 사용되요.

@Target

해당 애너테이션을 적용하려는 대상을 지정해요. 적용 대상을 나열하면 아래와 같아요.

  1. ANNOTATION_TYPE : 애너테이션
  2. CONSTRUCTOR : 생성자
  3. FIELD : 멤버 변수, enum 상수 \to 기본형에 사용
  4. LOCAL_VARIABLE : 지역변수
  5. METHOD : 메서드
  6. PACKAGE : 패키지
  7. TYPE : 클래스, 인터페이스, enum
  8. TYPE_PARAMETER : 타입 매개변수
  9. TYPE_USE : 타입이 사용되는 모든 곳
    \to 참조형에 사용이라곤 하는데 JDK 11 기준으론 기본형에도 사용이 가능

사용 예 \to@Target({ FIELD, METHOD, TYPE, LOCAL_VARIABLE })

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

@Target({ ElementType.TYPE_USE, ElementType.METHOD })
@interface MyAnnotation {}
@MyAnnotation	// TYPE_USE
public class Main {

    @MyAnnotation // METHOD
    public static void func() {}
    public static void main(String[] args) {
        @MyAnnotation	// TYPE_USE
        int i = 1;

        func();
    }
}

@Retention

애너테이션이 유지되는 기간을 지정하는데 사용해요. 유지 기간 종류는 아래와 같아요.

  1. SOURCE : 소스 파일에만 존재, class 파일에는 없음
  2. CLASS : class 파일에는 있으나 실행 시 사용 불가 \to default
  3. RUNTIME : class 파일에도 존재하면서 실행 시에도 사용 가능

사용 예 \to @Retentation(RetensionPolicy.SOURCE)

@Override@SuppressWarning 같은 애너테이션은 컴파일 시에만 필요하지 실행할 때 필요로 하지 않아요. 그래서 이 경우 유지 기간을 SOURCE로 잡아줘요. @FuntionalInterface는 컴파일러가 체크를 해주면서 실제 프로그램 실행 시에도 사용되므로 RUNTIME으로 잡아주죠. 이렇게 특정 시점에 필요한 것들을 구분해서 사용하기 위해 기간을 정해주는 거에요.

CLASS의 경우 class 파일에 남아는 있지만 JVM이 이를 인식해서 실행시켜줄 수 없어요. 그렇다고 class 파일 만들기 전 컴파일 시 사용되는 용도의 애너테이션을 굳이 class 파일에 까지 남길 필요가 없어요. 그래서 해당 기간은 기본 값이긴 하나 잘 쓰이진 않아요(바이트 파일에 굳이 남겨야 한다면 언젠간 쓰이겠죠? ㅎㅎ).

@Documented

애너테이션의 정보가 javadoc으로 작성한 문서에 포함되도록 해요.

@Documented
public @interface DocInterface {}

@Inherited

애너테이션이 자식 클래스에 상속되도록 해요. 부모 클래스에 애너테이션을 붙였다고 자식 클래스도 자동으로 애너테이션을 상속하는 건 아니에요. 애너테이션에 @Inherited를 선언해줘야 자식 클래스도 이를 상속해요.

@Inherited
@interface Anno {}

@Anno
class A {}

// @Inherited가 붙은 애너테이션을 사용했으므로
// 자식 클래스는 따로 선언할 필요가 없어요.
// @Anno
class B extends A {}

@Repeatable

보통 하나의 대상에 하나의 애너테이션만 붙이는데, @Repeatable이 붙은 애너테이션은 여러 번 붙일 수 있어요.

사용법이 조금 복잡해요.

  1. 애너테이션 타입형 배열 value()를 선언한 애너테이션을 선언해요
  2. 애너테이션 타입에 @Repeatable({위에 선언한 애너테이션 타입}.class)를 선언해요

사용법은 이렇지만 이해하기가 어렵죠... 아래 예시를 보시면 이해가 되실 거에요.

import java.lang.annotation.Repeatable;

@interface RepeatableAnnos {
    RepeatableAnno[] value();
}

// 매개변수로 들어가는 class는 반드시 적용 애너테이션 타입 배열의 value()가 선언되어야 해요.
@Repeatable(RepeatableAnnos.class)
@interface RepeatableAnno {
    // 해당 부분은 필수가 아니에요.
    // 하지만 애너테이션에 데이터 초기화를 위해선 아래와 같이 '타입 value()' 형식으로
    // 선언해줘야 해요.
    String value();   
}
public class Main {

	// 같은 애너테이션을 여러번 선언할 수 있어요.
    @RepeatableAnno("asdfasdf")
    @RepeatableAnno("???")
    public static void main(String[] args) {
    }
}

@Native

JVM이 설치된 운영체제의 메서드를 말해요. 일반적으로 C언어로 작성되어 있는데, 자바에서는 메서드의 선언부만 정의하고 구현은 따로 하지 않아요. 이 부분은 정확하게 아는 데로 정리할게요...

애너테이션 타입 지정

애너테이션도 마찬가지로 정의가 가능해요. 내부 요소를 선언하는 방법은 아래와 같아요. 애너테이션도 인터페이스처럼 상수는 정의되지만 디폴트 메서드는 정의할 수 없어요.

@interface MyAnno {
	int count();					// 변수명 뒤에 ()는 필수에요
    String test() default "hello";	// 기본 값도 지정 가능해요
    String[] testTools();
}

public class Main {
	// 이렇게 초기화가 가능해요.
    // 여기서 test의 경우 자동으로 hello가 초기화되어요.
    @MyAnno(count = 1,  testTools = {"JUnit", "Jupiter"})
    public static void main(String[] args) {
    }
}

이렇게 애너테이션의 요소를 선언하는 규칙이 있어요. 이를 나열하자면,

  1. 요소의 타입은 기본형, String, enum, 애너테이션, Class만 허용
  2. () 안에 매개변수 선언 불가
  3. 예외 선언 불가
  4. 요소를 타입 매개변수로 정의할 수 없어요
@interface AnnoTest {
	int id = 100;						// 가능해요! 근데 이건 상수 선언이에요.
    									// final static int id = 100;

    // String major(int i, int j);		// 안되요! 매개변수 선언 못해요
    // String minor() throws Exception;	// 안되요! 예외 선언 못해요
    // ArrayList<T> list();				// 안되요! 요소 타입에 타입 매개변수 사용 못해요
 }

요소를 사용하는 예제는 아래에 드릴테니 한번 돌려보시면서 이해하는 것이 빠를거에요.

package ch12;

import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Deprecated
@SuppressWarnings("1111")   // 유효하지 않은 애너테이션 무시
@TestInfo(testedBy = "Vic", testDate=@DateTime(yymmdd = "220503", hhmmss = "235959"))
public class AnnotationEx5 {
    public static void main(String[] args) {
        // 객체를 얻어와요
        Class<AnnotationEx5> clazz = AnnotationEx5.class;
        
        TestInfo anno = (TestInfo)clazz.getAnnotation(TestInfo.class);
        System.out.println("anno.testedBy() : " + anno.testedBy());
        System.out.println("anno.testDate().yymmdd() : " + anno.testDate().yymmdd());
        System.out.println("anno.testDate().yymmdd() : " + anno.testDate().hhmmss());

        for(String str : anno.testTools()){
            System.out.println("testTools : " + str);
        }
        System.out.println();

        // 적용된 모든 애너테이션을 가져와요
        Annotation[] annoArr = clazz.getAnnotations();
        for(Annotation a : annoArr){
            System.out.println(a);
        }
    }
}

@Retention(RetentionPolicy.RUNTIME) // 실행 시 사용가능하도록 지정
@interface TestInfo {
    int count() default  1;
    String testedBy();
    String[] testTools() default "JUnit";
    TestType testType() default TestType.FIRST;
    DateTime testDate(); 
}

enum TestType { FIRST, FINAL }

@Retention(RetentionPolicy.RUNTIME)
@interface DateTime {
    String yymmdd();
    String hhmmss();
}

profile
#행복 #도전 #지속성

0개의 댓글