# Table of Contents

# AOP

AOP(Aspect Oriented Programming)은 공통 기능을 추출하여 별도의 모듈로 만드는 것이다. 이를 통해 모든 컴포넌트에 산재했는 공통 기능을 비즈니스 코드와 분리하여 관리할 수 있다. AOP는 주로 로깅, 트랜잭션, 예외처리, 보안 등의 공통 기능을 뽑아내는데 사용한다.

# 의존성 추가

AOP를 사용하려면 다음 의존성을 추가해야한다.

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

# @Aspect

회원가입 및 인증을 담당하는 AuthService가 있다고 가정하자.

package com.yologger.samples.example;

@Service
public class AuthService {

    public void logout() {
        System.out.println("logout()");
    }

    public void join(String email, String password) {
        System.out.println("join()");
    }
    public void login(String email, String password) {
        System.out.println("login()");
    }
}

이제 AuthService의 메소드가 호출될 때 로그가 출력되도록 AOP를 구현해보자.

공통 기능을 정의할 클래스에는 @Aspect 어노테이션을 추가한다.

package com.yologger.samples.example;

import org.aspectj.lang.annotation.Aspect;

@Aspect
@Component
public class AuthAspect {
    // ..
}

# @PointCut

@PointCut 어노테이션으로 공통 기능을 적용할 타겟을 지정할 수 있다.

예를 들어 AuthService.logout()메소드는 다음과 같이 타겟팅할 수 있다.

@Aspect
@Component
public class AuthAspect {

    @Pointcut("execution(void com.yologger.samples.example.AuthService.logout())")
    public void targetLogout() {
    }
}

타겟 클래스가 Asepect 클래스와 같은 패키지에 있다면 패키지명을 생략할 수 있다.

@Aspect
@Component
public class AuthAspect {

    @Pointcut("execution(void AuthService.logout())")
    public void targetLogout() {
    }
}

AuthService.join()메소드는 두 개의 String 타입 인자를 가진다. 이 메소드는 다음과 같이 포인트컷을 설정할 수 있다.

@Aspect
@Component
public class AuthAspect {

    // ...

    @Pointcut("execution(void AuthService.join(String, String))")
    public void targetJoin() {

    }
}

모든 인자를 다 커버하려면 ..를 사용하면 된다.

@Aspect
@Component
public class AuthAspect {

    // ...

    @Pointcut("execution(void AuthService.join(..))")
    public void targetJoin() {

    }
}

AuthService클래스의 모든 메소드를 다 커버하려면 와일드카드(*)를 사용하면 된다.

@Aspect
@Component
public class AuthAspect {

    // ...

    @Pointcut("execution(void AuthService.*(..))")
    public void targetAll() {

    }
}

반환값 타입에 관계없이 메소드를 커버하려면 반환값 타입에도 와일드카드를 사용하면 된다.

@Aspect
@Component
public class AuthAspect {

    // ...

    @Pointcut("execution(* AuthService.*(..))")
    public void targetAll() {

    }
}

추가적으로 접근제한자도 제한할 수 있다.

@Aspect
@Component
public class AuthAspect {

    // ...

    @Pointcut("execution(public * AuthService.*(..))")
    public void targetAll() {

    }
}

# Advice

포인트컷으로 지정한 타겟이 호출될 때 실행할 기능을 어드바이스(Advice)라고 한다. 어드바이스는 다섯가지 어노테이션으로 지정할 수 있다.

  • @Before
  • @After
  • @AfterReturning
  • @AfterThrowing
  • @Around

# @Before

@Before을 붙인 메소드는 타겟이 실행되기 전 호출된다.

@Aspect
@Component
public class AuthAspect {

    @Pointcut("execution(void AuthService.logout())")
    public void targetLogout() {

    }

    @Before("targetJoin()")
    public void beforeLogout() {
        System.out.println("Log: Before logout.");
    }
}

# @After

@After를 붙인 메소드는 타겟이 실행된 후 호출된다.

@Aspect
@Component
public class AuthAspect {

    @Pointcut("execution(void AuthService.logout())")
    public void targetLogout() {

    }

    @After("targetJoin()")
    public void AfterLogout() {
        System.out.println("Log: After logout.");
    }
}

타겟은 다른 말로 조인포인트(JoinPoint)라고도 한다. 조인포인트의 상세 정보에 접근하려면 JoinPoint 타입의 인자를 어드바이스 메서드에 선언하면 된다.

@Aspect
@Component
public class AuthAspect {

    @Pointcut("execution(void AuthService.logout())")
    public void targetLogout() {

    }

    @After("targetJoin()")
    public void AfterLogout(JoinPoint joinPoint) {
        System.out.println("Method name: " + joinPoint.getName());
        System.out.println("Parameters: " + Arrays.toString(joinPoint.getArgs()));
    }
}

# @AfterThrowing

@AfterThrowing를 붙인 메소드는 타겟이 실행되는 과정에서 예외가 발생하면 호출된다.

@Aspect
@Component
public class AuthAspect {

    @Pointcut("execution(void AuthService.logout())")
    public void targetLogout() {

    }

    @AfterThrowing("targetLogout()")
    public void AfterThrowingLogout() {
        System.out.println("Log: AfterThrowing logout.");
    }
}

타겟에서 발생한 예외는 다음과 같이 전달받을 수 있다.

@Aspect
@Component
public class AuthAspect {

    @Pointcut("execution(void AuthService.logout())")
    public void targetLogout() {

    }

    @AfterThrowing(pointcut = "targetLogout()", throwing = "exception")
    public void AfterThrowingLogout(Throwable exception) {
        System.out.println("Log: AfterReturning logout.");
    }
}

# @AfterReturning

@AfterReturning를 붙인 메소드는 타겟이 정상적으로 실행되고 값을 반환한 후 호출된다.

@Aspect
@Component
public class AuthAspect {

    @Pointcut("execution(void AuthService.logout())")
    public void targetLogout() {

    }

    @AfterReturning("targeLogout()")
    public void AfterReturningLogout() {
        System.out.println("Log: AfterReturning logout.");
    }
}

타겟에서 값을 반환한다면 어드바이스 메소드에서 다음과 같이 전달받을 수 있다.

@Aspect
@Component
public class AuthAspect {

    // ...

    @AfterReturning(pointcut = "targetRefreshToken()", returning = "result")
    public void AfterReturningRefreshToken(object result) {
        System.out.println("Result: " + result);
    }
}

# @Around

@Around를 사용하면 더욱 세밀한 제어가 가능하다. 예를 들어 타겟 메소드를 실행 순서, 실행 시점, 실행 여부 등을 제어할 수 있다. @Around 어드바이스 메소드는 ProceedingJoinPoint 타입의 인자를 전달받는데 이 객체로 타겟 메소드의 실행 순서, 실행시점, 실행 여부 등을 제어한다.

@Aspect
@Component
public class AuthAspect {

    @Pointcut("execution(boolean AuthService.logout())")
    public void targetLogout() {

    }

    @Around("targetLogout()")
    public void aroundLogout(ProceedingJoinPoint proceedingJoinPoint) {
        try {
            // 타겟 메소드를 실행한 후 결과값을 반환받는다.
            Object result = proceedingJoinPoint.proceed();
        } catch (Throwable e) {
            // 타겟 메소드 실행 도중 예외 발생
            throw new RuntimeException(e);
        }
    }
}