AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)
- 공통적인 기능이나 관심사를 분리하여 모듈화하고, 필요한 시점에 핵심 로직에 삽입하여 실행하는 프로그래밍 패러다임
- 핵심 로직: 핵심 비즈니스 로직
- 부가적인 관점: 핵심 로직을 실행하기 위한 DB 연결(+ @Transaction), 로깅, 보안, 파일 입출력 등
- 시스템 전반에 필요한 기능들을 모듈화 시키고 비즈니스 로직을 가지는 객체와 결합하는 방식
- Cross-Concern(횡단 관심사): 보안이나 로깅과 같이 시스템 여러 부분에서 필요한 공통적인 기능
- AOP는 횡단 관심사를 분리하고, 이를 결합하는 기능이 필요한데 스프링은 이러한 기능을 프레임워크에서 지원
- Spring AOP는 Proxy객체를 생성(Proxy 기반으로 AOP를 쓸 수 있음)
비지니스 로직과 부가적인 기능(보안, 트랜잭션, 로깅 등)을 분리시키고 Runtime시 분리됐던 비지니스 로직과 부가적인 기능을 결합시켜서 완전하게 구현되도록 하는 것
AOP의 주요 개념
- Aspect
- 여러 클래스에 걸쳐 적용되는 관심사의 모듈화
- 부가기능을 정의하는 Advice와 적용 위치를 결정하는 PointCut으로 구성
- PointCut
- Aspect가 적용될 프로그램상 실제 위치
- Spring은 기본적으로 AspectJ 포인트컷 표현 언어를 사용
- Advice를 적용할 타겟의 메서드를 선별하는 정규표현식
- execution(반환타입 패키지경로 클래스 메서드(파라미터 타입))
- *: 모든 타입 OK
- (..): 모든 파라미터 타입 및 파라미터 수 상관 X
- (String): 파라미터가 String이어야 함
- (String, ..): 첫 번째 파라미터는 String, 나머지 파라미터는 모든 타입 및 파라미터 수 상관 X
- (): 파라미터가 없어야 함
- (*): 모든 파라미터 타입 및 파라미터는 1개
- (*, *): 모든 파라미터 타입 및 파마리터는 2개
- execution(* org.zerock.springdemo.*.*(..)): 패키지 하위의 모든 클래스와 메서드에 적용
- execution(* org.zerock.springdemo.domain.todo.TodoService.add*(..): todo 패키지 내 TodoService의 메서드 중 add로 시작하는 모든 메서드에 적용
- execution(반환타입 패키지경로 클래스 메서드(파라미터 타입))
- JoinPoint
- PointCut의 후보군(Aspect가 적용될 수 있는 위치들)
- mothod가 호출되는 시점, 특정 class의 생성자가 호출되는 시점, exception이 발생하는 시점 등
- Advice가 추가(Join)될 대상 메서드
- Spring AOP에서 조인 포인트는 항상 메소드 실행을 나타냄
- Spring AOP의 경우 Proxy 패턴을 이용하기 때문에 항상 Proxy에 해당하는 Sub Class를 만들어야 함
- Aspect를 적용하려는 Class가 Final Class이거나 Method가 Final 또는 Static일 때도 적용 X
- Spring에서 관리하는 Bean에서만 작동
- Target
- Advice가 추가될 객체
- Advice
- 실질적으로 부가기능 로직이 정의되어있는 객체
- Target에 동적으로 추가될 부가 기능
- @Around: 메서드의 시작과 끝 부분에 추가(조인 포인트로 진행할지 아니면 자체 반환 값을 반환하거나 예외를 발생시켜 조언된 메서드 실행을 단축할지 선택하는 일도 담당)
- @Before: 메서드의 시작 부분에 추가
- @After: 메서드의 끝 부분에 추가(예외 발생 유무 상관 X, finally)
- @AfterReturning: 예외가 발생하지 않았을 때, 실행
- @AfterThrowing: 예외가 발생했을 때, 실행
- AOP Proxy
- Target에 Advice가 동적으로 추가되어 생성된 객체(AOP 프레임워크에서 생성된 객체)
- Spring AOP는 기본적으로 AOP 프록시에 대해 표준 JDK 동적 프록시를 사용
- Weaving
- Aspect를 실제 코드에 적용하는 과정을 나타냄(공통 코드를 핵심 로직 코드에 삽입하는 것)
- 컴파일 타임, 로드 타임, 런타임에 수행될 수 있음(Spring AOP는 런타임에 위빙을 수행)
프로젝트에 Spring AOP 적용하기
build.gradle.kts에 AOP 의존성 추가
implementation("org.springframework.boot:spring-boot-starter-aop")
🤔 의존성을 추가하다가 들었던 의문점
JPA를 사용하는 경우 JPA 내부에 AOP를 포함하고 있는데 왜 명시적으로 의존성 코드를 추가해야 하는가에 대한 의문이 들었다.
💡 사용하고 있는 의존성 확인을 위함(개발자 편의를 위해)
JPA가 AOP를 포함하고 있더라도 의존성 목록에 등록해 놓으면 현재 프로젝트에서 어떤 의존성을 사용하는지 쉽게 알 수 있으므로 등록하는 것이 좋다.(나는 알지만 다른 개발자는 모르기 때문에)
Entry 함수가 있는 클래스 위에 다음과 같이 @EnableAspectJAutoProxy 어노테이션 추가
어노테이션에 AspectJ가 들어가는 이유: 스프링 AOP가 기본적으로 Aspect를 차용을 해서 만들었기 때문
@EnableAspectJAutoProxy
@SpringBootApplication
class TodolistApplication
fun main(args: Array<String>) {
runApplication<TodolistApplication>(*args)
}
@Around 예시 코드
@Aspect
@Component
class TestAop {
@Around("execution(* org.zerock.todolist.domain.todo.service.TodoService.*(..))")
fun thisIsAdvice(joinPoint: ProceedingJoinPoint) {
println("AOP START!!")
joinPoint.proceed()
println("AOP END!!")
}
}
메서드 실행 결과
AOP START!!
Hibernate: select t1_0.id,t1_0.completed,t1_0.content,t1_0.created_at,t1_0.title,t1_0.user_id,t1_0.writer from todo t1_0 where t1_0.id=?
2024-06-04T14:15:12.513+09:00 TRACE 16560 --- [nio-8080-exec-1] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [10]
Hibernate: select c1_0.todo_id,c1_0.id,c1_0.content,c1_0.created_at,c1_0.password,c1_0.user_id,c1_0.writer from comment c1_0 where c1_0.todo_id=?
2024-06-04T14:15:12.557+09:00 TRACE 16560 --- [nio-8080-exec-1] org.hibernate.orm.jdbc.bind : binding parameter (1:BIGINT) <- [10]
AOP END!!
StopWatch를 사용해 메서드 수행시간을 측정
@Target(AnnotationTarget.FUNCTION) // 어노테이션이 적용될 대상을 의미
@Retention(AnnotationRetention.RUNTIME) // 이 어노테이션이 어느 시점까지 사용될 수 있는지 여부(default: RUNTIME - Kotlin에서)
annotation class StopWatch()
@Aspect // Spring Bean에만 AOP 적용 가능
@Component // Bean으로 등록
class StopWatchAspect {
private val logger = LoggerFactory.getLogger("Execution Time Logger") // org.slf4j.LoggerFactory
@Around("@annotation(org.zerock.springdemo.infra.aop.StopWatch)")
fun run(joinPoint: ProceedingJoinPoint) {
val stopWatch = StopWatch() // org.springframework.util.StopWatch
stopWatch.start()
joinPoint.proceed()
stopWatch.stop()
// method name
val methodName = joinPoint.signature.name
// Array 형태로 어떤 Argument들이 들어왔는지 확인할 수 있음
val methodArguments = joinPoint.args
// start와 stop 사이에 시간이 얼마나 지났는지를 표기(ms)
val timeElapsedMs = stopWatch.totalTimeMillis
logger.info("Method Name: {} | Arguments: {} | Execution Time: {}ms", methodName, methodArguments.joinToString(", "), timeElapsedMs)
}
}
수행시간을 측정할 메서드 위에 @StopWatch 어노테이션을 추가
@StopWatch
override fun getTodoById(todoId: Long): TodoResponse {
val todo = todoRepository.findByIdOrNull(todoId) ?: throw ModelNotFoundException("Todo", todoId)
return todo.toResponse()
}
메서드 실행 결과
2024-06-04T14:15:12.566+09:00 INFO 16560 --- [nio-8080-exec-1] Execution Time Logger : Method Name: getTodoById | Arguments: 10 | Execution Time: 121ms
참고자료
https://docs.spring.io/spring-framework/reference/core/aop.html
'TIL(Today I Learned)' 카테고리의 다른 글
TIL - 서버에서 보낸 쿠키가 브라우저에 저장되지 않는 문제 (0) | 2024.06.07 |
---|---|
TIL - React에서 useState를 사용할 때 호출이 여러 번 발생 (0) | 2024.06.05 |
TIL - React, Spring Boot로 카카오 소셜 로그인 구현 STEP 3 (0) | 2024.06.03 |
TIL - Redis Key값에 특수 문자(이상한 문자)가 같이 들어가는 경우 (0) | 2024.05.31 |
TIL - React, Spring Boot로 카카오 소셜 로그인 구현 STEP 2 (0) | 2024.05.30 |