강의 : 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션
10. Java 애노테이션
ReflectionServlet은 요청 URL과 메서드 이름이 같다면 해당 메서드를 동적으로 호출할 수 있다.
하지만 URL과 메서드 이름이 다르면 호출할 수 없다. 이 문제는 애노테이션을 활용해 해결할 수 있다.
Controller에 추가 정보를 주석으로 적어두고 사용할 수 있다면 URL과 비교해 메서드를 호출하는데 사용할 수 있다.
주석은 컴파일 시점에 모두 제거된다. 프로그램 실행 중에 읽어서 사용할 수 있는 주석이 애노테이션이다.
10-1. 애노테이션 예제
애노테이션은 @interface 키워드를 사용해 만든다.
애노테이션은 기본 타입(int, float, boolean 등), String, Class(메타데이터) 또는 인터페이스, 앞선 타입들의 배열 외에는 정의할 수 없다. 특히 일반적인 클래스를 사용할 수 없다.
그리고 예외를 사용할 수 없으며 void를 반환 타입으로 사용할 수 없다.
요소 이름은 메서드 형태로 정의되며, 괄호()를 보함하되 매개변수는 없어야 한다.
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnoElement {
String value();
int count() default 0;
String[] tags() default {};
// MyLogger data(); // 다른 타입은 적용 X
// 클래스 정보는 가능
Class<? extends MyLogger> annoData() default MyLogger.class;
}
@AnnoElement(value = "data", count = 10, tags = {"t1", "t2"})
public class ElementData1 {
}
public class ElementData1Main {
public static void main(String[] args) {
Class<ElementData1> annoClass = ElementData1.class;
AnnoElement annotation = annoClass.getAnnotation(AnnoElement.class);
String value = annotation.value();
System.out.println("value = " + value);
int count = annotation.count();
System.out.println("count = " + count);
String[] tags = annotation.tags();
System.out.println("tags = " + Arrays.toString(tags));
}
}
실행 결과
value = data
count = 10
10-2. 메타 애노테이션
메타 애노테이션은 애노테이션을 정의하는데 사용하는 특별한 애노테이션이다.
@Retention
애노테이션의 생존 기간을 지정한다.
- RetentinPolicy.SOURCE: 소스 코드에만 남아있다. 컴파일 시점에 제거된다.
- RetentionPolicy.CLASS: 컴파일 후 class 파일까지는 남아 있지만 자바 실행 시점에 제거된다.(기본 값)
- RetentionPolicy.RUNTIME: 자바 실행 중에도 남아있다. 대부분 이 설정을 사용한다.
@Target
애노테이션을 적용할 수있는 위치를 지정한다.
주로 TYPE, FIELD, METHOD를 사용한다.
public enum ElementType {
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE,
MODULE,
RECORD_COMPONENT;
}
@Documented
자바 API 문서를 만들 때 해당 애노테이션이 함께 포함되는지 지정한다. 보통 함께 사용한다.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Documented
public @interface AnnoMeta {
}
public class MetaData {
//@AnnoMeta // 필드에 적용 - 컴파일 오류
private String id;
@AnnoMeta // 메서드에 적용
public void call() {}
public static void main(String[] args) throws NoSuchMethodException {
AnnoMeta typeAnno = MetaData.class.getAnnotation(AnnoMeta.class);
System.out.println("typeAnno = " + typeAnno);
AnnoMeta methodAnno = MetaData.class.getMethod("call").getAnnotation(AnnoMeta.class);
System.out.println("methodAnno = " + methodAnno);
}
}
실행 결과
typeAnno = @annotation.basic.AnnoMeta()
methodAnno = @annotation.basic.AnnoMeta()
@Inherited
애노테이션을 정의할 때 @Inherited 메타 애노테이션을 붙이면, 애노테이션을 적용한 클래스의 자식도 해당 애노테이션을 부여 받을 수 있다.
단, 이 기능은 클래스에서만 작동하고, 인터페이스의 구현체에는 적용되지 않는다.
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface InheritedAnnotation {
}
@Retention(RetentionPolicy.RUNTIME)
public @interface NoInheritedAnnotation {
}
@InheritedAnnotation
@NoInheritedAnnotation
public class Parent {
}
public class Child extends Parent {
}
@InheritedAnnotation
@NoInheritedAnnotation
public interface TestInterface {
}
public class TestInterfaceImpl implements TestInterface {
}
public class InheritedMain {
public static void main(String[] args) {
print(Parent.class);
print(Child.class);
print(TestInterface.class);
print(TestInterfaceImpl.class);
}
private static void print(Class<?> clazz) {
System.out.println("class: " + clazz);
for (Annotation annotation : clazz.getAnnotations()) {
System.out.println(" - " + annotation.annotationType());
}
System.out.println();
}
}
실행 결과
class: class annotation.basic.inherited.Parent
- InheritedAnnotation
- NoInheritedAnnotation
class: class annotation.basic.inherited.Child
- InheritedAnnotation
class: interface annotation.basic.inherited.TestInterface
- NoInheritedAnnotation
- InheritedAnnotation
class: class annotation.basic.inherited.TestInterfaceImpl
10-3. 애노테이션 기반 검증기
@NotEmpty - 빈 값을 검증하는데 사용
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmpty {
String message() default "값이 비어있습니다.";
}
@Range - 숫자의 범위를 검증하는데 사용
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Range {
int min();
int max();
String message() default "범위를 넘었습니다."
}
User 클래스에 검증용 애노테이션 추가
public class User {
@NotEmpty
private String name;
@Range(min = 1, max = 100, message = "나이는 1과 100 사이여야 합니다.")
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
Team 클래스에 검증용 애노테이션 추가
public class Team {
@NotEmpty(message = "이름이 비어있습니다.")
private String name;
@Range(min = 1, max = 999, message = "회원 수는 1과 999 사이여야 합니다.")
private int memberCount;
public Team(String name, int memberCount) {
this.name = name;
this.memberCount = memberCount;
}
public String getName() {
return name;
}
public int getMemberCount() {
return memberCount;
}
}
검증기 개발
public class Validator {
public static void validate(Object obj) throws Exception {
Fields[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(NotEmpty.class)) {
String value = (String) field.get(obj);
NotEmpty annotation = field.getAnnotation(NotEmpty.class);
if (value == null || value.isEmpty()) throw new RuntimeException(annotation.message());
}
if (field.isAnnotationPresent(Range.class)) {
long value = field.getLong(obj);
Range annotation = field.getAnnotation(Range.class);
if (value < annotation.min() || value > annotation.max()) throw new RuntimeException(annotation.message());
}
}
}
}
public class ValidatorV2Main {
public static void main(String[] args) {
User user = new User("user1", 0);
Team team = new Team("", 0);
try {
log("== user 검증 ==");
Validator.validate(user);
} catch (Exception e) {
log(e);
}
try {
log("== team 검증 ==");
Validator.validate(team);
} catch (Exception e) {
log(e);
}
}
}
실행 결과
17:16:39.166 [ main] == user 검증 ==
17:16:39.187 [ main] java.lang.RuntimeException: 나이는 1과 100 사이여야 합니다.
17:16:39.187 [ main] == team 검증 ==
17:16:39.187 [ main] java.lang.RuntimeException: 이름이 비어있습니다.
10-4. 정리
스프링이나 JPA는 리플렉션과 애노테이션을 활용한 복잡한 메타프로그래밍으로 의존성 주입, ORM, AOP, 설정의 자동화, 트랜잭션 관리와 같은 기능들을 제공한다.
- 의존성 주입: 스프링은 @autowired 애노테이션만 붙이면 객체의 필드나 생성자에 자동으로 의존성을 주입한다.
- ORM: JPA는 @Entity, @Table, @Column 등의 애노테이션으로 객체-테이블 관계를 설정한다.
- AOP: 스프링은 @Aspect, @Before, @After 등의 애노테이션으로 런타임에 코드를 동적으로 주입해 관점 지향 프로그래밍을 구현한다.
- 설정의 자동화: @Configuration, @Bean 등의 애노테이션을 사용해 다양한 설정을 편리하게 적용한다.
- 트랜잭션 관리: @Transactional 애노테이션 만으로 메서드 레벌의 DB 트랜잭션 처리가 가능해진다.
이러한 기능들은 개발자가 비즈니스 로직에 집중할 수 있게 해준다.
스프링이나 JPA 같은 프레임워크들은 리플렉션과 애노테이션을 극대화해서 사용한다.
따라서 리플렉션과 애노테이션에 대한 이해를 바탕으로 프레임워크의 동작 원리를 더 깊이 파악하고 효과적으로 활용할 수 있다.