스프링 핵심 원리 - 고급편 - 섹션9,10. 스프링 AOP 개념,구현
CS/김영한 스프링 강의

스프링 핵심 원리 - 고급편 - 섹션9,10. 스프링 AOP 개념,구현

지금 문제가 뭐냐면 실제로 비즈니스 기능을 수행하는 핵심 기능과 로그를 남기는 부가 기능으로 나누는데, 이런 부가 기능은 대체로 여러 클래스에서 쓰일 확률이 높다.

 

그래서 여러 클래스에 거쳐 사용되기 때문에 횡단 관심사라고 하고, 때문에 사용하려면 각각의 클래스마다 복붙을 해야 한다. 현실적으론 복잡해지는데, 원하는 클래스마다 함수를 써서 넣어야 하고 이걸 유틸같은 함수로 정의해서 사용해도 결국은 안의 코드를 수정해야 한다. 왜냐하면 핵심 기능을 감싸는 코드여야 하기 때문

 

이 문제를 해결하기 위해 스프링에서 AOP를 지원한다.

 

 

이런 고민은 특정 앱을 만든 한명이 아닌 보통 만들때 고민하는 문제들이라 아얘 지원하는걸 만들었다. @Aspect인데, 지금까지는 클래스 각각 하나를 보면서 앱을 만들었다면, 횡단 관심사로써 전체적으로 다시 관점을 바꾸는 것이다. 이렇게 애스펙트를 사용한 프로그래밍 방식을 관점 지향 프로그래밍이라고 한다. 이름은 그 당시 유행하던걸로 해서 뭐 대단해 보이는데 막상 보면 별거 없다.

이걸 사용하기 위한 대표적인 프레임워크가 AspectJ인데, 스프링의 AOP가 이 프레임워크의 문법을 차용했다(기능까지 가져온건 아님)

 

프레임워크라 원래 클래스를 프록시로 바꿀 수 있는 구간을 조인포인트라고 하는데 컴파일 시점에, 클래스 로딩 시점에, 런타임 시점(프록시)에 3가지 구간이 있다. 다 장단점이 있네마네 하는데.. 어차피 런타임만 사용할 수 있는 스프링 AOP만 해도 대부분의 문제가 해결되서 이것만 쓸듯. 이 런타임 시점의 단점은 프록시라 메서드 오버라이딩 개념으로 실행되서 막 생성자까지 할 수 있는 컴파일 시점처럼 안된다는 것. 근데 쉽고 활용성 높은게 장땡이다. 지금까지 써본건 런타임 시점임.

 

 

 

이제 적용해보자. 프로젝트 새로 만들고 기초 만들고 테스트코드 만든다.

 

아직 프록시를 안썻기 때문에 isProxy가 false나옴.

 

@Aspect를 사용해서 프록시 클래스를 만들어보자.

@Around로 포인트컷에 만족하는 조건을 내가 만든 패키지 이하들로 했으므로 조건을 만족해서 다 프록시 클래스가 만들어진다.

 

포인트컷을 @Around으로 직접 주입하는것 말고도 @Pointcut으로 별도로 분리할 수도 있다.

 

 

@Pointcut으로 함수화해서 한걸 이용하여 public으로 바꾸고 여러곳에서도 모듈화해서 사용할 수 있다.

 

이제 and를 사용해보자. service에만 적용할 트랜잭션을 모방한 부가기능을 넣어보자.

보면 service로 된 것만 트랜잭션 부가함수가 작동한 걸 알 수 있다.

 

이 실행 순서를 바꿀 수도 있지만 그 전에 포인트컷을 외부로 빼서 사용하는 방법을 먼저 보자.

 

 

사실 별거 없었다..

이제 순서 바꿔보자.

 

@Aspect는 순서를 보장하지 않아 순서를 @Order로 정해줘야 하는데 @Order가 함수 단위가 아닌 클래스 단위로 된다는 것이다. 그래서 불편하지만 클래스를 따로 만들어 @Aspect를 각각 해줘야 한다.

 

 

 

 

 

 

 

 

 

 

 

어드바이스가 @Around만 있는건 아니고 여러개 있다.

 

하지만 모두 @Around에서 기능을 조금씩 빼와서 쓰는 것 뿐임.

 

@Slf4j
@Aspect
public class AspectV6Advice {

    @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
    public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {

        try {
            //@Before
            log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
            Object result = joinPoint.proceed();
            //@AfterReturning
            log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
            return result;
        } catch (Exception e) {
            // @AfterThrowing
            log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
            throw e;
        } finally {
            // @After
            log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
        }
    }

    @Before("hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[before] {}", joinPoint.getSignature());
    }

    @AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()",
            returning = "result")
    public void doReturn(JoinPoint joinPoint, Object result) {
        log.info("[return] {} return={}", joinPoint.getSignature(), result);
    }
    @AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()",
            throwing = "ex")
    public void doThrowing(JoinPoint joinPoint, Exception ex) {
        log.info("[ex] {} message={}", joinPoint.getSignature(),
                ex.getMessage());
    }
    @After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[after] {}", joinPoint.getSignature());
    }

}

 

근데 이걸 왜 써야 하냐? 바로 제약을 함으로써 실수 방지 및 가독성 향상이 있기 때문임.

실수란 @Around를 사용하면 핵심 함수 언제 실행할지를 proceed로 명시하고 반드시 return을 해줘야 한다. 그리고 이 어노테이션 이름만 봐도 감이 오는 제약들.