Java를 배우고 사용하는 동안 알게 모르게 애너테이션을 많이 사용해 왔습니다.
그럼에도 불구하고 애너테이션이 뭘까? 하는 질문에 명확한 답을 내놓지 못하는 제 모습을 보게 되었죠.
그래도! 지금이라도 애너테이션에 대한 제대로 된 개념을 공부하면서 애너테이션이 무엇인지 한번 파헤쳐 보도록 하겠습니다.
우리가 사용하는 Java를 개발하신 분들은 소스코드에 대한 문서를 따로 만드는 것보다 소스코드와 문서를 하나의 파일로 관리하는 것이 낫다고 생각했습니다.
그래서 소스코드의 주석에 소스코드에 대한 정보를 저장하고, 소스코드의 주석으로부터 HTML문서를 생성해내는 프로그램인 javadoc.exe을 만들어서 사용했습니다.
우리는 자바를 사용하면서 애너테이션을 많이 마주쳐 보았을거라 확신합니다.
예를 들어 몇가지 애너테이션을 한번 나열해 보겠습니다. 특히 Spring Framwork 사용자라면 더욱 익숙하겠죠.
특히 스프링에서 많은 종류의 애너테이션을 사용하면서 당장 애너테이션을 나열하라고 하면 20개 이상을 나열할 수 있을 정도로 이미 우리는 애너테이션과 많이 친숙해졌습니다.
✔️ 애너테이션 한줄 정의
Java에서 기본적으로 제공하는 애너테이션은 몇개 없습니다.
자주 보았던 애너테이션도 있지만 그렇지 못한 애너테이션들도 꽤 있습니다. 표준 애너테이션의 목록과 설명은 아래의 표를 참고하시면 되겠습니다.
애너테이션에 대한 파트를 읽으면서 가장 자주 들었던 생각은 그래서 이익이 뭔데? 였습니다.
Override 할때를 생각해보면, 우리가 toString 메서드를 오버라이딩 할때, @Override를 붙이지 않아도 잘 동작했던 것을 기억하고 계실겁니다.
그렇다면 이런 상황이라면 어떨까요?
class Animal{
void bark() { System.out.println("BARK!"); }
}
class Pig extends Animal {
void Bark() { System.out.println("PIG");}
}
Animal 클래스는 bark 메서드를 가지고 있고, Pig 클래스는 Animal 클래스를 상속받아 bark 메서드와 Bark 메서드를 가지고 있습니다.
이 클래스를 작성한 사람의 의도는 bark 메서드를 Pig 클래스에서 오버라이딩 하는 것입니다.
안타깝게도 Pig 클래스의 객체를 생성하고 bark 메서드를 실행해서 원하는 "PIG" 메시지가 콘솔창에 보이지 않는 순간까지 클래스를 작성한 사람은 오버라이딩이 잘못 되었다는 것을 알 수 없습니다.
그래서 사용하는 애너테이션이 @Override 애너테이션 입니다.
class Animal{
void bark() { System.out.println("BARK!"); }
}
class Pig extends Animal {
@Override
void Bark() { System.out.println("PIG");}
}
위의 코드와 같이 @Override 애너테이션만 붙여주면 컴파일 타임에 개발자는 컴파일러부터 오버라이딩이 잘못되었다는 에러 메시지를 받고 메서드를 수정할 것입니다.
이 애너테이션을 붙이는 것 만으로도 알아내기 어려운 실수를 미연에 방지해주기 때문에 꼭 붙이는 걸 추천합니다.
또 @Override 애너테이션과 비슷한 목적으로 사용하는 애너테이션이 하나 더 있습니다.
바로 @FunctionalInterface 애너테이션 입니다. 함수형 인터페이스는 추상 메서드를 하나만 가지고 있어야 한다는 제약을 가지고 있습니다.
만약 추상 메서드를 두 개 이상 가지고 있다면 함수형 인터페이스의 역할을 할 수 없습니다.
// 함수형 인터페이스가 아님 -> 함수형 인터페이스로 작성하려 했으나 에러가 나오지 않음
public interface Runnable {
public abstract void run();
public abstract void walk();
}
// @FunctionalInterface 애너테이션이 추상 메서드가 하나 뿐인지 컴파잍 타임에 검사를 함
@FunctionalInterface
public interface Runnable { // 에러 -> 함수형 인터페이스로 선언했으나 추상 메서드가 두 개 이상임
public abstract void run();
public abstract void walk();
}
@FunctionalInterface또한 애너테이션을 붙임으로서 컴파일 타임에 실수를 방지할 수 있습니다.
오버라이딩과 함수형 인터페이스를 작성할때는 꼭 애너테이션을 붙여주는 것이 좋겠습니다.
@Deprecated
Java는 하위 버전과의 호환성을 굉장히 중요한 가치로 여기는 프로그래밍 언어 입니다.
일례로 지네릭스 도입 후 어떤 언어는 지네릭스를 사용하기 이전 버전의 지원을 끊었지만, Java는 지네릭스를 사용할 것을 권고하면서 하위 버전과의 호환성 유지를 위해 사용은 할 수 있게 하는 것이 그 증거 라고 할 수 있겠습니다.
새로운 버젼의 JDK가 세상에 나오면, 새로운 기능이 추가되고 기존의 부족했던 기능들이 개선되서 나오기도 합니다. 이 과정에서 기존의 기능보다 더 낫거나 대체가 가능한 기능이 추가 되어도, 여러 곳에서 이미 사용되고 있을 기능을 함부로 삭제하는 것은 Java의 가치와 맞지 않다고 볼 수 있습니다.
따라서, 더 이상 사용되지 않는 필드나 메서드에 @Deprecated를 붙여, 개발자에게 이 애너테이션이 붙은 대상은 다른 것으로 대체되었으니 더 이상 사용하지 말것을 권한다는 의미입니다.
강제성은 없지만, @Deprecated가 붙은 것들을 사용하게 되면 컴파일시 경고 메시지가 송출됩니다.
@SuppressWarnings
컴파일러가 보여주는 경고메세지가 나타나지 않도록 억제해주는 애너테이션 입니다.
@Deprecated가 붙은 것들을 사용하면 컴파일러의 경고가 발생하게 됩니다. Deprecated 된 것들을 사용해서 나오는 경고들을 알면서도 묵인해야 하는 경고에 속합니다.
그렇다고 해서 '-Xlint' 옵션을 붙이지 않으면 컴파일러가 자세한 경고의 내용을 보여주지 않으므로 다른 경고를 놓칠 염려가 있습니다.
따라서 @SuppressWarings 애너테이션을 사용하여 묵인해야 하는 경고 메세지를 지정해, 컴파일 후에 어떤 경고 메시지도 나타나지 않게 해주는 역할을 합니다.
@SuppressWarnings 에서 현재 지정할 수 있는 경고 메시지의 종류는 4가지가 있습니다.
경고 메시지 | 설명 |
---|---|
deprecation | @Deprecated가 붙은 대상을 사용해서 나오는 경고 메시지 |
unchecked | 지네릭스로 타입을 지정하지 않았을 때 발생하는 경고 메시지 |
rawtypes | 지네릭스를 사용하지 않아서 발생하는 경고 메시지 |
varargs | 가변인자의 타입이 지네릭 타입일 때 발생하는 경고 메시지 |
@SafeVarags
메서드에 선언된 가변인자의 타입이 non-reifiable 타입일 경우, 해당 메서드를 선언하는 부분과 호출하는 부분에서 "unchecked" 경고가 발생합니다.
해당 코드에 문제가 없다면 @SafeVarags를 사용해야 합니다.
이 애너테이션은 static이나 final이 붙은 메서드와 생성자에만 붙일 수 있습니다.
🎸 즉 오버라이딩 할 수 있는 메서드에는 사용할 수 없습니다.
이번 애너테이션 파트를 보면서 처음으로 보게 된 단어라 생소해서 따로 정리해 보았습니다
@SafeVarags를 붙이면 호출하는 곳에서 발생하는 경고도 억제가 됩니다.
반면에 @SuppressWarnings("unchecked")로 @SafeVarags를 대체하게 되면, 메서드 선언뿐만 아니라 메서드가 호출되는 곳에도 애너테이션을 붙여줘야 하는 수고로움을 감수해야 합니다.
따라서 @SafeVarags를 사용하는것이 더 좋습니다.
여기서 가장 중요한 점은, @SafeVarags로 'unchecked' 경고는 억제가 가능하지만 'varargs' 경고는 억제 할 수 없으므로 @SafeVarags를 사용할때 @SuppressWarnings("varargs")를 함께 사용해야 합니다.
예외처리에서도 공부했듯이 Java에서 제공하는 예외 클래스들이 있는 반면에, 예외 클래스를 상속받아서 비즈니스적으로 의미있는 커스텀 예외를 정의하여 사용자 정의 예외를 사용할 수 있었던 것을 기억하실겁니다.
애너테이션도 애너테이션을 정의할 때 사용하는 메타 애너테이션을 이용해 애너테이션을 직접 정의할 수 있습니다.
@Target
말 그대로 애너테이션이 적용가능한 대상을 지정하는데 사용됩니다.
@Target으로 지정할 수 있는 애너테이션 적용대상의 종류는 아래의 표에 정리했습니다.
@Retention
애너테이션이 유지(retention)되는 기간을 지정할 때 사용하는 애너테이션입니다.
@Retention으로 지정할 수 있는 유지 정책은 아래의 표와 같습니다.
🦅 지역 변수에 붙은 애너테이션은 컴파일러만 인식할 수 있습니다. 따라서 유지정책이 RUNTIME인 애너테이션을 지역변수에 붙이면 실행시 인식되지 않습니다.
@Documented
애너테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 합니다.
@Inherited
애너테이션이 자손 클래스에 상속되도록 합니다. @Inherited가 붙은 애너테이션을 조상 클래스에 붙이면, 자손 클래스도 이 애너테이션이 붙은 것과 같이 인식 됩니다.
그 외 기타 애너테이션
애너테이션은 아래와 같이 정의해야 합니다.
다섯 개의 요소를 가진 @TestInfo 애너테이션을 정의해 보겠습니다.
@Retention(RetentionPolicy.RUNTIME) // 실행 시에 사용가능하도록 지정
@interface TestInfo {
int count() default 1;
String testedBy();
String[] testTools() default "Junit";
TestType testType() default TestType.FIRST; // Enum Type 포함 가능
DateTime testDate(); // 자신이 아닌 다른 애너테이션(@DateTime)을 포함 가능
}
@Retention(RetentionPolicy.RUNTIME) // 실행 시에 사용가능하도록 지정
@interface DateTime {
String yymmdd();
String hhmmss();
}
enum TestType { FIRST, FINAL }
애너테이션을 적용할때는 요소들의 값을 빠짐없이 지정해주어야 하며, 순서는 상관이 없습니다.
위의 코드와 같이 default 값을 지정해 놓은 경우에는 생략이 가능합니다.
🧸 애너테이션의 요소가 오직 하나뿐이고 이름이 value인 경우, 요소의 이름을 생략하고 값만 적어도 됩니다.
@interface TestInfo {
String value();
}
@TestInfo("passed")
class NewClass { ... }
참고로 모든 애너테이션의 조상인 Annotation은 일반 인터페이스로 구현되어 있고, equals & hashCode & toString 메서드가 오버라이딩 되어 있기 때문에 직접 만든 애너테이션에서도 호출이 가능합니다.
애너테이션의 요소를 선언할 때 반드시 지켜야 하는 규칙은 아래와 같습니다.
요즘 들어 기본기가 부족하다는 것을 많이 느낄때가 있습니다.
프로젝트 소스를 분석하면서 본 애너테이션들도 꽤 있는데 그것이 어떻게 정의되어 있고, 사용법은 어떻게 되는지도 알지 못했습니다.
그래도 기쁜 점은 예전에는 뭐가 부족하고 뭐를 더 해야하는지 모르는 상태였다면 이제는 무엇을 공부하고 어떤 것을 채워가야 하는지 정확히 알고 있다는 점에서 공부 방향성을 잡는데 큰 도움이 될 거 같다는 예감이 듭니다.
오늘 하루도 공부한 나 자신 ~ 칭찬해!