본문 바로가기
카테고리 없음

[Spring Boot] Spring AOP ( @Aspect )

by dop 2021. 4. 20.

AOP (Aspect Oriented Programming)

프로젝트 진행중에 GetMapping에 소요되는 시간을 알고싶다면 어떻게 해야할까? 몇십, 몇백개의 GetMapping 메소드가 있다면, 각각의 메소드에 동일한 코드를 작성해야 된다. 뭐...어찌어찌 해서 다 작성했다고 하자, 그런데 추가로 메소드 명도 같이 출력하려면 또 같은작업을 수십 수백번 반복해야한다.

AOP를 이용하면 이러한 불편함을 해소할 수 있다. AOP는 핵심 비즈니스로직과 공통 관심사를 '관점'을 기준으로 분리하여 모듈화하여 재사용성을 높이고 개발효율을 증대시킬 수 있다.

@Aspect

  • Gradle : implementation 'org.springframework.boot:spring-boot-starter-aop' 의존성 추가

필자는 AOP를 위한 클래스만 모아서 관리하기 위해, aop라는 패키지를 만들어 AopTimeLogger라는 클래스를 하나 생성했다.

@Aspect
@Component
public class AopTimeLogger {
    private final Logger logger = LoggerFactory.getLogger(getClass());
    private long timer= 0L;
    //내부 구현 코드
}

@Aspect, @Component 어노테이션을 추가해 Aspect라는걸 나타내고 Spring Bean으로 등록하기 위함이다. 이후에 설명할 내용들이 내부 구현 코드 부분에 들어간다고 보면 된다.

@Pointcut, @Before ,@AfterReturning

@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void GetMapping() {
}

@Pointcut 어노테이션은 Aspect를 어디에 적용할지 정하는 기능을 담당한다. 위와같이 어노테이션에 적용할 수도 있고, execution을 이용해 패키지, 클래스 단위로 설정도 가능하다. (단 Pointcut 메소드는 다른 곳에서 호출해야하기 때문에 public으로 작성해야 한다. )

@Before("GetMapping()")
public void before(JoinPoint joinPoint) {
    timer = System.currentTimeMillis();
}

@AfterReturning(pointcut = "GetMapping()", returning = "result")
public void afterGet(JoinPoint joinPoint, Object result) {
    long runtime = System.currentTimeMillis() - timer;
    String className = joinPoint.getTarget().toString();
    String methodName = joinPoint.getSignature().getName();
    logger.info(className);
    logger.info(methodName + " Running Time : " + runtime);
}

Before, AfterReturning은 이름과 같이 핵심로직 실행 전,후에 실행된다. @Pointcut이 적용된 메소드를 어노테이션의 매개변수로 담아 매핑시켜준다. JoinPoint 객체는 적용된 메소드의 정보를 담고있고, getTarget()은 클래스명, getSignature().getName()은 메소드명을 반환한다.
@AfterReturning에는 추가로 returning 변수가 존재하는데 적용된 메소드가 반환한 값을 받을 변수명을 작성해준다. (어떤 타입이 반환되는지 정해지지 않았기에 Object 타입으로 넣어주도록 하자)

@Around

Before과 After로 나누지 않고 하나의 @Around 어노테이션으로 전,후처리를 할 수 있다.

@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void PostMapping() {
}

@Around("PostMapping()")
public Object AroundPost(ProceedingJoinPoint joinPoint) throws Throwable {
    timer = System.currentTimeMillis();
    try {
        return joinPoint.proceed();
    } finally {
        long runtime = System.currentTimeMillis() - timer;
        String className = joinPoint.getTarget().toString();
        String methodName = joinPoint.getSignature().getName();
        logger.info(className);
        logger.info(methodName + " Running Time : " + runtime);
    }
}

가장 눈에띄는 변화는 예외처리 구문과 메소드의 매개변수가 JoinPoint가 아니라 ProceedingJoinPoint인 점이다. 메소드 내부에서 핵심로직 수행 시점을 결정해야하고, joinPoint.proceed() 가 수행시점이 된다. proceed메소드를 넣지않으면 정상적으로 실행되지 않으니 주의하도록 하자.

* execution 관련 내용

  • 접근제한자, 리턴타입, 인자타입, 클래스/인터페이스, 메소드명, 파라미터타입, 예외타입 등을 전부 조합가능한 가장 세심한 지정자
  • 풀패키지에 메소드명까지 직접 지정할 수도 있으며, 아래와 같이 특정 타입내의 모든 메소드를 지정할 수도 있다.
  • ex) execution(* com.blogcode.service.AccountService.*(..) : AccountService 인터페이스의 모든 메소드

excution과 annotation외에 다른 방식이 많지만 아직은 이 2가지로 충분할 것 같다.

이미지 출처 : https://jojoldu.tistory.com/71

728x90