자바의 어노테이션에 대해 학습하세요.
- 어노테이션 정의하는 방법
- @retention
- @target
- @documented
- 어노테이션 프로세서
어노테이션 정의하는 방법
어노테이션 (Annotation)
- @Override와 같이 @ 기호를 사용하는 문법 요소
- java 5부터 등장
어노테이션의 장점
- 기존 자바는 개발을 하며 각 계층별 설정 데이터들을 XML로 명시
- 서비스의 규모가 커질수록 설정량이 많아지고 도메인 처리의 데이터들이 분산되어 수정이 힘들었다.
- 어노테이션이 등장하며 데이터 유효성 검사 등 직접 클래스에 명시해 줄 수 있게 되어
수정이 필요할 때 쉽게 파악할 수 있으며 어노테이션의 재사용도 가능하게 되었다. - AOP 를 쉽게 구성할 수 있게 해준다.
어노테이션의 용도
- 문서화
- 컴파일러 체크
- 코드 분석과 자동 생성
- 런타임 프로세싱
- 유효성 검사와 같은 메타데이터 로써 사용되고 reflection을 이용하여 특정 클래스를 주입할 수도 있다.
어노테이션의 분류
- Maker 어노테이션
멤버 변수가 없고 컴파일러에게 의미를 전달하기 위한 표식으로 사용 ex. @Override - Single-Value 어노테이션
멤버로 단일변수를 갖고 데이터를 전달할 수 있는 어노테이션 - Full 어노테이션
둘 이상의 변수를 갖는 어노테이션으로 데이터를 키 = 값 형태로 전달
빌트인 어노테이션
- 자바에 내장되어 있는 어노테이션으로 컴파일러를 위한 어노테이션
@Override
- 현재 메서드가 슈퍼 클래스의 메서드를 오버라이드한 것임을 명시
@Deprecated
- 마커 어노테이션으로 다음 버전에 지원되지 않을 수 있기 때문에 사용하지 말라고 경고 알림
@SuppressWarning
- 경고를 제거하는 어노테이션으로 개발자가 의도를 가지고 설계했는데
컴파일은 이를 알지 못하고 컴파일 경고를 띄울 수 있으므로 이를 제거하는 목적
@SafeVarargs
- 자바 7 이상에서 사용 가능하고 제네릭 같은 가변인자 매개변수 사용 시 경고 무시
@Functionalinterface
- 자바 8 이상에서 사용 가능하고 컴파일러에게 함수형 인터페이스라는 것을 알리는 어노테이션 이다.
- 함수형 인터페이스
1개의 추상 메서드만들 가지고 있는 인터페이스 ex.Runnable
메타 어노테이션
- 어노테이션에 사용되는 어노테이션으로 어노테이션을 정의하기 위해 사용된다.
@Retention
- 어노테이션이 유지되는 기간(Life time)을 설정
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
- SOURCE
.java 까지 남아있는다.
컴파일러에 의해 버려진다. - CLASS
.class 까지 남아있는다.
Default - RUNTIME
런타임까지 남아있는다 (= 사라지지 않는다)
클래스파일에도 존재하고 런타임에 VM에 의해 유지되어 리플렉션을 통해
클래스 파일의 정보를 읽어 처리 가능하다.
@Target
- 어노테이션이 적용가능한 대상 ( 동작 대상 )을 지정한다.
- 다른 타입이 온다면 컴파일 에러를 호출한다.
- @Target(ElementType.~)와 같이 사용한다.
- 단순하게 적용될 위치가 한개라면 ()를 사용해서 나타내지만 만약 여러개라면 {}를 넣어 이를 나타내게 된다.
public enum ElementType {
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE,
MODULE,
@jdk.internal.PreviewFeature(feature=jdk.internal.PreviewFeature.Feature.RECORDS,essentialAPI=true)
RECORD_COMPONENT;
}
기본
- TYPE
Class, Interface(어노테이션 타입 포함), Enum, jdk 14에 생긴 record - FIELD
필드 값(프로퍼티), Enum 상수 값 - METHOD (메소드)
- PARAMETER (매개변수)
- CONSTRUCTOR (생성자)
- LOCAL_VARIABLE (지역변수)
- ANNOTATION_TYPE (어노테이션)
- PACKAGE (자바 패키지)
jdk 1.8이후 추가
- TYPE_PARAMETER (타입 매개 변수)
- TYPE_USE (타입 사용)
- MODULE (모듈)
jdk 14이후 추가
- RECORD_COMPONENT (recode 컴포넌트)
@documented
- 어노테이션의 정보가 javadoc 문서에 포함되도록 하는 어노테이션
package annotationex;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomAnnotation {
String testDocument();
}
package annotationex;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomAnnotationNoDocument {
String testDocumentNoDocument();
}
package annotationex;
public class CustomAnnotationTest {
public static void main(String[] args) {
}
@CustomAnnotation(testDocument = "document test")
public void testDocument() {}
@CustomAnnotationNoDocument(testDocumentNoDocument = "document test")
public void testNoDocument() {}
}
- JavaDoc Export 시, @Documented를 사용한 어노테이션만 javadoc에 어노테이션 표시
@Inherited
- 자식 클래스에게도 어노테이션이 상속되도록 하는 어노테이션
@Repeatable
- 어노테이션을 반복적으로 선언할 수 있게 하는 어노테이션
커스텀 어노테이션
- @interface 형태로 만들어진다.
- 메타 어노테이션을 붙여 메타 데이터를 표시할 수 있다.
- @interface 안에 매개변수가 없다면 Maker
코드와 같이 한 개만 존재한다면 Single-value
두 개 이상을 갖는다면 Full 어노테이션 이다.
package annotationex;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
String name() default "다슬";
}
package annotationex;
public class AnnotationExClass {
@AnnotationTest
private String defaultName;
@AnnotationTest(name="다슬")
private String customName;
public AnnotationExClass() {
this.defaultName = "다슬";
this.customName = "다슬";
}
public String getDefaultName() {
return defaultName;
}
}
reflect를 활용한 어노테이션 값 접근
- 매개 변수의 값은 "다슬" 이지만 각 필드에 할당된 어노테이션의 필드 값은 다르다.
package annotationex;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
public class AnnotationExClass {
@AnnotationTest
private String defaultName;
@AnnotationTest(name="바보")
private String customName;
public AnnotationExClass() {
this.defaultName = "다슬";
this.customName = "다슬";
}
public String getDefaultName() {
return defaultName;
}
}
class AnnotationExClassTest {
public static void main(String[] args) {
AnnotationExClass annotationExClass = new AnnotationExClass();
Field[] declaredFields = annotationExClass.getClass().getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.print(declaredField.getName());
Annotation[] declaredAnnotations = declaredField.getDeclaredAnnotations();
for (Annotation declaredAnnotation : declaredAnnotations) {
AnnotationTest declaredAnnotation1 = (AnnotationTest) declaredAnnotation;
System.out.println(declaredAnnotation1.name());
}
}
}
}
// defaultName리플렉션
// customName바보
- Retention을 RUNTIME으로 지정하지 않을 경우 리플렉션을 이용한 값을 불러오지 못한다.
어노테이션 프로세서
- 자바 컴파일러 플러그인의 일종
- 이미 작성되어 있는 코드의 특정 지점을 가로채어 동작 방식에 변화를 주는 기술
- 컴파일 도중 새로운 소스코드를 생성하거나 기존 소스코드를 변경할 수 있다.
어노테이션 프로세서 사용 예
- 롬복 ( 기존 코드를 변경 )
- AutoService ( 리소스 파일을 생성 )
- @Override
어노테이션 프로세서의 장점
- 바이트코드에 대한 조작은 런타임에 발생되는 조작이므로 런타임에 대한 비용이 발생한다.
- 애노테이션 프로세서는 어플리케이션을 구동하는 런타임 시점이 아닌,
컴파일 시점에 조작하여 사용하므로써 런타임에 대한 비용이 제로가 된다 - 하지만, 기존의 코드를 고치는 방법으로는 현재로써는 public한 API가 없다.
롬복(Lombok)의 동작원리
Lombok
- @Getter @Setter @Builder 등의 어노테이션과
어노테이션 프로세서를 제공하여 표준적으로 작성해야 할 코드를 개발자 대신 생성해주는 라이브러리이다. - 컴파일 시점에 애노테이션 프로세서를 사용하여
소스코드의 AST (Abstract Syntax Tree)를 조작한다. - 소스코드의 AST는 원래 참조만 할 수 있다. / 그러나 수정이 됨을 확인함
- 참조만 해야하는 것을 내부 클래스를 사용하여 기존의 코드를 조작하는 것으로 해킹이라고 하기도 한다.
논란
- 공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스코드를 조작
- 특히 이클립스의 경우 Java Agent를 사용하여 컴파일러 클래스까지 조작하여 사용한다.
- 해당 클래스들 역시 공개된 API가 아니다보니 버전 호환성에서 문제가 생길 수 있고 언제라도 그런 문제가 발생해도 이상하지 않다.
- 그럼에도 불구하고 편리함 때문에 널리 쓰이고 있으며,
대안이 몇 가지 있지만 롬복의 모든 기능과 편의성을 대체하지는 못하는 상황이다.
ServiceLoader
- 애플리케이션 내부에서 플러그인을 제공할 때 사용
- 특정 기능을 제공하기 위한 인터페이스가 있고,
다양한 벤더 회사들이 이 인터페이스를 기반으로 자신만의 구체 서비스를 구현하게 된다. - 사용자 입장에서는,
어떤 벤더 회사가 어떻게 구현을 하던 공통 인터페이스만 가지고 있으면
각 벤더 회사 서비스의 장단점을 고려해 원하는 구현체만 골라서 사용하면 된다. - 이러한 인터페이스를 SPI (Service Provider Interface) 라고 한다.
ServiceLoader 사용법
- 사용자가 불특정 플러그인을 클래스패스가 읽을 수 있는 장소에 다운로드 했다고 가정하고,
애플리케이션은 해당 클래스패스를 이용해 SPI를 구현하고 있는 모든 구현체를 애플리케이션 내부로 가져와야 함 - 그리고 원하는 조건에 맞는 인스턴스를 골라 사용하면 된다.
- 이 과정을 구현하기 위해 개발자가 리플렉션을 사용해 직접 구현할 수 있지만 자바에서 라이브러리 형태로 서비스 로더를 제공한다.
java.util.ServiceLoader
1. 인터페이스 정의 및 구현
public interface RandomInt{
public int ranmdom();
}
package com.adduci;
public class MyRandomInt implements RandomInt{
@Override
public int random(){
return new Random().nextInt(10);
}
}
2. META-INF/services 디렉터리를 만들고, 텍스트 파일을 넣어 위에 구현한 MyRandomInt 클래스 명시
com.adduci.MyRandomInt
3. 어플리케이션 내에서 로드하여 사용
ServiceLoader<RandomInt> loader = ServiceLoader.load(RandomInt.class);
Iterator<RandomInt> iterator = loader.iteratr();
for(RandomInt service : iterator){
System.out.println(service.random());
}
// Stream 사용
loader.stream().map(Service.Provider::get).findFirst();
'Back-end > java' 카테고리의 다른 글
스터디 14주차 - 제네릭 (0) | 2021.07.08 |
---|---|
스터디 13주차 - I/O (0) | 2021.07.07 |
스터디 11주차 - Enum (0) | 2021.07.05 |
스터디 10주차 - 멀티쓰레드 프로그래밍 (0) | 2021.07.02 |
스터디 9주차 - 예외 처리 (0) | 2021.07.01 |