본문 바로가기
Spring|Spring-boot/Spring AOP

[Spring boot] AOP 활용예제

by oncerun 2021. 6. 9.
반응형

2022.01.27 - [Spring|Spring-boot/Spring AOP] - Spring AOP

 

Spring AOP

사전 지식 빈 후처리기 @Aspect Aspect 기억 되살리기 @Aspect를 어드바이저로 변환해서 저장하는 과정 1. 스프링 애플리케이션 로딩 시점에 자동 프락시 생성기(AnnotationAwareAspectJAutoProxyCreator) 호출 2.

chinggin.tistory.com

2022.01.29 - [Spring|Spring-boot/Spring AOP] - Spring AOP (2)

 

Spring AOP (2)

설정 사항 - org.aspectj 패키지 관련 기능은 aspectjweaver.jar 라이브러리가 제공하는 기능이다. 따라서 해당 라이브러리 의존관계의 추가가 필요하며 spring-boot인 경우 spring-boot-starter-aop를 포함하면 스

chinggin.tistory.com

 

 

AOP는 비즈니스 로직의 코드와 그 외 부가적인 관심을 코드상에서 분리하기 위해 나온 하나의 방법이다.

 

예를 들어 HTTP통신을 통해 들어온 responseBody를 들어올 땐 복호화를 나갈 땐 암호화를 해야 한다면 

모든 비즈니스 앞 뒤에 동일한 코드가 반복되어 사용될 것이다. 

 

또 각 메서드의 실행시간을 구한다던지, 혹은 들어온 파라미터와 실행된 메서드 이름, 그리고 리턴한 값 등을 로깅하기 위해서 aop를 사용하기도 한다.

 

 

첫 번째로 실행된 메서드의 이름과 들어온 파라미터의 값, 리턴 값을 로깅하고 추가적으로 메서드의 실행시간까지 로깅해 보자.

 

우선 단순 CRUD를 지원하는 API서버를 구동시켜 놓겠다. 

package com.example.aop.controller;


import com.example.aop.annotation.Timer;
import com.example.aop.dto.User;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class Controller {

    @GetMapping("/get/{id}")
    public String httpGetMethod(@PathVariable Long id, @RequestParam String name){
        return id + " " + name;
    }

    @PostMapping("/post")
    public User httpPostMethod(@RequestBody User user){
        return user;
    }

    @DeleteMapping("/delete")
    public void httpDeleteMethod() throws InterruptedException {
        Thread.sleep(2000);
    }

    @PutMapping("/put")
    public User httpPutMethod(@PathVariable Long id, @RequestBody User user){

        return user;
    }

}

 

 

이제 각 메서드의 정보에 접근할 수 있는 AOP 빈객체를 만들어야 한다. 당연히 Spring을 활용하기 때문에 AOP자체는 @Component로 활용해야 한다.

@Component
@Aspect
public class AopExample {
    
}

 

@Component 어노테이션은 클래스단위를 빈으로 등록하겠다는 어노테이션이다.

@Aspect는 Spring AOP를 위한 어노테이션이다.

 

Aspect를 사용하기 전 Pointcut으로  AOP가 실행되어야 하는 실행 시점을 정한다. Spring에서는 한정적인 범위에서의

Pointcut을 기본적으로 제공하지만 추가적으로 라이브러리를 사용하면  더 넓은 범위의 AOP를 활용할 수 있을 것이다.

2022.01.29 - [Spring|Spring-boot/Spring AOP] - Pointcut - execution

 

Pointcut - execution

AspectJ는 Pointcut을 편리하게 표현하기 위한 표현식을 별도로 제공한다. AspectJ pointcut expression이라고 부르며 간단하게 포인트 컷 표현식이라고 부른다. 이러한 포인트 컷 표현식에는 Pointcut Designator

chinggin.tistory.com

    @Pointcut("execution(* com.example.aop.controller.*.*(..))")
    public void pointCut(){}

pointcut의 표현식은 자료가 많이 때문에 추가적인 설명은 하지 않고 controller패키지 하위 모든 접근자, 모든 타입, 파라미터의 유무와 상관없이 모든 메서드에 pointcut을 걸었다.

 

