[Java] 비밀번호 검증 커스텀 어노테이션 적용하기 + 어노테이션 + 커스텀 어노테이션

TNFUDS·2025년 11월 7일

FinTrack 프로젝트

목록 보기
4/14

그동안 자바 개발을 하며
@Override, @Bean 등 다양한 어노테이션을 보았지만, 어떤 원리로 동작하는지 제대로 이해해보는 시간이 없었다.
이번 기회에 동작 원리를 공부하고 커스텀 어노테이션을 어떻게 프로젝트에 적용했는지 작성하고자 한다.


1. 어노테이션이란?

어노테이션이란 코드에 대한 메타데이터(Metadata)로
컴파일러나 런타임 환경이 해석할 수 있는 추가 정보를 제공한다.

즉, 어노테이션은 프로그램의 실행 로직에는 직접 관여하지 않지만,
코드에 의미나 동작을 부여하기 위해 사용된다.

예를 들어,

@Override
public String toString() {
    return "User";
}
  • @Override는 컴파일러에게 "이 메서드는 부모 클래스의 메서드를 재정의한 것이다" 라고 알려주는 표시다. 컴파일 타임에 체크 역할을 한다. (컴파일러가 “정말 재정의했는지” 확인함)

반면,

@Bean
public UserService userService() {
    return new UserService();
}
  • @Bean은 스프링이 런타임 시점에 Bean 객체로 등록하라는 명령 신호다. 런타임 단계에서 스프링 컨테이너가 해당 어노테이션을 읽고 동작한다.

즉,
어노테이션은 "누가 읽느냐"와 "언제 읽느냐"에 따라 다르게 작동한다.
(컴파일러, JVM, 스프링 컨테이너 등)


어노테이션의 용도

  1. 컴파일러에게 코드 작성 문법 에러를 체크하도록 정보를 제공한다.
  2. 소프트웨어 개발툴이 빌드나 배치 시 코드를 자동으로 생성할 수 있도록 정보를 제공한다.
  3. 런타임 시 특정 기능을 실행하도록 정보를 제공한다.

2. 어노테이션 구성과 동작원리 (스프링은 어떻게 사용하는가?)

어노테이션은 사실상 특별한 인터페이스라고 볼 수 있다.
자바에서는 어노테이션도 @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)를 이용해
클래스에 붙은 어노테이션을 런타임에 스캔하고 그에 맞는 동작을 수행한다.

예: @ComponentBean 등록

@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 파일에 포함된 어노테이션 메타데이터를 리플렉션을 통해 읽어 실제 동작을 수행한다.



3. 프로젝트에 적용하기

커스텀 어노테이션 조건

커스텀 어노테이션을 만들기 위해서는
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 커스텀 어노테이션을 구현해 비밀번호가 조건을 만족하는지 검사할 수 있다.

profile
내 세상을 넓혀가는 중

0개의 댓글