-
Custom Annotation을 만들고 AOP로 활용하는 법개발/Spring(boot) 2025. 1. 19. 23:53
이전 글을 읽고 보면 이해가 더 쉬울 것 같습니다.
Spring Security 인가(Authorization)를 공부하다 보면 아래와 같은 코드를 마주치게 된다.
@Component public class BankService { @PreAuthorize("hasRole('ADMIN')") public Account readAccount(Long id) { // ... is only invoked if the `Authentication` has the `ROLE_ADMIN` authority } }
"ADMIN" 권한이 있는 유저만 해당 메서드 또는 api에 접근할 수 있도록 통제하는 것 이다.
근데 AOP를 공부하다 보니 나만의 @PreAuthorize를 만들 수 있을 것 같았고
이번 글은 나만의 권한 체크 커스텀 어노테이션을 만드는 방법에 대해 작성하려 한다.
순서는 아래와 같다.
1. AOP에 대한 간단한 정리
2. annotation 작성 방법
3. AOP를 통해 어노테이션을 감지하는 방법
4. AOP와 annotation을 통해 권한을 체크하는 custom annotation 작성 방법
순서로 작성할 예정이다.
이전 글에서 작성했던 AOP가 어떻게 동작하는지에 대해 간단하게 복습해보자.
Spring AOP의 경우 호출 전(@Before), 호출 이후(@After), 호출 전체(@Around) 등으로 관리할 수 있으므로 이와같이 프록시 객체가 생성된다.
일반 호출의 경우
프록시 객체의 경우
따라서 Spring AOP의 경우 호출 전(@Before), 호출 이후(@After), 호출 전체(@Around) 등으로 관리할 수 있다.
그렇다면 프록시 객체를 사용한다면 나만의 권한 체크를 도와주는 커스텀 어노테이션을 충분히 만들 수 있어 보인다.
먼저 어노테이션을 만들어보자. 신기하게도 어노테이션은 @interface라고 선언을 해서 만들 수 있다.
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAuthorize { MyAuthority value(); }
MyAuthorize라는 어노테이션을 작성했고 어노테이션에서 사용 가능한 속성 대해 조금 더 알아보자.
어노테이션에서는 아래와 같은 속성을 사용할 수 있다.
@Target, @Retention, @Documented, @Inherited, @Repeatable
이 중에서 @Target, @Retention에 대해서만 간단히 설명해보자면
1. @Target
@Target은 어노테이션이 어디에 적용될 수 있는지를 지정하는 ElementType이라는 enum값을 받는다.
ElementType의 종류는 아래와 같다.
- ElementType.TYPE : 클래스, 인터페이스, 열거형에 적용
- ElementType.FIELD : 필드(멤버 변수)에 적용
- ElementType.METHOD : 메서드에 적용
- ElementType.PARAMETER : 메서드의 매개변수에 적용
- ElementType.CONSTRUCTOR : 생성자에 적용
- ElementType.LOCAL_VARIABLE : 지역 변수에 적용
- ...
어노테이션을 붙였을 때의 scope를 지정하는 enum값 정도로 이해했다.
2. @Retention
@Retention은 애너테이션이 어느 시점까지 유지되는지를 지정하는 RetentionPolicy라는 enum값을 받는다.
RetentionPolicy의 종류는 아래와 같다.
- RetentionPolicy.SOURCE : 컴파일러에 의해 제거. 소스 코드에서만 유효
- RetentionPolicy.CLASS : 컴파일 후 클래스 파일에 저장되지만, 런타임에는 사용할 수 없습니다.
- RetentionPolicy.RUNTIME : 런타임 시점까지 유지. Reflection을 통해 접근 가능
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAuthorize { MyAuthority value(); }
그럼 다시 MyAuthorize를 보면
이 어노테이션은 메서드, 클래스, 인터페이스, 열거형에 적용되는 어노테이션이고(@Target)
런타임 시점까지 유지된다.(@ Retention)
MyAuthorize라는 어노테이션 파일까지 만들었으니
이제 AOP를 통해 MyAuthorize 어노테이션을 감지하는 방법에 대해 알아볼 차례이다.
먼저 AOP를 적용하기 위해서는 아래와 같이 @EnableAspectJAutoProxy를 사용해야 하고
@EnableAspectJAutoProxy public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }
이후 아래와 같이 AOP가 적용된 클래스 파일을 만들 수 있다.
@Aspect public class MyAuthorityAop { }
만약 아래와 같은 구조로 프록시 객체가 만들어진다면
의도하지 않았지만 스프링 프로젝트에 있는 모든 파일에 프록시 객체가 만들어지는 상황이 생길 수 있다.
따라서 어떤 파일에 AOP를 적용해 프록시 객체를 만들지 scope를 지정해줄 수 있다.
지원하는 Pointcut 지시어들(Supported Pointcut Designators)
- execution : 메서드 실행 join point에 매칭
- within : 특정 타입 내의 join point만 매칭
- @within : 클래스에 특정 애너테이션이 선언된 경우, 해당 클래스 내 모든 메서드가 join point로 매칭
- @annotation: 조인 포인트의 대상 메서드가 지정된 애너테이션을 가지고 있을 경우에만 매칭
- this : ..
- target: ...
- args: ...
- @target: ...
- @args: ...
@within과 @annotation이 적합해보인다.
이를 사용해 아래와 같이 작성한다면 @MyAuthorize가 붙은 클래스 파일 또는 메서드에 대해 권한 검사를 할 수 있다.
@Component @Aspect @RequiredArgsConstructor public class MyAuthorityAop { private final MyAuthorityService authorityService; @Before(value = "@annotation(myAuthorize))") public void checkMethodAuthority(MyAuthorize myAuthorize) { authorityService.checkUserAuthority(); } @Before(value = "@within(myAuthorize))") public void checkClassAuthority(MyAuthorize myAuthorize) { authorityService.checkUserAuthority(); } }
그리고 "/user/{id}" 라는 요청에 대해 권한 확인을 하고 싶다면 아래와 같이 어노테이션만 작성해주면 된다.
@MyAuthorize(MyAuthority.USER) @GetMapping("/user/{id}") public Response getUser(@PathVariable Long id) { return myAppService.getUserById(id); }
이렇게 어노테이션을 작성해주면
1. AOP가 해당 메서드 또는 클래스에 대해 프록시 객체를 만들고
2. MyAuthorityAop에 있는 checkMethodAuthority 호출
2-1. 만약 권한이 없는 경우 예외 처리
3. myAppService.getUserById(id) 호출
순서로 동작하게 된다.
커스텀 어노테이션을 만들고 이를 실제로 적용하는 방법에 대해 작성해봤다.
작성하면서 느낀 점은 어노테이션은 단순히 scope를 지정해 주는 용도이고 실제 로직은 aop가 전부 한다는 느낌을 받았다.
어노테이션은 메타 데이터를 제공하는 특별한 종류의 인터페이스로, 클래스, 메서드, 필드 등에 부가 정보를 추가하는 데 사용된다고 하니 용도에 맞게 사용하는 것 같다.
Spring Security도 좋지만 간단한 인가 로직의 경우 간단한 커스텀 어노테이션을 만드는게 오히려 시간이 덜 걸릴 수도 있겠다는 생각이 들었다.
출처
https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/aspectj-support.html
https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/pointcuts.html
반응형'개발 > Spring(boot)' 카테고리의 다른 글
@Transactional을 final class에 붙이면 안되는 이유(AOP의 동작원리) (0) 2025.01.12 스프링의 DB 관련 예외 추상화 (0) 2025.01.05 ApplicationEventPublisher와 EventListener (0) 2024.12.01