개발/부트캠프

본캠프 : AOP(Aspect-Oriented Programming)

EJ EJ 2025. 3. 12. 13:10

공통 관심사를 한 곳에서 관리할 수 없을까요…?

공통 관심사 : 공통적이긴 한데, ‘공통’적이기 때문에 해당 로직에서 ‘핵심’ 로직은 아닐 가능성이 높습니다.

따라서 AOP를 사용한다면 코드에서는 핵심 로직에만 집중할 수 있는 장점이 있습니다!

 

AOP(Aspect-Oriented Programming)란?

AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 프로그램의 핵심 로직(Core Concern)과 부가적인 기능(Cross-Cutting Concern, 예: 로깅, 보안, 트랜잭션 등)을 분리하여 관리하는 프로그래밍 패러다임입니다.

📌 AOP 주요 개념

  1. Aspect(애스펙트): 공통적으로 적용할 기능(예: 로깅, 보안, 트랜잭션)
  2. JoinPoint(조인포인트): AOP 기능이 적용될 수 있는 지점(메서드 실행 등)
  3. Advice(어드바이스): AOP가 실행하는 코드 (Before, After, Around 등)
  4. Pointcut(포인트컷): 어떤 메서드에 AOP를 적용할지 결정하는 표현식
  5. Weaving(위빙): 애스펙트를 대상 코드에 적용하는 과정

📌 동작 방식

Spring AOP는 기본적으로 프록시(Proxy) 기반으로 동작합니다.

프록시(Proxy)란?

프록시라는 말은 앞으로 개발자 생활을 하면서 정말 많이 듣게 되실 겁니다!

그 때, 영어로 생각하기 보다는, 프록시의 한글 뜻인 ‘대리’를 생각하게 되면 많은 경우 이해가 쉬워지실 거예요.

Spring AOP가 프록시 기반으로 동작한다는 뜻은, 원래 클래스를 상속받은 새로운(대리) 클래스를 사용한다는 뜻입니다.

📌 위빙

핵심 비즈니스 로직과 부가 기능을 결합하는 과정을 말합니다.

어려워보이지만, 간단히 말하면, 프록시를 만드는 것 자체가 위빙입니다.

위빙 = 프록시 생성

📌 동적 프록시

인터페이스가 있을 때의 프록시 생성 방법 (implements 후 구현체 생성)

📌 CGLIB

인터페이스가 없을 때의 프록시 생성 방법 (extends 후 override)

 

📌 의존성 설정(build.gradle - dependencies)

implementation 'org.springframework.boot:spring-boot-starter-aop'

📌 코드 분석

AdminAccessLoggingAspect 클래스는 관리자(Admin) 접근 로그를 기록하는 AOP 기능을 제공합니다.

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AdminAccessLoggingAspect {

    private final HttpServletRequest request;

    @Before("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")
    public void logBeforeChangeUserRole(JoinPoint joinPoint) {
        String userId = String.valueOf(request.getAttribute("userId"));
        String requestUrl = request.getRequestURI();
        LocalDateTime requestTime = LocalDateTime.now();

        log.info("Admin Access Log - User ID: {}, Request Time: {}, Request URL: {}, Method: {}",
                userId, requestTime, requestUrl, joinPoint.getSignature().getName());
    }
}

 

  • @Slf4j → 로그 사용 가능하게 설정
  • @Aspect → 이 클래스가 AOP 기능을 제공하는 클래스임을 선언
  • @Component → Spring Bean으로 등록하여 자동 감지 가능하게 설정(따로 Config로 등록할 필요가 없다.)
  • @RequiredArgsConstructor → 생성자를 자동으로 생성 (HttpServletRequest 주입 가능)
  • HttpServletRequest를 주입받아 요청 정보를 가져올 수 있도록 설정
    (Spring이 관리하는 request 객체는 현재 요청의 정보를 담고 있음)
  • @Before("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")UserchangeUserRole(..)메서드가 실행되기 전에 AOP가 동작
    • 대상 지정 1번 방법: "execution(* 패키지명.클래스명.메서드명(..))" → 특정 메서드에만 적용하는 Pointcut 표현식
    • 대상 지정 2번 방법: @Pointcut, @AopTarget
    • @After: 메서드 실행 후 동작 / @Around : 메서드 실행 전과 후 모두 동작
  • JoinPoint joinPoint → 실행된 메서드 정보를 가져오는 객체
  • request.getAttribute("userId") → 요청 속성에서 userId 가져오기
    (예: 필터나 인터셉터에서 setAttribute로 저장한 값 활용)
  • request.getRequestURI() → 요청된 URL 가져오기
  • LocalDateTime.now() → 현재 요청 시간을 기록
  • 로그 메시지를 남겨 관리자가 changeUserRole(..) API를 호출한 정보를 기록
  • joinPoint.getSignature().getName() → 실행된 메서드 이름 가져오기 (changeUserRole)

▶ JoinPoint 추가 설명

AOP에서 JoinPoint 객체는 현재 실행 중인 메서드 정보를 가져올 수 있는 중요한 역할을 합니다.
그중에서 joinPoint.getSignature().getName()을 사용하면 실행된 메서드의 이름을 가져올 수 있습니다.

 

다른 유사한 JoinPoint 메서드

메서드 설명 예제 결과(changeUserRole)
joinPoint.getSignature().getName() 메서드 이름 반환 "changeUserRole"
joinPoint.getSignature().toShortString() 간략한 메서드 정보 "UserAdminController.changeUserRole(..)"
joinPoint.getSignature().toLongString() 자세한 메서드 정보
(매개변수 포함)
"public ResponseEntity UserAdminController.changeUserRole(java.lang.Long)"
joinPoint.getTarget().getClass().getName() 실행된 클래스의 전체 패키지명 포함한 클래스명 반환 "org.example.expert.domain.user.controller.UserAdminController"

 

- 정리

  • joinPoint.getSignature().getName() → 현재 실행된 메서드의 이름을 가져오는 메서드
  • AOP에서 실행된 메서드를 추적하는 로그를 남길 때 유용함
  • 필요에 따라 메서드 전체 정보 (toShortString(), toLongString()) 도 활용 가능

📌 실행 예제

만약 PATCH/admin/users/{userId} 요청이 들어와서 changeUserRole(userId, userRoleChangeRequest) 메서드가 실행되었다면, 로그는 다음과 같이 출력됩니다. 

(ex. PATCH/admin/users/1)

(여기서 User ID는 request.setAttribute("userId", 1234)로 설정된 값)

Admin Access Log - User ID: 1234, Request Time: 2024-03-12T10:15:30, Request URL: /admin/users/1 Method: changeUserRole