그동안 자바 개발을 하며
@Override, @Bean 등 다양한 어노테이션을 보았지만, 어떤 원리로 동작하는지 제대로 이해해보는 시간이 없었다.
이번 기회에 동작 원리를 공부하고 커스텀 어노테이션을 어떻게 프로젝트에 적용했는지 작성하고자 한다.
어노테이션이란 코드에 대한 메타데이터(Metadata)로
컴파일러나 런타임 환경이 해석할 수 있는 추가 정보를 제공한다.
즉, 어노테이션은 프로그램의 실행 로직에는 직접 관여하지 않지만,
코드에 의미나 동작을 부여하기 위해 사용된다.
예를 들어,
@Override
public String toString() {
return "User";
}
@Override는 컴파일러에게 "이 메서드는 부모 클래스의 메서드를 재정의한 것이다" 라고 알려주는 표시다. 컴파일 타임에 체크 역할을 한다. (컴파일러가 “정말 재정의했는지” 확인함)반면,
@Bean
public UserService userService() {
return new UserService();
}
@Bean은 스프링이 런타임 시점에 Bean 객체로 등록하라는 명령 신호다. 런타임 단계에서 스프링 컨테이너가 해당 어노테이션을 읽고 동작한다.즉,
어노테이션은 "누가 읽느냐"와 "언제 읽느냐"에 따라 다르게 작동한다.
(컴파일러, JVM, 스프링 컨테이너 등)
어노테이션은 사실상 특별한 인터페이스라고 볼 수 있다.
자바에서는 어노테이션도 @interface 키워드로 정의한다.
그림 출처: https://velog.io/@anak_2/Java-annotations-이란-설명-활용
Meta Annotations은 어노테이션을 만들기 위한 어노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
int count() default 1;
}
| 구성 요소 | 설명 |
|---|---|
@Target | 어노테이션을 어디에 붙일 수 있는지 지정 (예: TYPE, METHOD, FIELD, PARAMETER 등) |
@Retention | 어노테이션이 언제까지 유지될지 지정 - SOURCE: 컴파일 시만 존재- CLASS: 클래스 파일에만 존재- RUNTIME: 실행 중에도 JVM에 의해 참조 가능 |
@interface | 어노테이션 정의 키워드 |
value() | 어노테이션의 속성 값 (필수 또는 기본값 가능) |
default | 기본값 지정 가능 |
| 종류 | 설명 | 사용 예시 |
|---|---|---|
SOURCE | 컴파일 시 사라짐 (주로 코드 검사용) | @Override, @SuppressWarnings |
CLASS | .class 파일에는 남지만 실행 시 사용되지 않음 | (거의 사용 X) |
RUNTIME | 런타임 시 JVM이 읽을 수 있음 | @Bean, @Service, @Entity |
스프링 프레임워크는 대부분 RUNTIME 어노테이션을 이용한다.
즉, 실행 중에 리플렉션(Reflection)으로 해당 어노테이션을 읽어 동작한다.
스프링은 리플렉션(Reflection)과 빈 후처리기(BeanPostProcessor)를 이용해
클래스에 붙은 어노테이션을 런타임에 스캔하고 그에 맞는 동작을 수행한다.
예: @Component 와 Bean 등록
@Component
public class UserService { }
스프링이 실행될 때 ClassPathScanningCandidateComponentProvider가
클래스패스를 스캔하여 @Component가 붙은 클래스를 찾는다.
찾은 클래스는 BeanDefinition 으로 등록되어
ApplicationContext(IoC 컨테이너)에 Bean으로 올라간다.
스프링 컨테이너 (Spring Container)
객체(Bean)를 생성하고 관리하는 IoC(Inversion of Control, 제어의 역전) 엔진
예2: @Transactional
@Transactional
public void registerUser() {
...
}
스프링은 실행 중 리플렉션을 통해 @Transactional 메서드를 감지한다.
그 후 AOP 프록시를 만들어 트랜잭션 시작/종료 로직을 자동으로 삽입한다.
AOP 프록시 (Aspect Oriented Programming, 관점 지향 프로그래밍)
공통 기능을 흩뿌리지 않고 한 곳에서 관리하는 기술
프록시(Proxy) 라는 가짜 객체를 만들어 메서드 실행 전후에 자동으로 공통 기능을 끼워 넣는다.
리플렉션 (Reflection)
실행 중에 클래스의 정보를 읽거나 조작할 수 있는 기술
쉽게 말하자면 코드가 자기 자신을 분석할 수 있는 능력
Annotation Processor는 컴파일 과정에서 어노테이션 정보를 분석하고, 관련 코드를 생성하여 .class 파일로 변환한다.
스프링은 런타임 시점에 이 .class 파일에 포함된 어노테이션 메타데이터를 리플렉션을 통해 읽어 실제 동작을 수행한다.
커스텀 어노테이션을 만들기 위해서는
1. @interface 클래스와
2. ConstraintValidator<@interface, String>의 구현 클래스
가 필요하다.
ValidPassword 인터페이스
@Documented
@Constraint(validatedBy = PasswordValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPassword {
String message() default "비밀번호는 소문자, 숫자, 특수문자 중 2가지 이상 조합으로 6자 이상이어야 합니다.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
message()를 통해서 실패시 표시할 메시지를 커스텀할 수 있다.groups() 는 스프링에서 제공하는 @Validated를 사용할 때 유용한 속성이다.payload()는 밸리데이션 클라이언트가 사용하는 메타데이터를 전달하기 위해서 사용한다.@Documented해당 어노테이션을 javadoc에 포함시킨다.
@Constraint(validatedBy = PasswordValidator.class)구현한 커스텀 어노테이션의 제약을 구현하는 클래스로는 PasswordValidator.class 로 지정하여 커스텀할 로직을 넣어준다.
@Target({ElementType.FIELD})어노테이션이 적용될 위치를 지정한다.
여러 개일 경우 {}로 감싸서 표현한다.
종류
ElementType.PACKAGE : 패키지선언ElementType.TYPE : 타입선언ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언ElementType.CONSRTUCTOR : 생성자 선언ElementType.FIELD : 멤버변수 선언ElementType.LOCAL_VARIABLE : 지역 변수 선언ElementType.METHOD : 메서드 선언ElementType.PARAMETER : 전달인자 선언ElementType.TYPE_PARAMETER : 전달인자 타입 선언ElementType.TYPE_USE : 타입 선언@Retention(RetentionPolicy.RUNTIME)어노테이션이 유지되는 기간을 지정하는데 사용한다.
유지되는 기간에는 3가지 방법이 있다.
세가지 유지정책(Retention policy)
RetentionPolicy.Source : 컴파일 전까지만 유효RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유효RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조 가능PasswordValidator 클래스
public class PasswordValidator implements ConstraintValidator<ValidPassword, String> {
private static final String LOWERCASE = ".*[a-z].*";
private static final String DIGIT = ".*\\\\d.*";
private static final String SPECIAL = ".*[!@#$%^&*(),.?\\\":{}|<>].*";
@Override
public boolean isValid(String password, ConstraintValidatorContext context){
if (password == null || password.length() < 6) return false;
int typeCount = 0;
if (password.matches(LOWERCASE)) typeCount++;
if (password.matches(DIGIT)) typeCount++;
if (password.matches(SPECIAL)) typeCount++;
return typeCount >= 2;
}
}
따라서 UserRegisterRequest에 작성한
@NotBlank(message = "비밀번호는 필수 입력값입니다.")
@ValidPassword // 커스텀 어노테이션
private String password;
의 ValidPassword 커스텀 어노테이션을 구현해 비밀번호가 조건을 만족하는지 검사할 수 있다.