Kuma's Curious Paradise
자바와 스프링의 어노테이션, 커스텀 어노테이션 만드는 법 본문
1. 자바의 어노테이션과 스프링의 어노테이션
어노테이션은 @ 기호로 시작하는 메타데이터로, 클래스나 메서드 위에 붙이면 어떤 의미나 동작을 추가로 부여한다.
예를 들어,
- @Deprecated는 “이거 이제 쓰지 마세요”라고 경고한다.
- @Autowired는 “스프링 씨, 이 클래스 빈으로 만들어서 자동 주입해주세요”라는 뜻이다.
자바에는 기본적으로 제공하는 어노테이션들이 있다.
JDK에서 자체 작동하는 어노테이션들이다.
예를 들면 :
- @Override
- @Deprecated
- @FunctionalInterface
이런 어노테이션들은 컴파일러가 인식/처리하지만(override한 메서드의 이름이 다른지 확인하는 등), 실행 중에 무슨 동작을 하진 않는다.
스프링은 좀더 발전된 어노테이션 체계를 추구한다.
어노테이션을 통해 자동으로 객체를 등록하거나, 트랜잭션 처리, AOP 적용을 해 준다.
예를 들면 :
- @Component
- @Service
- @Transactional
자바 어노테이션을 활용하여, 스프링 프레임워크가 특정 동작을 알아서 하게 만든다. (실행 중에 트랜잭션이 동작하는 등)
2. 어떻게 @를 붙인다고 해서 이 모든 일이 가능할까?
어노테이션은 단순히 메타데이터를 붙이는 것 뿐이다.
이 메타데이터를 실제로 읽고 해석하는 작업은 '리플렉션'이 한다.
예를 들면 :
@Component
public class UserService {}
스프링이 실행되면 -> 리플렉션으로 모든 클래스를 스캔한다 -> "어? 여기에 @Component 붙어 있네? 빈으로 등록해 주자!' 식으로 처리된다.
그렇다면 어노테이션은 표시이고, 실제로 어노테이션을 찾고, 해당 어노테이션에 따라 동작을 실행시키는 건 리플렉션의 역할이라고 생각할 수 있다.
2-1. 리플렉션이란?
리플렉션은 코드 실행 중에 클래스, 메서드, 필드 등을 들여다보고 조작하는 기능이다.
따라서 다음과 같은 코드가 가능하다.
Class<?> clazz = UserService.class;
if (clazz.isAnnotationPresent(Component.class) {
// Component 어노테이션이 붙어 있으면 해야 할 일
}
이런 식으로 스프링 프레임워크는 동적으로 어노테이션을 감지하고 동작을 수행할 수 있다.
따라서 스프링은 어플리케이션을 시작하며 다음과 같은 일을 한다.
1. 클래스 패스를 스캔하며 모든 클래스를 찾는다.
2. 각 클래스에 어떤 어노테이션이 붙어 있는지 리플렉션으로 확인한다.
3. @Component 같은 어노테이션이 있으면 빈으로 등록한다.
4. @Transactional, @Scheduled, @Autowired 같은 어노테이션이 붙어 있으면 특정 행동을 자동으로 수행한다.
3. 커스텀 어노테이션
3-1. 어노테이션의 구성 요소
@Target(ElementType.TYPE) // 이 어노테이션을 어디에 붙일 건지
@Retention(RetentionPolicy.RUNTIME) // 어노테이션 정보가 언제까지 유지될지 (컴파일까지? 런타임까지?)
/**
* 어노테이션은 특별한 종류의 인터페이스로 치기 때문에, 정의할 때 @interface를 붙여 선언한다
* @interface 를 붙인 클래스는 자동으로 자바의 'Annotation' 클래스를 상속받는다.
*/
public @interface SomeAnnotation {
String value(); // 어노테이션에 전달할 값
int count() default 1; // 값이 주어지지 않았을 때 사용할 기본값
}
3-2. 커스텀 어노테이션 만들기
@PostMapping("/places/likes/{placeId}")
public ResponseEntity<?> togglePlaceLike(@CurrentMember Member member,@PathVariable Long placeId) {
return CommonResponse.created(likeService.togglePlaceLike(member, placeId));
}
위와 같이 현재 요청을 보낸 멤버의 정보를 가지고 오는 '@CurrentMember' 라는 어노테이션을 만들고 싶다면 다음과 같이 할 수 있다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentMember {
}
어노테이션을 먼저 선언하고, ElementType을 PARAMETER로 지정한다.
@Component
public class CurrentMemberArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentMember.class) &&
parameter.getParameterType().equals(Member.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 익명 사용자 처리
if (authentication == null || !authentication.isAuthenticated() ||
authentication instanceof AnonymousAuthenticationToken) {
return null;
}
// 인증된 사용자 처리
MemberDetails memberDetails = (MemberDetails) authentication.getPrincipal();
return memberDetails.member(); // Member 객체를 반환
}
}
이제 이 어노테이션이 붙은 파라미터에 어떤 값을 넣을지 정하는 resolver를 만든다.
컨트롤러의 method의 parameter로 들어올 값에 붙일 것이기 때문에, HandlerMethodArgumentResolver 인터페이스를 통해 이를 처리한다.
HandlerMethodArgumentResolver가 무엇인지는 아래 블로그에서 설명을 너무 잘해 주셔서 첨부한다.
위의 코드에서 각 메서드는 다음과 같은 일을 맡는다.
- supportsParameter → 이 resolver가 어느 파라미터에서 작동할지
→ @CurrentMember 어노테이션이 붙어 있고 타입이 Member인 경우만 처리 - resolveArgument → 실질적으로 파라미터에 어떤 값을 넣을지
→ Spring Security의 SecurityContextHolder에서 현재 인증 객체(Authentication)를 꺼내 그 안의 principal에서 Member를 꺼내 반환
이제 이 resolver를 스프링에 등록한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final CurrentMemberArgumentResolver currentMemberArgumentResolver;
public WebConfig(CurrentMemberArgumentResolver currentMemberArgumentResolver) {
this.currentMemberArgumentResolver = currentMemberArgumentResolver;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentMemberArgumentResolver);
}
}
이렇게 어노테이션을 통해 반복적으로 써주어야 하는 코드를 줄일 수 있다.
최근 데이터를 -> 엑셀로 만드는 기능을 작성해야 했는데, 인터넷으로 찾아보며 엑셀의 컬럼 및 데이터타입 지정을 위해 커스텀 어노테이션을 어떻게 쓰는지도 볼 수 있었다. 자세한 내용은 아래를 참고하면 좋겠다.
https://techblog.woowahan.com/2698/
아 엑셀다운로드 개발,,, 쉽고 빠르게 하고 싶다 (feat. 엑셀 다운로드 모듈 개발기) | 우아한형제
실제로는 적절한 이름을 가진 private method로 코드가 나누어져 있습니다. @NoArgsConstructor(access = AccessLevel.PRIVATE) public ExcelRenders { public static createCellStyle(Workbook wb, ColumnType columnType) { XSSFCellStyle cellStyl
techblog.woowahan.com
'스프링' 카테고리의 다른 글
Jackson과 친해지기: @JsonFormat, @JsonCreator, @JsonProperty (1) | 2025.05.02 |
---|---|
equals()와 hashCode() 그리고 @EqualsAndHashCode : 왜 쓰고, 언제 쓰는가? (1) | 2025.04.12 |
[아이북조아] 10만 회원의 성향 변경 배치 처리: 문제와 해결 (0) | 2025.01.08 |
[멍티비티] 최종 발표 자료 및 멍티비티 회고 & 기술적 고민 (1) | 2025.01.05 |
[아이북조아] 최종 발표 자료 및 아이북조아를 마치며 (5) | 2025.01.05 |