mingg IT

[Spring] AOP 개념 정리 (ver 2022년) 본문

BackEnd

[Spring] AOP 개념 정리 (ver 2022년)

mingg123 2021. 12. 29. 12:33

AOP(Aspect Oriented Programming) 의 약자임. 관점 지향 프로그래밍이라고 부름.

어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것.

핵심적인 관점은 우리가 적용하는 비지니스 로직.

부가적인 관점은 이러한 비지니스로직을 수행하기위해 행해지는 데이터베이스 연결, 로깅, 파일 입출력을 예로 들 수 있음.

Aspect로 모듈화하고 핵심적인 비지니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지임.

AOP 주요 용어

  • Aspect : 모듈화 한것. 주로 부가 기능을 모듈화 한다.
  • Target : Aspect를 적용하는 곳 (ex 클래스, 메서드)
  • Advice : 실질적으로 어떤 일을 해야할지. 실질적인 부가기능을 담은 구현체. 부가기능의 로직
  • JointPoint : Advice가 적용될 위치, 끼어들 수 있는 지점, 메서드 진입 지점, 생성자 호출 지점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용 가능
  • PointCut : JointPoint의 상세한 스펙을 정의한것. (A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음)

크게 로그를찍는 예제와 Timer를 사용하는 예제 두가지를 알아보려고 한다.

로그를 찍는 예제

  • AOP를 사용하지 않았을 경우 예제

RestApiController.java

@RestController
@RequestMapping("/aoptest")
public class RestApiController {

    @GetMapping("/get/{id}")
    public void get(@PathVariable Long id,
            @RequestParam String name) {
        System.out.println("get method");
        System.out.println("get method " + id);
        System.out.println("get method " + name);
    }

    @PostMapping("/post")
    public void post(@RequestBody User user) {
        System.out.println("post method : " + user);
    }
}

User.java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private Long id;
    private String name;
    private String email;
}
  • Get request를 날렸을 경우
  • Post Request를 날렸을 경우

현재 Get, Post 의 메소드마다 System.out.print를 이용하여 다 로그를 찍었다. 허나 이 메소드가 2개가 아닌 100개 200개라면 모든 메소드마다 로그를 찍을 순 없다.

이때 AOP를 이용하여 로그를 찍는 기능을 하나로 모듈화 하여 사용할 수 있다.

  • AOP를 사용한 예제

User.java

@GetMapping("/get/{id}")
    public String get(@PathVariable Long id,
            @RequestParam String name) {
        System.out.println("get method");
        // System.out.println("get method " + id);
        // System.out.println("get method " + name);
        return id + " " + name;
    }

    @PostMapping("/post")
    public User post(@RequestBody User user) {
        System.out.println("post method : " + user);
        return user;
    }

우선 User에 있던 System.out.println을 모두 주석처리 해준다. get 은 id와 이름을 return하도록 수정하고 post 는 user를 return 하도록 수정한다.

ParameterAop.java

package com.example.AOP.aop;

import java.lang.reflect.Method;

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;

@Aspect
@Component
public class ParameterAop {
    @Pointcut("execution(* com.example.AOP..*.*(..))")

    private void cut() {

    }

    // cut이 해당되는 시점 이전에 수행하겠음.
    @Before("cut()")
    public void before(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        System.out.println("method type :" + method);
        for (Object obj : args) {
            System.out.println("type : " + obj.getClass().getSimpleName());
            System.out.println("value :" + obj);
        }
    }

    @AfterReturning(value = "cut()", returning = "returnObj")
    public void afterReturn(JoinPoint joinPoint, Object returnObj) {
        System.out.println("returnObj : " + returnObj);
    }
}

AOP Aspect, Component annotation을 사용한다.

@PointCut을 사용하여 AOP를 적용할 위치를 설정한다.

@Before 은 해당 메소드를 실행하기 이전에 수행한다.

MethodSignature를 사용하면 method의 type을 알 수 있다.

@AfterReturning 은 메소드 실행 이후 return된 object에 접근할 수 있다.

  • GET 수행 시

  • POST 수행 시

결과를 확인해보면 method type으로 해당 컨트롤러에서 사용했던 method의 type이 출력되고

