관점 지향 프로그래밍(AOP)이란 객체 지향으로 독립적으로 분리하기 어려운 부가 기능을 모듈화하는 방식이다. 만약 특정 메소드들에 대해 보안 검사가 필요하다고 가정해보자. 클래스A, 클래스B, 클래스C 안의 메소드에 대해서 보안 검사가 필요하다고 하다면 객체 지향으로는 각 클래스 내에 보안 검사 메소드를 작성해야한다. 이는 코드의 중복을 야기하고 효율적이지 못하다.
관심사란 보안 검사와 같은 기능을 모듈화한 것이며 여러 클래스에 코드를 분산하는 대신 관심사를 다루는 로직을 하나의 Aspect(보안 검사)에 넣는다.
스프링 AOP에서는 일반 클래스에 애스펙트를 구현할 수 있고 Aspectj의 @Aspect 어노테이션을 사용하여 애스펙트를 정의한다.
implementation("org.springframework.boot:spring-boot-starter-aop")
스프링 AOP를 사용하기 위해서는 spring-boot-starter-aop를 디펜던시에 추가해야한다.
@Aspect
@Component
class SecurityChecker{
private val logger = LogManager.getLogger(SecurityChecker::class.java)
@Around("execution(* app.sample.messages..*.*(..))")
fun checkSecurity(joinPoint: ProceedingJoinPoint): Any{
logger.info("checking security before join point")
val result = joinPoint.proceed()
logger.info("checking security after join point")
return result
}
}
조인 포인트는 특정 관심사가 처리되는 지점이다. 위의 예시에서는 보안검사가 실행되는 지점을 뜻한다. 스프링 AOP에서 조인포인트의 대상은 메소드지만 다른 AOP 구현체에서는 필드 접근과 예외 발생에 대한 조인 포인트도 지원한다.
어드바이스는 특정 관심사를 처리하는 로직으로 위의 코드에서 checkSecurity 메소드에 해당한다. 어드바이스에는 여러 유형이 있다.
어라운드 어드바이스의 경우는 조인 포인트의 호출을 가로챈다. 즉 조인 포인트의 호출을 어라운드 어드바이스에 위임한다는 것이다.
어라운드 어드바이스의 파라미터 ProceedingJoinPoint는 스프링 컨테이너가 건네준 조인 포인트다. ProceedingJoinPoint.proceed() 메소드는 조인 포인트를 실행하고 조인 포인트의 반환값을 반환한다. proceed() 메소드 호출 시점 전의 코드는 조인포인트 전에 실행되고 호출 시점 후의 코드는 조인포인트 후에 실행되게 된다.
중요한 점은 어라운드 어드바이스의 반환값이 가로챈 조인 포인트 호출의 반환값이 된다는 것이다. 따라서 반드시 proceed() 메소드의 반환값을 어라운드 어드바이스에서 반환해주어야 한다.
포인트컷은 일치하는 여러 조인트 포인트를 정의한 식이다. 위의 예제에서 본 execution(* app.sample.messages..*.*(..)) 는 app.sample.messages 패키지 내에 있는 모든 클래스의 메소드를 조인포인트로 정의한 것이다.
@Pointcut 어노테이션으로 포인트컷의 시그니처를 선언할 수 있다.
@Pointcut("execution(* app.sample.messages..*.*(..))")
fun checkMethodSecurity() {}
@Around("checkMethodSecurity()")
fun checkSecurity(joinPoint: ProceedingJoinPoint): Any{ ... }
위의 포인트컷에서 excution은 스프링 AOP에 어떤 것을 매칭할지 알려주는 포인트컷 지시자(PCD)이다. 유용한 PCD로 @annotation이 있다. @annotation PCD를 사용하면 특정 어노테이션이 달려있는 메소드에 어드바이스를 실행할 수 있게된다.
// SecurityCheck.kt
@Target(AnnotationTarget.FUNCTION)
//@Retention(AnnotionRetention.RUNTIME) : 코틀린에서는 어노테이션의 Retention 시점이 RUNTIME이기 때문에 정의할 필요 없다.
annotation class SecurityCheck()
// SecurityChecker.kt => Aspect
@Pointcut("@annotation(app.sample.messages.aop.SecurityCheck)")
fun checkMethodSecurity() {}
@Around("checkMethodSecurity()")
fun checkSecurity(joinPoint: ProceedingJoinPoint): Any{
주의할 점은 @annotation의 인자로 넘기는 어노테이션은 같은 패키지내에 있을 경우 어노테이션 이름만 넘겨도 되지만 다른 패키지(하위 패키지도 포함)에 있을 경우 패키지 이름까지 명시해줘야 어노테이션을 인식한다는 것이다.
스프링 AOP는 어드바이스의 대상이 되는 조인 포인트에서의 어드바이스 실행을 위해 프록시 객체를 생성한다. 만약 어드바이스의 대상이 인터페이스를 구현하지 않는 프록시라면 CGLIB로 어드바이스를 대상 객체를 상속받아 프록시 객체를 만든다.