이 pointCut()은 메서드의 정보를 가져오기 위한 포인트 컷이고 다음은 시간을 재기 위한 포인트 컷을 만들 것이다.

시간을 측정하기 위한 포인트 컷은 Custom Annotation을 활용해 볼 생각이다.

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}

 

별도의 커스텀 어노테이션을 만들어 사용해 본다.

 

@Target은 해당 어노테이션을 활용할 지점을 정하는데 TYPE 경우 종류가 많이 때문에 검색을 추천한다.

@Retention 어노테이션은 해당 어노테이션의 생명 주기를 정한다.

 

이제 해당 어노테이션으로 포인트 컷(어디에서 aop를 적용할지?)을 추가할 것이다. 

 

 

 

 

이제 적용시킬 메서드에 커스텀 어노테이션을 붙여보자.

package com.example.aop.controller;


import com.example.aop.annotation.Timer;
import com.example.aop.dto.User;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class Controller {

    @Timer
    @GetMapping("/get/{id}")
    public String httpGetMethod(@PathVariable Long id, @RequestParam String name){
        return id + " " + name;
    }

    @Timer
    @PostMapping("/post")
    public User httpPostMethod(@RequestBody User user){
        return user;
    }

    @Timer
    @DeleteMapping("/delete")
    public void httpDeleteMethod() throws InterruptedException {
        Thread.sleep(2000);
    }
    
   @Timer
    @PutMapping("/put/{id}")
    public User httpPutMethod(@PathVariable Long id, @RequestBody User user){

        return user;
    }
}

 

 

이제 메서드는 정했기 때문에 메서드 전, 후, 또는 앞 뒤, 에러 포함 , 미포함을 고려해야 한다. 

 

연습을 위해 메소드 실행 전과 예외 없이 성공한 후, 예외 상관없이 앞 뒤로 발생하는 around를 적용할 것이다.

2022.01.29 - [Spring|Spring-boot/Spring AOP] - Spring AOP @target, @within

 

Spring AOP @target, @within

@target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트 @within : 주어진 애노테이션이 있는 타입 내 조인 포인트 이 둘은 사용자 정의 애노테이션으로 AOP 적용 여부를 판단

chinggin.tistory.com

 

이제 실행되는 메서드의 이름과 입력받은 파라미터, 리턴한 값을 로그로 찍어보자.

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.logging.Level;
import java.util.logging.Logger;
@Component
@Aspect
public class AopExample {

    private final static Logger LOG = Logger.getGlobal();

    @Pointcut("execution(* com.example.aop.controller.*.*(..))")
    public void pointCut(){}

    @Pointcut("@annotation(com.example.aop.annotation.Timer)")
    public void timerPointCut(){}


    @Before("pointCut()")
    public void before(JoinPoint joinPoint){
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        Object[] objects = joinPoint.getArgs();

        for(Object param: objects ){
            LOG.log(Level.INFO, "들어온 파라미터 값 : "+param );
        }
        LOG.log(Level.INFO, "실행된 Method Name : "+ method.getName() );
    }

    @AfterReturning(value = "pointCut()",returning = "returnValue")
    public void afterReturning(JoinPoint joinPoint,Object returnValue){
        LOG.log(Level.INFO, "실행된 메소드가 리턴한 값  : "+ returnValue );
    }


    


}

 

 

@Before는 예외와 상관없이 무조건 메서드 이전에 실행한다.

Postman을 사용해서 로컬 서버에 @RequsetParam 값으로 name = steve와  @PathVariable 값으로 100을 넣어주고 요청을 보냈다. 

 

 

들어온 파라미터와 실행된 메서드, 리턴한 값까지 확인할 수 있다. 원래 비즈니스 로직에서 이러한 공통된 작업을 처리하기 위해선 반복적으로 코드가 중복될 수밖에 없기 때문에 AOP는 관심사를 분리하여 코드를 분리했다.

 

