Spring 의 AOP

김종하·2020년 12월 5일
1

Spring boot booster

목록 보기
4/13
post-thumbnail


(사진출처)
이번 포스팅에서는 Spring 에서 AOP 를 활용하는 방법에 대해 알아보도록 하겠다.
우선 AOP(Aspect Oriented Programming) 에 대해 이해해보도록 하자
AOP란 관점지향 프로그래밍 기법이다. 여러 곳에서 사용되는 관심사(Concern) 을 Aspect로 모듈화 하여 사용하는 것을 의미하는데, 이것을 조금 더 전문적으로 얘기하자면 Cross-cutting concern 을 분리해 모듈화한다 라고 말할 수 있겠다.

여기서 관심사(Concern)에 대해 사용자 입장에서 해석하면 특정 로직을 수행하는 코드 정도로 해석하면 이해가 쉽다.

프로그래밍을 하다보면 특정 로직을 여러곳에서 반복적으로 사용해야 하는 경우가 있다. 예를들어, 성능을 측정하기 위해 로그를 남기는 로직이 있다고 생각해보자. 그런데 이런 로그를 남기는 로직이 한군데서만 사용되는가?
아마도, 여러 곳에서 반복적으로 사용할 수 있을 것이다. 이때, 해당 로직을 모듈화해서 사용하면 되는 것이다.

코드를 살펴보며 이해를 돕도록 하자.

public interface Factory {
    void produce();
}

---------------------------------------------------------------

public class CarFactory implements Factory{
    @Override
    public void produce() {
    	try {
              Thread.sleep(1000l);
              System.out.println("make a car");
        } catch (InterruptedException e) {
              e.printStackTrace();
        }
    }
}

---------------------------------------------------------------

public class BikeFactory implements Factory{
    @Override
    public void produce() {
        try {
            Thread.sleep(1000l);
            System.out.println("make a bike");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

다음과 같이 Factory를 구현한 구현체 CarFactory 와 BikeFactory가 있다.
여기에 성능을 측정할 로그를 찍는 관심사(Concern을 추가해보자)

public class CarFactory implements Factory{
    @Override
    public void produce() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1000l);
            System.out.println("make a car");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis()-begin);
    }
}

---------------------------------------------------------------

public class BikeFactory implements Factory{
    @Override
    public void produce() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1000l);
            System.out.println("make a bike");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis()-begin);
    }
}

위처럼 반복코드가 삽입되는 것을 볼 수 있다. 바로 이럴때, AOP 를 활용하는 것이다. 성능 로그를 찍는 관심사를 분리해 모듈화 시킴으로써 반복을 제거할 수 있다.

이제, 언제 AOP가 사용되고 왜 사용되는지에 대한 이해가 됐다. 그렇다면 AOP를 어떻게 만들고 적용시킬 수 있는지에 대해 알아보도록 하자.
AOP를 쓰기 위한 인터페이스는 다양한 언어들에서 지원된다. 자바에서는 AspectJ 가 있다 (참고 - AspectJ Docs) .
Spring 에서는 AspectJ 와는 조금 다른 인터페이스 를 지원해주는데 (참고 - Spring AOP docs) Proxy 패턴을 활용하여 Runtime 에 AOP를 적용하는 방법을 활용한다.

잠깐.. 그렇다면 Proxy 패턴을 활용해 Runtime에 적용?? Proxy는 뭐고 Runtime 에 적용시킨다는 말은 다른 사이클에서도 적용시킬 수 있다는 건가?

간단하게 설명하자면, AspectJ 를 사용할 경우 런타임에 적용하는 방법 이외에도, 컴파일시에 ajc라는 컴파일러를 사용해 한번 더 컴파일 하여 AOP가 적용된 바이트코드를 사용하는 방법, 또 로드할때 AOP를 적용시키는 방법 (이렇게 Class Loader 가 클래스를 로딩할때 작업을 수행하는 것은 load-time-weaving 이라한다. 이 경우 weaving 작업을 할 agent 가 필요하다).
그리고 스프링에서는 이러한 적용방법이 아닌 프록시패턴을 활용해 런타임시에 AOP를 적용한다.

그렇다면 Proxy패턴 에 대해 먼저 알아보도록 하자.
프록시패턴을 도식화하면 다음과 같다.
(출처 - 스프링 프레임워크의 핵심기술 강의)
사용자가 Subject를 요청하면 RealSubject를 감싼 Proxy객체를 제공하는 패턴이라고 생각하면 된다. 이러한 패턴을 사용하는 이유는 RealSubject 는 그대로 두고 Proxy 의 코드만 변경함으로써 부가 기능을 추가하고 접근제어를 할 수 있기 때문이다.

코드를 통해 이해를 돕도록 하자.

public class BikeFactory implements Factory{
    @Override
    public void produce() {
        try {
            Thread.sleep(1000l);
            System.out.println("make a bike");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

--------------------------------------------------------------- 

public class ProxyBikeFactory implements Factory {
    @Override
    public void produce() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1000l);
            System.out.println("make a bike");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis()-begin);
    }
}

