본문 바로가기
  • 시 쓰는 개발자
Spring

Aspect Oriented Programming

by poetDeveloper 2024. 3. 4.

백기선님의 스프링 강의를 듣고 작성하였습니다. 내용이 어려워서 종종 내용 확인해보고 틀린 부분은 보완하겠습니다.

 

스프링 핵심 3요소(Spring Triangle) 中  "관점 지향 프로그래밍", AOP(Aspect Oriented Programming)에 대해서 알아보자.

 

AOP : 공통적인 일을 분리한다.

AOP에서 말하는 Aspect는 측면, 관점이라기 보다는 "여러 객체에 공통으로 적용되는 공통 관심사항"이다. 따라서 이런 공통 관심사항을 고려하며 프로그래밍하는 것이 바로 AOP이며, 더 정확히는 이런 공통되는 일을 분리하여 효율성을 높이는 방식을 의미한다.

만약 A작업 → 메세지 출력 → B작업 이런 flow가 있다고 하자. 이 구조가 여러 곳에서 쓰인다면 메세지 출력 부분만 다르고 A, B는 반복되는 다소 낭비되는 구조일 것이다. 그렇기에 저런 구조를 가진 메소드를 만들어놓고 가져다 쓰는 것이 Aspect oriented programming이다. 아래 예시를 보자.

우리가 어떤 메소드의 성능을 측정하고 싶을 때, StopWatch()를 사용할 수 있다. 생각해보면 메소드 시작할 때 스톱워치를 start 하고 끝날 때 end하고, 이때의 시간을 마지막에 print하면 되는 아주 단순한 구조이다. 근데 성능 측정이 필요한 모든 메소드에서 같은 구조를 보일 것이다. 시간 측정의 시작과 끝이 메소드의 시작과 끝에 정해져있기 때문이다.

public String TestController(Test test){

	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
    
	[ 메소드 작업 진행 ]
    
	stopWatch.stop();    
	System.out.println(stopWatch.prettyPrint());
    
	return "testCode";
}

그런데 성능 측정이 필요한 모든 부분에서 이런 구조를 수작업으로 넣게 되면 말도 안되는 수고이고, 이것은 AOP가 아니다. AOP는 stopWatch 코드가 없어도 저 기능을 수행할 수 있어야한다.

 

AOP를 적용하는 방법

AOP를 자바에서 쓰기 위한 구현체가 있는데, 그게 바로 AspectJ이다. 사실상 자바 표준이라고 한다. 아래에서 AOP를 구현하는 3가지 방법에 대해 알아보자.

 

1. 컴파일을 이용

예를 들어 test.java를 컴파일하면 test.class가 생기는데, 이 컴파일 과정 중간에 AOP를 적용해 실제 test.class 결과물에는 시간 측정이 되어있는 것이다.

EX) test.java ---- AOP ---- test.class

 

2. ByteCode 조작

test.class를 "메모리에 올릴 때" AOP를 적용하는 것이다. 그래서 코드상에도 stopWatch 코드가 없고, 컴파일 결과인 test.class에서도 stopWatch를 찾아볼 수 없지만 메모리에 로딩할 때 ByteCode를 조작해서 stopWatch를 넣어주는 것이다. 그래서 메모리상의 test.class와 내 컴퓨터상에 있는 test.class는 서로 다른 파일이 된다.

 

3. 프록시 패턴 적용

스프링 AOP가 사용하는 방법이다. 디자인 패턴 중 하나를 사용해서 AOP를 사용하는 방법이다. 이를 참고하자.

https://refactoring.guru/design-patterns/proxy

 

Proxy

There are dozens of ways to utilize the Proxy pattern. Let’s go over the most popular uses. Access control (protection proxy). This is when you want only specific clients to be able to use the service object; for instance, when your objects are crucial p

refactoring.guru

프록시란 중개 역할을 하는 서버라고 생각하면 된다.

 

  이 사진을 보고 프록시 패턴을 이해해보자. 이 사진에서 프록시는 클라이언트와 서버 사이의 중개 서버인데, AOP를 적용할 땐 중개 서버라기 보다는 기존 코드와 결과를 보여주는 코드 사이에 있는 "프록시 코드"정도로 이해하면 될듯하다. 즉, 기존 코드를 유지하면서 프록시 코드를 중간에 적용해 우리가 원하는 결과(ex. 메소드 성능 측정)를 보일 수 있게 해준다.

  예를 들어보자. 원래 Pay test = new pay(); 라고 되어 있는 것을, Pay test = new payTime();와 같이 payTime을 쓰도록 바꾸는 것이 프록시 코드를 사용하는 것인데, 새로운 시간측정 프록시 코드가 추가되기는 했지만 중요한 것은 "기존 코드를 건드리지 않고" 시간이 측정되도록 결과를 냈다는 것이다.

 

AOP 구현하기

실제 AOP가 어떻게 적용되는지 보자. 여기서는 @Aspect를 활용한 AOP를 사용할 것이다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}

이러한 어노테이션을 우리가 만들었다 치고, 이 어노테이션이 붙은 메소드가 바로 AOP적용되는 메소드이다. 다만 여기까지만 쓰면 이 어노테이션은 아무 일을 하지 못한다. 우리는 이러한 어노테이션이 붙어있는 곳에서 어떤 일이 벌어져야 하는지 명시해줘야한다. 그 작업이 아래 코드이다.

@Component
@Aspect
public class LogAspect {
    Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Around("@annotation(LogExecutionTime)")
        public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Object proceed = joinPoint.proceed();

        stopWatch.stop();
        logger.info(stopWatch.prettyPrint());

        return;
	}
}

@Around 어노테이션을 쓰면 joinPoint라는 파라미터로 받을 수 있는데, 이 joinPoint가 바로 "타겟 메소드"이다. 즉, 성능 측정이 필요한 메소드가 저기로 들어오게 된다. 이러한 코드를 Advice라고 하는데, 실제로 동작하는 코드를 말한다. 위 코드에서 @Around를 보면 "LogExecutionTime"이라고 써져있는데 @LogExecutionTime가 붙어있는 모든 메소드를 joinPoint라는 인터페이스 타입으로 들어와서 실행(joinPoint.proceed())시키겠다는 의미이다. 즉 @LogExecutionTime이 붙은 모든 메소드에서 앞뒤로 stopWatch를 붙이는 수작업 없이도 성능 측정을 할 수 있다. 위 코드상에서 실행시키고 return도 하는데, 다만 그 앞뒤로 시간을 측정하는 부분이 있기 때문에 이것이 우리가 원하는 AOP코드라고 할 수 있다. 이 방식이 스프링에서 제공하는 프록시 패턴의 AOP이다.

 

추가) 만약 시간을 측정해야하는 메소드가 1000개라면 모든 메소드에 @LogExecutionTime을 붙이기는 어려울 것이다. 그럴 때는 @Around 부분을 이렇게 바꿔준다.

@Around("execution(* hello.hellospring..*(..))")

이렇게 바꿔주면 패키지중에서 hello.hellospring 하위에 있는 모든 곳에 적용시켜주겠다는 의미이다. 또한, 점이나 별로 표시된 부분에 클래스명, 파라미터 등을 세부 조작 할 수 있다.