[Spring] AOP(Aspect Oriented Programming)

HOJUN·2024년 6월 16일

Backend - Spring

목록 보기
28/34

AOP

Aspect Oriented Programming이란 관점(Aspect)에 따라서 기능을 분리하는 프로그래밍 기법이다.

OOP(Object Oriented Programming)와 반대되는 개념인 것 같지만, 부족한 면을 보완하기 위해서 만들어졌다.

목적과 기능에 따라 클래스와 객체를 분리해서 만든 OOP에서는 비즈니스와 서비스의 로직을 객체로 만들고
이를 부품으로 사용하는데 의의를 두고 있지만 어떤 관점에서 어떻게 사용할 것인지, 어떤 식으로 나눠서 사용할 것인지는 정의하지 않는다.

AOP는 애플리케이션에서 가장 중요한 비즈니스 로직이나 전역적인 로직, 혹은 특정 서비스에만 사용되는 로직을
관점에 따라서 사용하는 정의를 따로 할 수 있게한다.

공통된 로직을 적용한 부분을 횡단 관심사(Cross-Cutting Concerns)라고 하고 이를 모듈화해 하나의 단위로 사용하는 기능을 제공한다.

  • 컴파일 시점
    컴파일 시점에 적용되는 방식은 AspectJ컴파일러가 .class 파일로 컴파일 하기 전 부가기능을 추가해서 컴파일한다.
    이를 aspect와 code를 weaving한다고 한다.

  • 클래스 로딩 시점
    JVM에서 클래스 로더에 .class 파일을 올리는 과정에서 Byte Code를 조작해서 부가기능을 추가한다.

  • 런타임 시점
    애플리케이션이 실행되는 런타임 내에 부가기능을 추가하는 방식이다.
    런타임에는 코드 변경이 불가능하므로 프록시를 통해서 부가기능을 추가할 수 있다.
    메소드 실행 시점으로 부가기능 추가가 제한된다.

Spring에서는 런타임 시점에 AOP를 적용한다.

Annotation의미
@AspectAOP를 정의하는 Class에 할당
@PointCutAOP기능을 적용할 지점을 설정
@BeforeJoinPoint 실행 이전
@AfterJoinPoint 실행 이후
@AfterReturning메소드 실행 이후 호출 성공 시
@AfterThrowing메소드 실행 이후 예외 발생 시
@Around메소드 실행 전후
@Aspect
@Component
public class TimerAOP {
	@Pointcut(value = "within(com.example.filter.controller.UserApiController)")
    public void timerPointCut(){}
    
	@Around(value = "timerPointCut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {

        System.out.println("메소드 실행 이전");

        Arrays.stream(joinPoint.getArgs()).forEach(
                it -> {
                    System.out.println(it);
                }
        );

        var stopWatch = new StopWatch();
        stopWatch.start();

		joinPoint.proceed();

        stopWatch.stop();
        
        System.out.println("total = " + stopWatch.getTotalTimeMillis());
        System.out.println("메소드 실행 이후");
    }
}

Pointcut으로 어떤 클래스에 적용할 것인지 지정할 수 있다.
해당 클래스는 Spring의 관리 범위 내에 있는 Bean만 적용가능하다.

@Around를 통해서 timerPointCut 메소드 전후를 지정한다.
joinPoint에 존재하는 Argument를 모두 출력하고 해당 메소드가 실행된 시간을 측정해보자

Controller에서 메소드를 실행 전 이미 JoinPoint에 들어서서 실행 시간 측정을 시작했다.
joinPoint에 있는 inputStream의 Argument를 출력한 이후 시간을 출력하고 반환했다.

만약 해당 과정 내에서 joinPoint의 전화번호 중 ' - '을 제거하고 싶을 때,

Arrays.stream(joinPoint.getArgs()).forEach(
                it -> {
                    System.out.println(it);
                    if(it instanceof UserRequest){
                        var tempUser = (UserRequest) it;
                        var phoneNumber = tempUser.getPhoneNumber().replace("-", "");
                        tempUser.setPhoneNumber(phoneNumber);
                    }
                }
        );

replace 로직을 추가할 수 있고,

var newObj = Arrays.asList(new UserRequest());
joinPoint.proceed(newObj.toArray());

비어있는 UserRequest를 toArray로 바꿔서 내릴 수 있다.

응답으로 내려온 정보는 null로 보여지지만 서버에는 정상적으로 넘어온 것을 볼 수 있다.
해당 로직은 암, 복호화를 위해서 빈껍데기를 내리거나 로깅을 위해서 수행할 수 있다.

@Before(value = "timerPointCut()")
public void before(JoinPoint joinPoint){
    System.out.println("Before");
}

@After(value = "timerPointCut()")
public void after(JoinPoint joinPoint){
    System.out.println("After");
}

@AfterReturning(value = "timerPointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result){
    System.out.println("After Returning");
}

@AfterThrowing(value = "timerPointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Throwable e){
    System.out.println("After Throwing");
}

물론 다른 어노테이션도 확인할 수 있다.

Before, After, After Returning 메세지가 찍혀있고, 각 시점마다 차이를 확인할 수 있다.

요청시 Exception을 발생시켜보면 함수 호출 과정에서 예외를 감지했고
joinPoint.proceed로 메소드를 호출하고나서 예외가 터져 이후 메세지가 출력되지 않았다.

설명하지 않은 지시자는 다음 포스팅에서 진행하도록 하겠다..

0개의 댓글