목차
- 10분 테코톡 초롱의 애노테이션
- 남궁성 자바의 정석 애노테이션
10분 테코톡 초롱의 애노테이션
우리가 흔히 사용하는 @Override, @Test, @Controller 등등..
도대체 어떻게 동작하는 걸까?
@Controller
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
/**
* Alias for {@link Component#value}.
*/
@AliasFor(annotation = Component.class)
String value() default "";
}
@Override
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
Annotation 이란?
- 소스 코드에 추가적인 정보를 제공하는 메타 데이터의 기능을 수행한다.
- 비즈니스 로직에 직접적으로 영향을 주지 않는다.
- 클래스 또는 인터페이스 등을 컴파일하거나 실행할 때 어떻게 처리해야 할 것인지를 알려주는 설정 정보
- 클래스, 메소드, 변수, 인자 등에 추가할 수 있다.
Annotation 용도
- 컴파일 시 사용하는 정보 전달 (ex @Override)
- 빌드 툴이 코드를 자동으로 생성할 때 정보 전달 (ex @Data)
- 런타임 시 특정 기능을 처리할 때 사용하는 정보 전달 (ex @Autowired)
Annotation은 정보를 전달할 뿐 그 자체로는 어떤 동작도 하지 않는다.
그럼 누구한테 정보 전달을 하는걸까?
ex) Spring 스프링은 이런 애노테이션 정보를 받고 처리하는 로직을 갖고 있는 프레임워크이다.
Annotation 종류
Built-in Annotation: 자바에서 기본적으로 제공하는 Annotation
ex) @Override, @Deprecated, @SuppressWarnings, @SafeVarargs, @FunctionalInterface, @Native
Meta 애노테이션: 다른 Annotation에 적용하기 위한 Annotation
ex) @Target, @Retention, @Documented, @Inherited, @Repeatable
@Target: 해당 Annotation이 사용될 수 있는 위치를 선정
대상 타입 | 의미 |
ANNOTATION_TYPE | Annotation |
CONSTRUCTOR | 생성자 |
FIELD | 필드 (멤버변수, ENUM 상수) |
LOCAL_VARIABLE | 지역변수 |
METHOD | 메소드 |
PACKAGE | 패키지 |
PARAMETER | 매개변수 |
TYPE | 타입(클래스, 인터페이스, ENUM) |
TYPE_PARAMETER | 타입 매개변수(JDK 1.8) |
USE | 타입이 사용되는 모든 곳(JDK 1.8) |
@Retention: 해당 Annotation의 정보를 어느 범위까지 유지할 것인지 설정
대상 타입 | 의미 |
SOURCE | 컴파일 전까지만 유효하며 컴파일 이후 사라짐 |
CLASS | 컴파일러가 클래스를 참조할 때까지 유효 (default) |
RUNTIME | 리플렉션을 사용하여 컴파일 이후에도 JVM에 의해 계속 참조 |
@Documented: 해당 Annotation에 대한 정보를 javadoc에 포함시킴
@Inherited: 해당 Annotation의 상속을 가능하게 함
@Repeatable: 해당 Annotation을 여러 번 선언할 수 있게 함
Custom Annotation 만들기
/**
* 어노테이션 타입 선언은 특별한 종류의 인터페이스로 친다.
*/
public @interface AnnotationName {
/**
* 애노테이션은 속성을 가질 수 있다.
* 속성은 타입과 이름으로 구성, 이름 뒤에 괄호를 붙인다.
* default 가 붙으면 생략이 가능하지만 default가 없는 속성은 필수로 넣어줘야 한다.
*/
String name();
int count() default 1;
}
사용 방법
@AnnotationName(name = "이름", count = 3)
@AnnotationName(name = "이름")
name: 기본값이 없으므로 반드시 값을 기술해야 한다.
count: 기본값이 있기 때문에 생략 가능하다.
@Target(ElementType.METHOD)
public @interface AnnotationName {
String value();
int count() default 1;
}
// 사용
@AnnotationName("이름")
@AnnotationName(value = "이름", count = 3)
value 속성을 가진 애노테이션을 코드에서 사용할 때 값만 기술할 수 있다.
다른 속성의 값과 동시에 주고 싶으면 속성 이름을 반드시 언급해야 한다.
ex) @GetMapping
애노테이션 특징
@interface는 자동으로 Annotation 클래스를 상속한다.
따라서 애노테이션 인터페이스는 extends 절을 가질 수 없다.
내부의 메소드들은 abstract 키워드가 자동으로 붙는다.
애노테이션 제약
- 애노테이션 타입 선언은 제네릭일 수 없다.
- 메소드는 매개변수를 가질 수 없다.
- 메소드는 타입 매개변수를 가질 수 없다.
- 메소드 선언은 throws 절을 가질 수 없다.
정리
애노테이션은 (메타 데이터)정보만 제공한다. like 주석, 라벨
프레임워크, 라이브러리 에서 리플렉션 기술을 사용하여 해당 애노테이션을 가진 클래스나 메소드의 정보를 가져오고
특정 동작을 수행하거나 코드의 동작 방식을 동적으로 변경한다.
리플렉션을 사용하면 런타임 시에 클래스, 메소드, 필드 등의 메타데이터를 읽고, 프로그램의 구조를 동적으로 분석하거나 수정할 수 있다. 이를 통해 애노테이션이 설정된 부분에 대해 적절한 동작을 수행할 수 있게 되는 것이다.
예를 들어, Spring 프레임워크에서는 @Autowired 같은 애노테이션을 사용하여 의존성 주입을 처리한다.
이 애노테이션은 필드나 메소드 위에 설정되어 있지만, 그 자체로 동작하지는 않는다. 대신에, Spring 컨테이너가 리플렉션을 사용하여 해당 애노테이션이 달린 필드나 메소드를 찾아서, 의존성을 주입하는 동작을 수행한다.
남궁성 자바의 정석 애노테이션
애노테이션
주석처럼 프로그래밍 언어에 영향을 미치지 않으면서, 유용한 정보를 제공
애노테이션의 등장 배경
이전에는 소스 코드와 문서(javadoc)가 분리 / 소스 코드와 설정 파일(xml) 분리
이렇게 분리가 되어있어서 불편한 점이 많았다. ex) 소스 코드와 문서의 버전 불일치 문제
하지만 애노테이션의 등장으로 소스 코드와 설정 파일, 문서 간의 분리로 인한 불편함을 해결하고, 개발 프로세스를 단순화
표준 애노테이션
Java에서 제공하는 애노테이션
애노테이션 | 설명 |
@Override | 컴파일러에게 오버라이딩하는 메소드라는 것을 알린다. |
@Deprecated | 앞으로 사용하지 않을 것을 권장하는 대상에 붙인다. |
@SuppressWarnings | 컴파일러에게 특정 경고메시지가 나타나지 않게 해준다. |
@SafeVarargs | 제네릭 타입의 가변인자에 사용 (JDK1.7) |
@FunctionalInterface | 함수형 인터페이라는 것을 알린다.(JDK1.8) |
@Native - 메타 애노테이션 | native 메소드에서 참조되는 상수 앞에 붙인다. (JDK1.8) |
@Target - 메타 애노테이션 | 애노테이션이 적용 가능한 대상을 지정하는데 사용한다 |
@Documented - 메타 애노테이션 | 애노테이션 정보가 javadoc으로 작성된 문자에 포함되게 한다. |
@Inherited - 메타 애노테이션 | 애노테이션이 자손 클래스에 상속되도록 한다. |
@Retention - 메타 애노테이션 | 애노테이션이 유지되는 범위를 지정하는데 사용한다. |
@Repeatable - 메타 애노테이션 | 애노테이션이 반복해서 적용할 수 있게 한다. (JDK1.8) |
@Override
오버라이딩을 올바르게 했는지 컴파일러가 체크
오버라이딩할 때 메소드 이름을 잘못 적는 실수를 하는 경우가 많다.
class Parent{
void method() {}
}
class Child extends Parent{
void Method() {}
}
method 메소드를 오버라이딩하려 했으나 이름을 잘못 적음
오버라이딩할 때는 메소드 선언부 앞에 @Override를 붙이자.
만약 그대로 컴파일 하면
class Parent{
void method() {}
}
class Child extends Parent{
@Override
void Method() {}
}
컴파일 결과
error: method does not override or implement a method from a supertype
@Override
^
@Override는 마커 애노테이션이다. (Marker Annotation)
마커 애노테이션이란? 요소가 하나도 정의되지 않은 애노테이션이다.
@Deprecated
앞으로 사용하지 않을 것을 권장하는 필드나 메소드에 붙인다.
ex) Date 클래스의 getDate()
@Deprecated
public int getDate() {
return normalize().getDayOfMonth();
}
Q 그럼 왜 없애버리는게 아니라 @Deprecated를 붙일까?
A
많은 시스템이나 프로젝트에서 이미 getDate() 같은 메소드를 사용하고 있을 수 있다.
만약 그 메소드를 갑자기 삭제하면, 이 메소드를 사용하던 모든 코드에서 컴파일 에러가 발생하게 된다.
이를 방지하기 위해 @Deprecated를 통해 개발자에게 해당 메소드나 클래스가 곧 사라질 수 있으니 새로운 대안으로 전환하라는 경고를 주면서도, 기존 코드가 계속 동작할 수 있도록한다.
@FunctionalInterface
함수형 인터페이스에 붙이면, 컴파일러가 올바르게 작성했는지 체크
함수형 인터페이스에는 하나의 추상메소드만 가져야 한다는 제약이 있다.
컴파일 결과
error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
Test is not a functional interface
multiple non-overriding abstract methods found in interface Test
@SuppressWarnings
컴파일러의 경고메시지가 나타나지 않게 억제한다.
괄호()안에 억제하고자하는 경고의 종류를 문자열로 지정
@SuppressWarnings 사용 X
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(10);
}
실행 결과
Note: C:\Study\Java8\src\main\java\com\example\java8\App.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
ArrayList를 생성하면서 제네릭 타입을 지정하지 않았다.
그래서 컴파일을 하면 unchecked 경고가 발생한다.
@SuppressWarnings 사용 O
public class App {
public static void main(String[] args) {
@SuppressWarnings("unchecked")
ArrayList list = new ArrayList();
list.add(10);
}
}
unchecked 경고가 사라졌다.
이렇게 @SuppressWarnings를 사용하여 컴파일 경고를 억제할 수 있다.
→ 이미 개발자가 인지하고 있다는 뜻, 개발자가 직접 타입 안정성을 관리하겠다는 뜻
둘 이상의 경고를 억제하려면?
@SuppressWarnings({"unchecked", "deprecation", "varargs"})
-Xlint 옵션으로 컴파일하면, 경고메시지를 확인할 수 있다.
$ javac App.java
Note: App.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
// Xlint 옵션으로 다시 컴파일
$ javac -Xlint App.java
App.java:6: warning: [rawtypes] found raw type: ArrayList
ArrayList list = new ArrayList();
^
missing type arguments for generic class ArrayList<E>
^
missing type arguments for generic class ArrayList<E>
where E is a type-variable:
E extends Object declared in class ArrayList
App.java:7: warning: [unchecked] unchecked call to add(E) as a member of the raw type ArrayList
list.add(10);
^
where E is a type-variable:
E extends Object declared in class ArrayList
3 warnings
제네릭 타입을 사용하지 않은 ArrayList에 대해 발생한 컴파일 경고를 보다 명확하게 보여주고 있다.
-Xlint 옵션을 사용하면, 경고 메시지를 좀 더 구체적으로 확인할 수 있는데, 이 경우에는 "unchecked" 와 "rawtypes" 두 가지 유형의 경고가 발생했다.
[rawtypes] 경고
ArrayList list = new ArrayList(); 처럼 제네릭 타입을 지정하지 않은 경우 "raw type"이 사용되었다는 경고이다.
ArrayList는 제네릭 클래스이기 때문에 <E>라는 타입 매개변수를 받아야 하는데, 이를 지정하지 않으면 ArrayList는 기본적으로 Object 타입으로 동작하게 된다. 이로 인해 타입 안전성이 보장되지 않기 때문에 경고가 발생한다.
[unchecked] 경고
list.add(10);는 unchecked call이 발생한 상황이다. 제네릭을 사용하지 않으면 ArrayList가 어떤 타입을 저장하는지 명확하지 않으므로 컴파일러는 타입을 알 수 없어 경고를 발생시킨다. 여기서 add(10)이 안전한 호출인지 확신할 수 없다는 의미
메타 애노테이션
메타 애노테이션은 애노테이션을 위한 애노테이션
메타 애노테이션은 java.lang.annotation 패키지에 포함
@Target
애노테이션을 정의할 때, 적용대상 지정에 사용
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
대상 타입 | 의미 |
ANNOTATION_TYPE | Annotation |
CONSTRUCTOR | 생성자 |
FIELD | 필드 (멤버변수, ENUM 상수) |
LOCAL_VARIABLE | 지역변수 |
METHOD | 메소드 |
PACKAGE | 패키지 |
PARAMETER | 매개변수 |
TYPE | 타입(클래스, 인터페이스, ENUM) |
TYPE_PARAMETER | 타입 매개변수(JDK 1.8) |
USE | 타입이 사용되는 모든 곳(JDK 1.8) |
@Retention
애노테이션이 유지(retention)되는 기간을 지정하는데 사용
대상 타입 | 의미 |
SOURCE | 컴파일 전까지만 유효하며 컴파일 이후 사라짐 |
CLASS | 컴파일러가 클래스를 참조할 때까지 유효 (default) |
RUNTIME | 리플렉션을 사용하여 컴파일 이후에도 JVM에 의해 계속 참조 |
SOURCE
컴파일러에 의해 사용되는 애노테이션의 유지 정책은 SOURCE
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
오버라이딩 체크는 컴파일러에 의해 사용된다. → 유지 정책: SOURCE
컴파일 이후에 사라진다.
RUNTIME
실행시에 사용 가능한 애노테이션 정책은 RUNTIME
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
컴파일 시점 ~ 런타임 시점까지 사용
@Documented, @Inherited
javadoc으로 작성한 문서에 포함시키려면 @Documented를 붙인다.
애노테이션을 자손 클래스에 상속하고자 할 때, @Inherited를 붙인다.
@Inherited
@interface SuperAnno {}
@SuperAnno
class Parent {}
class Child extends Parent {} // Child에 애노테이션이 붙은 것으로 인식
@Repeatable
반복해서 붙일 수 있는 애노테이션을 정의할 때 사용
@interface ToDos {
ToDo[] value(); // ToDo 애노테이션 배열
}
@Repeatable(ToDos.class)
@interface ToDo{
String value();
}
@ToDo("delete test codes.")
@ToDo("override inherited methods.")
class MyClass{
}
@Repeatable인 @ToDo를 하나로 묶을 컨테이너 애노테이션을 정의해야 동작한다.
여기선 ToDos 애노테이션이 컨테이너 애노테이션 역할을 한다.
커스텀 애노테이션 타입 정의하기, 사용
@interface DateTime{
String yymmdd(); // 날짜
String hhmmss(); // 시간
}
enum TestType{
FIRST, SECOND
}
@interface TestInfo{
int count();
String testedBy();
String[] testTools();
TestType testType();
DateTime testDate();
}
@TestInfo(
count = 3,
testedBy = "bae",
testTools = {"Junit5", "AutoTester"},
testType = TestType.FIRST,
testDate = @DateTime(yymmdd = "240101", hhmmss = "235959")
)
public class App {
public static void main(String[] args) {
// 생략 ....
}
}
애노테이션의 요소들
적용시 값을 지정하지 않으면, 사용될 수 있는 기본값 지정 가능(NULL 제외)
@interface TestInfo{
int count() default 1; // 기본값 1
}
@TestInfo
public class NewClass{
}
요소가 하나이고 이름이 value일 때는 요소의 이름 생략 가능
@interface TestInfo{
String value();
}
@TestInfo("passed")
class NewClass {}
요소의 타입이 배열인 경우, 괄호 {}를 사용해야 한다.
@interface TestInfo{
String[] testTools();
}
@Test(testTools={"Junit", "AutoTester"})
@Test(testTools="Junit") // 값이 하나면 괄호 생략 가능
@Test(testTools={}) // 값이 없을 때는 괄호 {} 반드시 필요
모든 애노테이션의 조상
Annotation은 모든 애노테이션의 조상이지만 상속 불가
모든 애노테이션이 암묵적으로 이 인터페이스를 구현하게 된다.
java.lang.annotation.Annotation
public interface Annotation { // 인터페이스
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType(); // 애노테이션 타입 반환
}
boolean equals(Object obj)
두 애노테이션 인스턴스가 같은지 비교하는 메소드
int hashCode()
애노테이션의 해시 코드를 반환. 이는 자바 객체의 고유한 식별 값을 제공하기 위해 사용
String toString()
애노테이션의 문자열 표현을 반환
Class<? extends Annotation> annotationType()
애노테이션의 타입을 반환.
이 메소드는 리플렉션을 사용할 때 애노테이션의 타입을 동적으로 확인할 수 있게 해준다.
컴파일러가 해당 추상 메소드들을 구현한다. 그래서 구현하지 않고 사용이 가능하다.
공부 내용 전부 적용
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface DateTime{
String yymmdd(); // 날짜
String hhmmss(); // 시간
}
enum TestType{
FIRST, SECOND
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface TestInfo{
int count();
String testedBy();
String[] testTools();
TestType testType();
DateTime testDate();
}
@TestInfo(
count = 3,
testedBy = "bae",
testTools = {"Junit5", "AutoTester"},
testType = TestType.FIRST,
testDate = @DateTime(yymmdd = "240101", hhmmss = "235959")
)
public class App {
public static void main(String[] args) {
App app1 = new App();
TestInfo anno1 = app1.getClass().getAnnotation(TestInfo.class);
Class<App> cls = App.class; // 메타 데이터 가져오기
TestInfo anno2 = cls.getAnnotation(TestInfo.class);
int count = anno1.count();
System.out.println("count = " + count);
DateTime dateTime = anno1.testDate();
System.out.println("dateTime = " + dateTime);
for(String tool : anno1.testTools()){
System.out.println("tool = " + tool);
}
}
}
// 출력
count = 3
dateTime = @com.example.java8.DateTime(yymmdd="240101", hhmmss="235959")
tool = Junit5
tool = AutoTester
참고 자료
'Computer Sience > Java' 카테고리의 다른 글
[JAVA] 자바, JVM, JDK, JRE (0) | 2024.10.08 |
---|---|
[JAVA8] 애노테이션의 변화 (0) | 2024.10.07 |
[JAVA8] CompletableFuture (0) | 2024.10.04 |
[JAVA8] Date & Time (2) | 2024.10.03 |
[JAVA8] Optional (0) | 2024.10.03 |