AOP (Aspect-Oriented Programming, 관점 지향 프로그래밍) : 프로그램의 핵심 로직과는 별개로, 공통적으로 반복되는 부가적인기능(관심사)을 한 곳에 분리해서 모듈화하는 프로그래밍 방식
AOP Concepts :: Spring Framework
Let us begin by defining some central AOP concepts and terminology. These terms are not Spring-specific. Unfortunately, AOP terminology is not particularly intuitive. However, it would be even more confusing if Spring used its own terminology. Around advic
docs.spring.io
AOP 개념
- Aspect : 여러 클래스에 걸쳐 있는 관심사의 모듈화. Spring AOP에서 Aspect는 일반 클래스 또는 어노테이션이 추가된 일반 클래스를 사용하여 구현된다. ex) 트랜잭션 관리, "주문 전에 로그 찍기", "주문 후 알림 보내기"
- Join point : 프로그램 실행 중 메서드 실행 또는 예외 처리와 같은 지점, Spring AOP에서 Join point는 항상 메서드 실행을 나타냄. ex) placeOrder() 메서드 실행 시점
- Advice : 특정 Join point에서 Aspect가 수행하는 작업으로, Arround, Before, After Advice가 있다. ex) Advice를 인터셉터로 모델링하고 Joint point 주변에 인터셉터 체인을 유지, "로그를 찍는다", "알림을 보낸다"
- Pointcut : Joint point를 매칭. Advice는 Pointcut 표현식과 연결되며, Pointcut과 매칭되는 모든 Join point에서 실행된다. ex) 특정 이름의 메서드, Spring은 기본적으로 AspectJ Pointcut 표현식 언어를 사용한다. placeOrder()에만 적용되도록 설정
- Introduction : 타입 대신 추가 메서드나 필드를 선언한다. Spring AOP를 사용하면 모든 Advice된 객체에 새로운 인터페이스를 도입할 수 있다.
- Target object : 하나 이상의 Aspect에 의해 Advice되는 객체이다. Spring AOP는 런타임 프록시를 사용하여 구현되므로 이 객체는 항상 proxy된 객체이다. ex) OrderService 클래스
- AOP proxy : AOP 프레임워크가 Aspect를 구현하기 위해 생성하는 객체. ex) 진짜 OrderService 대신 돌아가는 proxy
- Weaving : Aspect를 다른 애플리케이션 유형 또는 객체와 연결하여 Advised 객체를 생성 ex) placeOrder() 실행 전에 로그 찍게 연결함
public class OrderService { // Target Object
public void placeOrder() {
System.out.println("주문이 접수되었습니다.");
}
}
=================================================================
@Aspect
@Component
public class LoggingAspect {
// Advice 정의 (Joint point 실행 전에)
@Before("execution(* OrderService.placeOrder(..))") // Pointcut 표현식
public void logBefore() {
System.out.println("주문을 시작합니다 (로그 찍기)");
}
}
=================================================================
주문을 시작합니다 (로그 찍기)
주문이 접수되었습니다.
Advice 종류
- Before advice : Join point 전에 실행되는 Advice지만, 실행 흐름이 Joint point로 진행되는 것을 막을 수 없다.
- After returning advice : Joint point가 정상적으로 완료된 후 실행되는 Advice
- After throwing advice : 메서드가 예외를 던져 종료하는 경우 실행할 Advice
- After (finally) : Joint point가 종료되는 수단에 관계없이 실행해야 하는 Advice
- Around advice : 메서드 호출과 같은 Joint point를 둘러싼 Advice
밑에 코드를 살펴봤을 때, 권한 체크하는 부분이 API마다 반복적으로 발생하는 문제를 해결하기 위해 AOP를 도입해보았습니다. 각 서비스에서 Spring Security를 사용하지 않고 있기 때문에 @PreAuthorize 대신에 커스텀 어노테이션을 만들어보기로 했습니다.
@PostMapping
public ResponseEntity<ApiResponseData<CouponResponseDto>> createCoupon(AuthenticatedUser authenticatedUser,
@Valid @RequestBody CreateCouponRequestDto requestDto){
if (authenticatedUser.getRole() == null ||
!(authenticatedUser.getRole().equals("ADMIN") ||
authenticatedUser.getRole().equals("MANAGER"))) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponseData.failure(ResponseCode.ACCESS_DENIED_EXCEPTION.getCode()
,"접근 권한이 없습니다."));
}
CouponResponseDto couponInfo = couponService.createCoupon(requestDto.toDto(), authenticatedUser);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponseData.success(couponInfo));
}
메서드와 클래스 모두 적용할 수 있게 Target을 설정해주었습니다.
// 어노테이션이 메서드와 클래스에 모두 적용될 수 있도록 지정
@Target({ElementType.METHOD, ElementType.TYPE})
// 어노테이션 정보를 런타임까지 유지 (AOP 에서 사용 가능)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleCheck {
// 허용된 역할 목록 설정 (기본은 빈 배열 = 모든 역할 허용)
Role[] allowedRoles() default {};
}
@Aspect
@Component
public class RoleCheckAspect {
// @RoleCheck 어노테이션이 메서드 또는 클래스에 붙은 경우를 가로챔
@Around("@annotation(com.taken_seat.coupon_service.infrastructure.role.RoleCheck) || " +
"@within(com.taken_seat.coupon_service.infrastructure.role.RoleCheck)")
public Object checkRole(ProceedingJoinPoint joinPoint) throws Throwable {
// 현재 실행되는 메서드 정보 추출
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 메서드에 붙은 @RoleCheck 어노테이션 추출
RoleCheck roleCheck = method.getAnnotation(RoleCheck.class);
// 메서드에 어노테이션이 없으면 클래스에서 추출
if (roleCheck == null) {
roleCheck = joinPoint.getTarget().getClass().getAnnotation(RoleCheck.class);
}
// 메서드 인자 중에서 AuthenticatedUser 객체 탐색
Object[] args = joinPoint.getArgs();
AuthenticatedUser authenticatedUser = null;
for (Object arg : args) {
if (arg instanceof AuthenticatedUser) {
authenticatedUser = (AuthenticatedUser) arg;
break;
}
}
// 인증된 사용자 정보가 없으면 접근 거부
if (authenticatedUser == null) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponseData.failure(ResponseCode.ACCESS_DENIED_EXCEPTION.getCode(),
"접근 권한이 없습니다."));
}
// 사용자의 역할 정보를 enum으로 변환
Role userRole = Role.valueOf(authenticatedUser.getRole());
// 어노테이션에 설정된 허용 역할 목록 가져오기
Role[] allowedRoles = roleCheck != null ? roleCheck.allowedRoles() : new Role[]{};
// 아무 역할도 지정되지 않은 경우: 모두 허용
if (allowedRoles.length == 0) {
return joinPoint.proceed();
}
// 사용자 역할이 허용된 역할 목록에 있는지 확인
boolean hasAllowedRole = Arrays.stream(allowedRoles)
.anyMatch(role -> role == userRole);
// 권한이 없는 경우: 접근 거부
if (!hasAllowedRole) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponseData.failure(ResponseCode.ACCESS_DENIED_EXCEPTION.getCode(),
"접근 권한이 없습니다."));
}
// 모든 조건 통과: 원래 메서드 실행
return joinPoint.proceed();
}
}
위와 같이 권한 체크하는 기능을 따로 분리한 뒤 설정한 커스텀 어노테이션을 적용하면
@RoleCheck(allowedRoles = {Role.ADMIN, Role.MANAGER})
public class CouponController implements CouponControllerDocs {
private final CouponService couponService;
public CouponController(CouponService couponService) {
this.couponService = couponService;
}
@PostMapping
public ResponseEntity<ApiResponseData<CouponResponseDto>> createCoupon(AuthenticatedUser authenticatedUser,
@Valid @RequestBody CreateCouponRequestDto requestDto){
CouponResponseDto couponInfo = couponService.createCoupon(requestDto.toDto(), authenticatedUser);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponseData.success(couponInfo));
}
정상적으로 잘 동작하는 것을 확인할 수 있다.
Spring AOP에 대한 개념이 잘 잡혀있지 않은 상황에서 어려움이 많았지만 이번 기회로 AOP에 대해 이해할 수 있는 계기가 된 것 같다.
'🔥스파르타 TIL (Spring)' 카테고리의 다른 글
동시성 제어에 Redisson과 Lua Script 사용해보기 (1) | 2025.05.05 |
---|---|
MapStruct 사용해보기 (3) | 2025.04.26 |
Redis를 활용한 BlackList 관리 중 깨달은 문제 (0) | 2025.04.23 |
Transaction 이란? (0) | 2025.03.14 |
JPA Auditing 이란? (1) | 2025.03.07 |