🔍 AOP: Aspect-Oriented Programming
관점 지향 프로그래밍 → [ 핵심 기능 | 공통 기능 ] 으로 나눈 뒤, 핵심 기능에서 공통 기능을 불러와 적용하는 방법.
비즈니스 로직이 핵심 기능이라면 인증이나 로깅은 부가 기능에 속한다.
공통 기능을 분리해서 사용하는 것에는 Filter나 Interceptor와 같은 기능도 포함이지만, 이 기능들은 각각 실행되는 시점이 다르며, 시점에 따라 순서도 다르다. Filter - Interceptor - AOP - Interceptor - Filter 순으로 실행된다. (요청에서 응답까지)
그 중 AOP를 활용해서 요청과 응답에 대한 정보를 남기는 것을 선택했다.
1. Advice
부가 기능을 정의한 코드
핵심 기능을 담고 있는 Target에 제공할 기능을 담고 있다.
2. Join Point
advice가 적용될 수 있는 위치
Target객체가 구현한 모든 메서드는 join point가 된다.
즉, target의 메서드는 advice가 적용될 수 있는 join point → 해당 메서드에 부가 기능을 적용시키는 것
3. Pointcut
advice를 적용할 메서드를 구분하는 정규표현식
참고할 글
1. IT is True: [Spring] 스프링 AOP 포인트컷(Pointcut) 표현식 정리
2. Shin._.Mallang: [AOP] AOP 포인트컷 표현식 (1) - execution
* 2번 블로그에는 포인트컷 관련된 글 각 링크로 정리되어 있다.
4. Aspect
Advice + Pointcut = Aspcet
AOP의 기본 모듈로 싱글톤 객체로 존재한다.
Aspect와 함께 Advisor라는 용어도 함꼐 사용한다.
🛠️ 구현
1. Application에 어노테이션 추가
@SpringBootApplication
@EnableAspectJAutoProxy
public class ExpertApplication {
AOP는 proxy
로 생성되어 중간에 가로채고 역할을 수행한다.
이를 위해 Application에 @EnableAspectJAutoProxy
를 사용해 AOP를 활성화하고 프록시로 동작하도록 지정한다.
2. 의존성 주입
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
3. Logging 클래스 구현
@Aspect
@Component
@RequiredArgsConstructor
public class LoggingAspect {
@Autowired
private HttpServletRequest request;
private final ObjectMapper mapper;
private final Logger log = LoggerFactory.getLogger(getClass());
...
}
gradle에 의존성을 설정해 aop를 사용할 수 있게 되었다. aop를 구현하기 위한 클래스에 @Aspect
를 지정한다.
aop는 싱글톤 객체로 동작해 bean 등록이 필요하다. @Component
어노테이션을 사용해 bean으로 만들자
그리고 요청 정보를 가져오기 위한 HttpServletRequest
와 JSON형식으로 표현하기 위해 ObjectMapper
가져왔다.
요청 정보와 JSON표현, Logger는 과제 수행을 위한 조건을 위해 사용했다.
4. point cut 작성
@Around("execution(* org.example.expert.domain.*.*.*AdminController.*(..))")
메서드에 Around
어노테이션을 붙여준다. 그리고 그 값으로 포인트컷을 작성하는데, 이 부분은 정규표현식을 확인해 작성해야 한다.
먼저 메서드가 실행될 때 동작하기 위해 execution
이 제일 앞에 들어가고, 그 뒤에 적용할 포인트컷을 작성하면 된다.
comment/controller/CommentAdminController
에 있는 메서드와 user/controller/UserAdminController
에 있는 메서드에 적용해야 하지만 중간에 서로 다른 패키지로 갈라지기 때문에 *
이 필요했다. domain으로 내려가는 것까지는 동일해 그 전까지는 순서대로 작성해주었고, domain 하위 패키지를 작성할 때 *
을 작성했다.
- org.example.expert.domain.*
domain 하위의 모든 패키지에 적용한다.
domain 아래에 있는 auth, comment, common, manager, todo, user에 적용하는 것이다.
이 domain하위 패키지 위치를 A라고 가정
- org.example.expert.domain.*.*
A 하위의 모든 패키지에 적용한다.
domain 하위의 모든 패키지 → A, 그 하위에 있는 모든 패키지에 적용한다.
위 패키지 구성 기준 controller, service, repository, exception 등이 포함된다.
이 A의 하위 패키지 위치를 B라고 가정
- org.example.expert.domain.*.*.*AdminController
B 하위에서 *AdminController
를 찾아낸다.
찾아야 하는 클래스가 CommentAdminController
와 UserAdminController
로 AdminController앞에 각각 다른 문자가 붙어있다.
모든 문자를 의미하도록 AdminController
앞에 *
를 붙인다.
- org.example.expert.domain.*.*.*AdminController.*(..)
적용할 클래스를 찾았으니 멈추고, 적용할 메서드를 찾는다.
해당 controller 안에 모든 method에 적용시킬 것으로 *
를 작성
method의 모든 매개변수에 적용해야 하니 괄호 안에 ..
을 넣는다.
여기서 (*)
를 사용하면, 매개변수 하나를 의미한다.
모든 매개변수를 위해 (..)
로 지정해야 한다.
5. 메서드 구현
@Around("execution(* org.example.expert.domain.*.*.*AdminController.*(..))")
public void adminLogging(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
log.info("method = {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
log.info("user = {}", request.getAttribute("userId"));
log.info("time = {}", LocalDateTime.now());
log.info("url = {}", request.getRequestURL().toString());
String request = mapper.writeValueAsString(args);
log.info("request = {}", request);
String response = mapper.writeValueAsString(joinPoint.proceed());
log.info("response = {}", response);
}
ProceedingJoinPoint
로 가로챌 데이터들을 가져온다.
실행할 method를 package와 함께 나타내고 싶어 joinPoint.getSignature().getDeclaringTypeName()
를 앞에 붙여주었다.
그냥 joinPoint.getSignature()
를 지정하면 package와 함께 method가 나오지만 그 parameter도 뒤에 붙어 나온다.
그냥 어떤 package의 어떤 method를 실행했는지 알기 위해 getDeclaringTypeName()
과 getName()
을 사용
그리고 HttpServletRequest
에 저장된 정보로 userId와 요청url을 가져올 수 있었다.
로그인 시 토큰을 발급하며 setAttribute
로 userId를 넣어주고 있길래 그 값을 가져왔고, 요청url은 toString()
으로 변환하였다.
요청과 응답에 대한 본문은 JSON으로 반환하기 위해 ObjectMapper의 writeValueAsString()을 이용했다.
ProceedingJoinPoint
의 proceed()
를 실행하면, 핵심 기능인 Target메서드가 실행되는데 그 반환은 Object로 나온다.ObjectMapper
덕분에 반환 타입이 무엇이든 신경쓰지 않고 JSON으로 쉬운 변환이 가능했다.✅ ProceedingJoinPoint 주요 기능
Signature signature = joinPoint.getSignature();
String declaringTypeName = joinPoint.getSignature().getDeclaringTypeName();
String name = joinPoint.getSignature().getName();
Class declaringType = joinPoint.getSignature().getDeclaringType();
Object proceed = joinPoint.proceed();
Object[] args = joinPoint.getArgs();
proceed()
핵심 기능을 실행하고 그 응답을 가져온다.getArgs()
요청 정보를 가져온다.
그 위 getSignature() 4개 결과는 아래와 같다.
signature = void org.example.expert.domain.user.controller.UserAdminController.changeUserRole(long,UserRoleChangeRequest)
declaringTypeName = org.example.expert.domain.user.controller.UserAdminController
name = changeUserRole
declaringType = class org.example.expert.domain.user.controller.UserAdminController
getSignature()
적용된 위치 join point 그 자체 → 반환 패키지.클래스.메서드(파라미터)
getDeclaringTypeName()
적용된 패키지getName()
호출한 메서드getDeclaringType()
적용된 위치 타입
6. 로그 결과

실제 구현보다 point cut을 어떻게 구성해야 하나 고민하느라 시간이 더 걸렸던 것 같다..
point cut이 잘못되면 컴파일도 성공하고, 어떤 오류도 나지 않지만 aop가 실행되지 않는다. point cut 작성에 주의해야 한다.
references
effortDev: Spring AOP, Aspect 개념 특징, AOP용어 정리
F-Lab: AOP를 활용한 스프링 애플리케이션의 로깅 및 유효성 검증
해어린 블로그: SpringBoot의 AOP를 이용해서 로그 남기
'Framework > SpringBoot' 카테고리의 다른 글
[Spring Boot] Toss Payments로 '가상계좌' 결제 기능 구현 (1) 연동하기 (0) | 2025.04.22 |
---|---|
[SpringBoot] Redis 적용 시 @Indexed 사용 문제점 (0) | 2025.03.26 |
[SpringBoot] 테스트 코드 작성 시 Mock과 Spy, 그리고 BCryptPasswordEncoder의 encode (0) | 2025.03.26 |
[SpringBoot] Cache 값을 저장하지 못하던 문제 해결 및 @Cacheable과 @CachePut 차이 (0) | 2025.03.06 |
[SpringBoot] access token과 refresh token을 만들어 postman으로 테스트하기 (1) (0) | 2025.02.27 |