공통 관심사를 한 곳에서 관리할 수 없을까요…?
공통 관심사 : 공통적이긴 한데, ‘공통’적이기 때문에 해당 로직에서 ‘핵심’ 로직은 아닐 가능성이 높습니다.
따라서 AOP를 사용한다면 코드에서는 핵심 로직에만 집중할 수 있는 장점이 있습니다!
AOP(Aspect-Oriented Programming)란?
AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 프로그램의 핵심 로직(Core Concern)과 부가적인 기능(Cross-Cutting Concern, 예: 로깅, 보안, 트랜잭션 등)을 분리하여 관리하는 프로그래밍 패러다임입니다.
📌 AOP 주요 개념
- Aspect(애스펙트): 공통적으로 적용할 기능(예: 로깅, 보안, 트랜잭션)
- JoinPoint(조인포인트): AOP 기능이 적용될 수 있는 지점(메서드 실행 등)
- Advice(어드바이스): AOP가 실행하는 코드 (Before, After, Around 등)
- Pointcut(포인트컷): 어떤 메서드에 AOP를 적용할지 결정하는 표현식
- 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)로 설정된 값)
'개발 > 부트캠프' 카테고리의 다른 글
본캠프 : 테이블 객체끼리 관계만들기 2 (0) | 2025.03.12 |
---|---|
본캠프 : 테이블 객체끼리 관계만들기 1 (0) | 2025.03.12 |
본캠프 : JwtFilter - ArgumentResolver 실습 1 (0) | 2025.03.11 |
본캠프 : 향상된 for문, stream 사용법 (0) | 2025.03.08 |
본캠프 : 비밀번호 암호화 (0) | 2025.03.07 |