이러한 AOP를 통해 로그를 찍어 TEST를 유용하게 진행할 수 있으며, 로그파일을 생성하거나, 혹은 실제 들어온 값을 복호화하여 내부 비즈니스 로직을 이용하고 값을 리턴할 때는 다시 암호화를 할 수도 있다. 

 

다음 호출 순서를  기억하자 aop를 적용하고 호출순서를 헷갈려해서 오류가 발생한 경우도 있다.

 

호출 순서(오류 없을 때)

  • Before
  • Around
  • After
  • AfterReturning

 

호출 순서(오류 났을 때)

  • Before
  • Around 에러 시 실행되지 않음
  • After
  • AfterReturning 에러 시 실행되지 않음
  • AfterThrowing

 

 

그럼 이제 추가적으로 어노테이션을 활용해서 포인트 컷을 걸고 두 개의 포인트 컷을 합쳐보자

이번에는 HTTP POST METHOD를 보낼 것인데, 미리 받을 @RequestBody DTO를 정의해 놨다.

 

package com.example.aop.dto;

public class User {

    private String id;
    private String pw;
    private String email;

    public void setId(String id) {
        this.id = id;
    }

    public void setPw(String pw) {
        this.pw = pw;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getId() {
        return id;
    }

    public String getPw() {
        return pw;
    }

    public String getEmail() {
        return email;
    }

    @Override
    public String toString() {
        return "User{" +
                "id='" + id + '\'' +
                ", pw='" + pw + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

 

 

스프링 부트는 기본적으로  Jackson라이브러리를 사용하기 때문에  디폴트 생성자와 getter는 필수로 생성해야 한다.

또한 return 값으로 객체가 반환되는 경우 JSON으로 변환된다. 그럼 JSON형식으로 요청을 보내고 RETURN값을 확인해 본다.

 

@Component
@Aspect
public class AopExample {
    private final static Logger LOG = Logger.getGlobal();
    
    @Pointcut("execution(* com.example.aop.controller.*.*(..))")
    public void pointCut(){}

    @Pointcut("@annotation(com.example.aop.annotation.Timer)")
    public void timerPointCut(){}

/*    @Before("pointCut()")
    public void before(JoinPoint joinPoint){}

    @AfterReturning(value = "pointCut()",returning = "returnValue")
    public void afterReturning(JoinPoint joinPoint,Object returnValue){}*/

    @Around("timerPointCut() && pointCut()")
    public void logAOP(ProceedingJoinPoint joinPoint){
        StopWatch stopWatch = new StopWatch();
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();

        Object[] objects = joinPoint.getArgs();

        for(Object param: objects ){
            LOG.log(Level.INFO, "들어온 파라미터 값 : "+param );
        }
        LOG.log(Level.INFO, "실행된 Method Name : "+ method.getName() );

        try {
            stopWatch.start();
            Object returnValue = joinPoint.proceed();
            LOG.log(Level.INFO, "Http Method Return Value  =  " + returnValue);
            stopWatch.stop();
            LOG.log(Level.INFO, "Around AOP 실행시간  =  " + stopWatch.getTotalTimeSeconds() );
        } catch (Throwable throwable) {
            LOG.log(Level.INFO, "Around AOP Error : Method nmae =  "+ method.getName() );
        }
    }
}

 

 

 

proceed() 메서드를 기준으로 메서드 전 후를 구별할 수 있기 때문에 다양한 용도로 많이 사용되는 어노테이션이다.

또한 포인트 컷을 연산자를 통해 중첩할 수 있다.

 

요청을 보내기 전에 httpPostMethod에 3초 정도 대기시간을 강제로 부여한다

 

 

2022.01.31 - [Spring|Spring-boot/Spring AOP] - Spring AOP 한계

반응형

'Spring|Spring-boot > Spring AOP' 카테고리의 다른 글

Pointcut - within, args  (0) 2022.01.29
Pointcut - execution  (0) 2022.01.29
Spring AOP (2)  (0) 2022.01.29
Spring AOP  (0) 2022.01.27
[Spring] AOP(Aspect Oriented Programming) 방법론  (0) 2020.06.22

댓글