스프링의 여러 장점이 되는 특징들이 있고 그중 하나이 AOP에 대해 간단히 정리해 보고자 한다.
좋은 개발환경의 중요 원칙은 '개발자가 비즈니스 로직에만 집중할 수 있게 한다'이다. 대부분의 시스템이 공통으로 가지고 있는 보안, 로그 , 트랜잭션과 같이 비즈니스 로직은 아니지만, 반드시 처리가 필요한 부분을 스프링에서는 '횡단 관심사(cross-concern)'라고 한다. AOP는 이러한 횡단 관심사를 모듈로 분리하는 프로그래밍의 패러다임이다.
AOP(Aspect-Oriented Programming)는 '관점 지향 프로그래밍'이라는 의미이고 '관점(Aspect)'이라는 용어는 개발자들에게는 '관심사(concern)'라는 말로 통용된다. 관심사는 핵심 로직은 아니지만, 코드를 온전하기 위해 필요한 것들이다. 일반적인 방식에서는 개발자가 이러한 코드들을 반복해서 작성하지만 AOP는 이러한 관심사들을 분리하는 것을 추구한다.
즉, '관심사 + 비즈니스 로직'에서 관심사 부분을 별도의 코드로 작성하고 컴파일 혹은 실행할 때 이를 결합하는 방식이다. 그렇기에 개발자들은 비즈니스 로직에 집중하여 개발을 할 수 있고 어떤 관심사들과 결합할 것인지를 설정하는 것만을 하면 되는 것이다. 스프링이 AOP를 지원하는 것이 가장 중요한 특징 중에 하나로 말하게 된 이유는 별도의 복잡한 설정이나 제약 없이 스프링 내에서 간편하게 AOP의 기능들을 구현할 수 있기 때문이다.
AOP는 기존의 코드를 수정하지 않고, 원하는 기능들과 결합할 수 있는 패러다임이다. 아래 그림을 한번 봐보자.
개발자의 입장에서 AOP를 적용하는 것은 기존의 코드 수정 없이 원하는 관심사(cross-concern)들을 엮을 수 있다는 점이다. 위의 그림에서 Target에 해당하는 것이 핵심 비즈니스 로직을 가지는 객체이다. 이는 순수한 비즈니스 로직이고 순수한 코어라고 볼 수 있다. 이러한 Target를 감싸는 것을 Proxy라고 한다. Proxy는 내부적으로 Target를 호출하지만, 중간에 필요한 관심사들을 거쳐서 Target을 호출하도록 자동 혹은 수동으로 작성된다. Proxy의 존재는 직접 코드로 작성할 수 있지만, 대부분의 경우 스프링 AOP 기능을 이용해서 자동으로 생성되는(auto-proxy) 방식을 이용한다. JoinPoint는 Target 객체가 가진 메서드이며 외부에서의 호출은 Proxy 객체를 통해서 Target 객체의 JoinPoint를 호출하는 방식으로 이해하면 된다.
JoinPoint는 Target이 가진 여러 메서드이며 어떤 메서드에 관심사를 결합할 것인지를 'Pointcut'이라고 한다. Pointcut은 관심사와 비즈니스 로직이 결합되는 지점을 결정하는 것이다. 앞에 Proxy는 이 결합이 완성된 상태이므로 메서드를 호출하게 되면 자동으로 관심사가 결합된 상태도 동작하게 된다. 관심사는 Aspect이며 이는 추상적인 개념이고 이를 구현한 코드가 Advice이다.
이는 아래의 그림과 같다.
Advice는 동작 위치에 따라 아래와 같이 구분한다.
- Before Advice: Target의 JoinPoint를 호출하기 전에 실행되는 코드이다. 실행 자체에는 관여할 수 없다.
- After Returning Advice: 모든 실행이 정상적으로 이루어진 후에 동작하는 코드이다.
- After Throwing Advice: 예외가 발생한 뒤에 동작하는 코드이다.
- After Advice: 정상적으로 실행되거나 예외가 발생했을 때 구분 없이 실행되는 코드이다.
- Around Advice: 메서드의 실행 자체를 제어할 수 있는 가장 강력한 코드이다. 직접 대상 메서드를 호출하고 결과나 예외를 처리할 수 있다.
Advice는 과거의 스프링에서는 별도의 인터페이스로 구현되고, 이를 클래스로 구현하는 방식이었지만 스프링 3 버전 이후에는 어노테이션만으로도 모든 설정이 가능하다. Target에 어떤 Advice를 적용할지는 XML이나 어노테이션을 이용해서 적용할 수 있다.
Pointcut는 어떤 Advice를 어떤 JoinPoint에 결합할 것인지를 결정하는 설정이다. Pointcut은 다양한 형태로 선언해서 사용될 수 있는데 주로 사용되는 설정 아래와 같다.
- execution(@execution): 메서드를 기준으로 Pointcut를 설정한다.
- within(@within): 특정한 타입(클래스)을 기준으로 Pointcut을 설정한다.
- this: 주어진 인터페이스를 구현한 객체를 대상으로 Pointcut을 설정한다.
- args(@args): 특정한 파라미터를 가지는 대상들만을 Pointcut으로 설정한다.
- @annotation: 특정한 어노테이션이 적용된 대상들만을 Pointcut으로 설정한다.
간단히 정리하면 JointPoint는 Target의 메서드이고 어느 JointPoint에 Advice를 결합할지를 정하는 것이 Pointcut이다. 그리고 정해진 JointPoint에서 어느 위치(ex. 실행 전, 실행 후)에 Advice를 결합할지에 따라 Before Advice부터 Around Advice까지 구분되는 것이다. 컴파일 및 실행 시 외부로부터 요청이 오면 Proxy는 이러한 결합을 하고 결합된 상태로 보여주는데 Spring에서는 자동으로 이뤄지는 것이다.
그렇다면 이제 스프링 부트로 어떻게 적용되는 지 간단하게 살펴보겠다.
스프링 부트에 AOP를 적용하기 위해서는 먼저 아래와 같이 의존성을 추가해줘야 한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
의존성을 추가해 주었다면 aop 패키지를 만들고 그 안에 Advice 클래스를 생성한다. 서비스 메서드에는 아무것도 추가 작성하지 않아도 된다. 아래 예시는 서비스가 실행 전에 로그를 출력해주는 예시이다.
@Aspect를 달아줌으로서 이 클래스가 Advice이라는 것을 알리고 @Component를 통해 bean 등록을 하고 @Log4j2를 통해 log를 출력하는 것이다. @Before는 BeforeAdvice를 구현한 메서드이다. @After, @AfterReturning, @AfterThrowing, @Around 역시 동일한 방식으로 구현한다.
괄호에 execution을 지정한 것은 Pointcut이며 @Pointcut을 통해 따로 지정할 수도 있다. 내부의 execution은 포인트 컷 조합식으로 아래와 같다.
@Around("execution(* com.example.demo.service.BookService.*(..))") 를 통해서 어떤 메서드들이 이 AOP를 적용받을 것인지를 정의했다.
execution(* com.example.demo.service.BookService.*(..))는 com.example.demo.service 패키지의 BookService의 모든 메서드가 적용받을 것이라고 한 것이다.
Pointcut | JoinPoints |
execution(public * *(..)) | public 메소드 실행 |
execution(* set*(..)) | 이름이 set으로 시작하는 모든 메소드명 실행 |
execution(* get*(..)) | 이름이 get으로 시작하는 모든 메소드명 실행 |
execution(* com.xyz.service.AccountService.*(..)) | AccountService 인터페이스의 모든 메소드 실행 |
execution(* com.xyz.service.*.*(..)) | service 패키지의 모든 메소드 실행 |
execution(* com.xyz.service..*.*(..)) | service 패키지와 하위 패키지의 모든 메소드 실행 |
within(com.xyz.service.*) | service 패키지 내의 모든 결합점 (클래스 포함) |
within(com.xyz.service..*) | service 패키지 및 하위 패키지의 모든 결합점 (클래스 포함) |
bean(*Repository) | 이름이 “Repository”로 끝나는 모든 빈 |
bean(*) | 모든 빈 |
bean(account*) | 이름이 'account'로 시작되는 모든 빈 |
bean(*dataSource) || bean(*DataSource) | 이름이 “dataSource” 나 “DataSource” 으로 끝나는 모든 빈 |
출처 : http://devjms.tistory.com/70, https://jeong-pro.tistory.com/171
메서드의 매개변수를 활용할 수 도 있는데 아래와 같다.
이 밖에 @Around도 한번 살펴보겠다.
@Around는 직접 대상 메서드를 실행할 수 있는 권한을 가지고 있고, 메서드의 실행 전과 실행 후에 처리가 가능하다. 여기서 필요한 것이 ProceedingJoinPoint이고 @Around와 같이 결합해서 파라미터나 예외 등을 처리할 수 있다. 아래의 예시 코드를 봐보자.
@Around 메서드는 리턴 타입이 void가 아닌 타입으로 설정하고, 메서드의 실행 결과 역시 직접 반환하는 형태가 되어야 한다. 매개변수로 ProceedingJoinPoint라는 파라미터를 지정하는데, 이는 AOP의 대상이 되는 Target나 파라미터 등을 파악할 뿐만 아니라, 직접 실행을 결정할 수 있다. 위의 실행 결과는 아래와 같다.
결과를 보면 service 실행 전에 Target과 Param을 출력하고 pjp.proceed()를 통해 메서드를 실행 전 위에서 설정한 @Before가 실행된다. 그리고 서비스 내용은 insert가 실행되고 서비스 종료 후에는 Time이 출력되는 것을 알 수 있다.
참고
https://engkimbs.tistory.com/746
https://memostack.tistory.com/238
https://jeong-pro.tistory.com/171
코드로 배우는 스프링 웹 프로젝트 - 구멍가게 코딩단
'Spring' 카테고리의 다른 글
Spring boot - 서버의 존재하지 않는 URL 접근 시 예외 처리 (0) | 2022.04.09 |
---|---|
스프링부트 - 인터셉터(Interceptor) (0) | 2022.03.06 |
Spring Controller를 REST 방식으로 변화 - Rest API (0) | 2022.02.23 |
스프링 MVC에 대한 간단 정리 (0) | 2022.02.09 |
스프링 프레임워크의 특징 및 의존성 주입 (0) | 2021.12.06 |