코드를 보면 기존의 BikeFacotry에서 로그를 찍는 부가기능을 빼고 Proxy객체에 BikeFactory의 Proxy객체에 적용시켜놨다. 이상태에서 BikeFactory를 요청하면 ProxyBikeFactory를 주면, BikeFactory에 부가기능을 위한 코드 수정없이도 원하는 작업을 수행할 수 있게 되는 것이다.
그런데 그렇다고 cross-cutting concern 을 적용할 모든 객체에 proxy객체를 직접 만드는건.. 너무 힘든 일이다. 다행히도 Dynamic Proxy라 해서 동적으로 Proxy 를 생성할 수 있는 방법이 있다.

그렇다면!?!? 뭔가 생각이 이어지지 않는가 !?!?
스프링에서 .class 파일 즉, 바이트코드를 읽고 빈으로 등록하는 시점에, 즉 런타임 시점중 AOP를 적용시킨 ProxyBean을 생성해 준다면?? 그렇다 바로 이런 동작원리로 Spring AOP는 동작하고 그렇기 때문에 Proxy패턴을 활용해 런타임시 AOP를 적용시킨다. 라고 말할 수 있는 것이다. 조금 더 상세하게 설명하자면 빈을 등록하는 라이프사이클 중 BeanPostProcessor 에서 AbstractAutoProxyCreator 가 빈의 AOP가 적용된 프록시빈을 등록해주는 과정을 거치게 된다.

(출처 - Bean 의 생성 라이프사이클 그림)

지금까지..AOP에 대한 이해와 스프링에서 어떻게 AOP가 적용되는지에 대한 이해가 끝났다. 그럼 이제 AOP를 활용해보도록 하자

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PerfLoggingAspect {

    @Around("execution(* me.summerbell.demospringaop.aop..*.*(..))")
    public Object logPerf(ProceedingJoinPoint pjp) throws  Throwable{
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis()-begin);
        return retVal;
    }

}

의외로 AOP 를 적용하는 방법은 간단하다. 가장먼저 해야할 일은 Spring AOP dependency를 추가하는 일이다. 의존성을 추가했다면, 우선 cross-cutting 할 aspect를 만들어 주는데 @Aspect 어노테이션을 붙여야하고, 당연히 스프링에서 동작할 객체이니 @Component를 붙여 빈으로 만들어준다.

그리고 여기서 사용된 @Around 는 JoinPoint 이다. JoinPoint 는 cross-cutting 관심사가 어느시점에 동작할지에 대한 정의라고 볼 수 있다. @Around를 씀으로써 long begin = System.currentTimeMillis(); 가 수행되고 pjp.proceed();를 통해 cross-cutting 이 적용된 본래 로직이 수행되고나서 System.out.println(System.currentTimeMillis()-begin); 를 수행한다.
글로쓰니 어지러운데 (분리된 관심사 수행 - 본래의 로직 수행 - 분리된 관심사 수행) 처럼 동작한다는 뜻이다. 만약 @Before 어노테이션을 사용한다면 기존 로직수행 이전에 분리된 관심사를 수행한다.
그리고 @Around 뒤에 excution 표현식을 활용해 PointCut을 정해주었다.
PointCut이란 해당관심사가 어디에 적용될지를 정의해주는 것이다. execution 표현식말고도 annotaion을 활용할 수 도 있고 bean 을 활용해 특정 빈의 모든 메소드에 적용시킬 수 도 있다. annotation과 bean을 활용해 pointcut을 정의하는 방법은 깃허브에 소스코드를 남겨두도록 하겠다.

@Component @Primary
public class BikeFactory implements Factory{
    @Override
    public void produce() {
        try {
            Thread.sleep(1000l);
            System.out.println("make a bike");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

--------------------------------------------------------------- 

@Aspect
@Component
public class PerfLoggingAspect {

    @Around("execution(* me.summerbell.demospringaop.aop..*.*(..))")
    public Object logPerf(ProceedingJoinPoint pjp) throws  Throwable{
        long begin = System.currentTimeMillis();
        Object retVal = pjp.proceed();
        System.out.println(System.currentTimeMillis()-begin);
        return retVal;
    }

}

--------------------------------------------------------------- 

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    Factory bikeFactory;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        bikeFactory.produce();
    }
}

이렇게 하면 모든게 아름답게 작동할꺼야 하고 돌린결과!
뭐야 왜 두번찍힌거지 하고 한참을 고민하다.. 디버거로 잡아보니...
"execution( me.summerbell.demospringaop.aop..*.*(..))"
에서 모든 클래스의 모든 메소드에 Aspect를 적용시켰더니 ApplicationRunner 가 실행되면서 한번 더 찍히는 것이였다.. 디버거를 통해 에러를 잡아보는 좋은 경험을 할 수 있었고
excution을 다음과 같이 고쳐서 수행하니 똑바로 작동하였다.
"execution(
me.summerbell.demospringaop.aop..Factory.*(..))"

*참고: 이 포스팅은 백기선님의 스프링프레임워크 핵심기술 강의를 듣고, 제 나름대로 다시 공부하고 정리한 포스팅 입니다. 만약, 조금 더 자세한 내용을 듣고 싶으시다면 이곳의 강의를 참고해보시기 바랍니다

0개의 댓글