type, value 를 출력해준다.

이후 각 메소드에서 System.out으로 찍었던 get method나 post method가 찍힘으로써 해당 메소드가 호출되기 전에 수행됨을 확인할 수 있다.

마지막에 returnObj를 통해 AOP 의 @AfterReturning가 정상 동작함을 확인할 수 있다.

내가 외부에서 어떤 요청이 들어왔는지, 어떤 메소드를 수행했는지, 어떤 값을 return했는지 알 수 있기 때문에 디버깅을 통해 따라가면서 어디서 부터 잘못 되었는지 편하게 확인할 수 있다.


Timer를 사용하는 예제

  • AOP를 사용하지 않는 경우

RestApiController.java

@PostMapping("/post")
    public User post(@RequestBody User user) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        System.out.println("post method : " + user);
        Thread.sleep(1000 * 2);
        System.out.println("total time : " + stopWatch.getTotalTimeSeconds());
        return user;
    }

    
    @DeleteMapping("/delete")
    public void delete() throws InterruptedException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        stopWatch.stop();

        Thread.sleep(1000 * 2);
        System.out.println("total time : " + stopWatch.getTotalTimeSeconds());
    }

시간을 측정하는 StopWatch를 메소드마다 만들어주고 실행 해주어야한다.

비지니스로직과는 전혀 상관없는 부가로직이 메소드마다 들어가있다.

이를 해결하기위해 사용하는 것이 AOP임. 즉 비지니스로직과 부가로직을 따로 빼서 관리하는것.

  • AOP를 사용할 경우

우선 시간을 측정해주는 custom annotation을 하나 만든다.

Timer.java

package com.example.AOP.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

}

TimerAop.java

@Aspect
@Component
public class TimerAop {
    @Pointcut("execution(* com.example.AOP..*.*(..))")
    private void cut() {

    }

    @Pointcut("@annotation(com.example.AOP.annotation.Timer)")
    private void enableTimer() {

    }

    @Around("cut() && enableTimer()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Object result = joinPoint.proceed();
        stopWatch.stop();

        System.out.println("total time : " + stopWatch.getTotalTimeSeconds());
    }
}

cut 메소드와, enableTimer 메소드 두가지를 사용한다.

@Around 를 이용해서 cut, enableTimer가 수행될 경우 Time을 측정 할 수 있도록 설정했고

내부에 Time을 측정할 수 있는 로직을 작성한다.

RestApiController.java

		@Timer
    @DeleteMapping("/delete")
    public void delete() throws InterruptedException {

        Thread.sleep(1000 * 2);

    }

delete 메소드에서 만들었던 Timer annotation을 사용한다.

  • delete 메서드 호출

delete method를 수행했을 경우, cut(), enableTimer() 둘다 수행되었기 때문에 시간이 측정되고, method Type도 보여주게 된다.

만약 @Timer를 사용용하지 않은 post를 호출하게되면 어떻게 될까 ?

  • post 메서드 호출

@Timer 를 사용하지 않았기 때문에 cut() 만 작동하여 method Type, value, returnObj등이 출력됨을 확인할 수 있다.

Timer같은 경우에는 메서드의 시간을 측정할 수 있기 때문에 외부와 통신을 하거나, DB를 사용하여 시간이 얼마나 걸렸는지 필요할 경우에 사용하면 된다.

@Timer를 사용해서 측정한 시간을 DB에 저장하거나, 모니터링 하는데 사용할 수 있다. 혹은 서버 안에서 5초안에 끝내야하는 서비스인데 5초가 넘어갈 경우 서버관리자에게 알려주거나 이런 용도로 쓸 수 있다.

로그를 사용한 예제, Timer를 사용한 예제로 공부를 해보았는데 결론은 AOP는 비지니스 로직과는 전혀 상관없는 것을 한곳에 모아서 모듈화 하기 위해 사용한다.

어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것.

핵심적인 관점은 우리가 적용하는 비지니스 로직.

부가적인 관점은 이러한 비지니스로직을 수행하기위해 행해지는 데이터베이스 연결, 로깅, 파일 입출력을 예로 들 수 있음.

Aspect로 모듈화하고 핵심적인 비지니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지임.

 

Comments