Spring AOP

용어 정리

Aspect

여러 곳에서 쓰이는 코드(공통 부분)를 모듈화한 것
Advice + JoinPoint 로 이루어진다.

Target

Aspect가 적용되는 곳

Advice

Aspect에서 실질적인 기능을 구현한 구현체

JoinPoint

Advice가 Target 에 적용되는 시점
메서드 진입할 때, 생성자 호출할 때, 필드에서 값을 꺼낼 때 등등

PointCut

Joint point 의 상세 스펙을 정의한 것

Proxy

타겟을 감싸서 타겟의 요청을 대신 받아주는 랩핑 오브젝트이다. 클라이언트가 타겟을 호출하게 되면 프록시가 호출을 가로채 어드바이스에 등록된 기능을 수행 후 타겟 메소드를 호출한다.

예제

의존성 추가

implementation("org.springframework.boot:spring-boot-starter-aop")

기본 예시

@Aspect
@Component
class Performance {
    
    @Pointcut("execution(* com.board.BoardService.getBoards(..))")
    fun getBoards(){}

    @Pointcut("execution(* com.user.UserService.getUsers(..))")
    fun getUsers(){}

    @Around("getBoards() || getUsers()")
    fun calculatePerformance(proceedingJoinPoint: ProceedingJoinPoint): Any {
        val start = System.currentTimeMillis()
        val result = proceedingJoinPoint.proceed()
        val end = System.currentTimeMillis()
        
        println("수행 시간 : "+ (end - start))
        return result
    }
}

Aspect에서 타겟 메서드의 파라미터 사용

@Aspect
@Component
class UserHistory(
    private val historyRepository: HistoryRepository
) {

    @Pointcut("execution(* com.user.UserService.update(*)) && args(user)")
    fun updateUser(user: User){}

    @AfterReturning("updateUser(user)")
    fun saveHistory(user: User){
        historyRepository.save(History(user.getIdx()))
    }
}

Annotaion 기반으로 Aspect 적용

애노테이션 생성

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
annotation class PerfLogging

Aspect클래스 포인트컷 변경

@Component
@Aspect
class PerfAspect {

    @Around("@annotation(PerfLogging)")
    fun logPerf(pjp: ProceedingJoinPoint): Any {
        val begin = System.currentTimeMillis()
        val result = pjp.proceed()
        println(System.currentTimeMillis() - begin)
        return result
    }
}

Aspect 적용

@Component
class SimpleServiceEvent {

    @PerfLogging
    fun created() {
    ...
    }
}

주의사항

동일한 클래스의 내부에서 Aspect가 적용된 메서드를 호출할 때는 적용되지 않는다. 따라서 별도의 클래스로 분리해야 한다.

@Component
class SimpleServiceEvent {

   // 아래 메서드를 실행할 경우 Aspect가 적용되지 않음
   fun callCreated() {
        this.creaeted()
    }

    @PerfLogging
    fun created() {
    ...
    }
}

클래스를 분리한다.

@Component
class SimpleServiceEvent(
    private val internalSimpleServiceEvent: InternalSimpleServiceEvent
) {

    fun callCreated() {
        internalSimpleServiceEvent.creaeted()
    }
}

@Component
class InternalSimpleServiceEvent {

    @PerfLogging
    fun created() {
    ...
    }
}

아래 방법 처럼 자신을 주입 받아서 처리할수 있다. (의존성 참조 순환 오류가 발생하는 경우도 있는것 같다)

@Service
class SimpleService {

    @Autowired
    private lateinit var simpleService: SimpleService

    fun outsideMethod(): String {
        return simpleService.get()
    }

    @PerfLogging
    fun get(): String {
    }
}

reference