AOP는 Aspect Oriented Programming이라고 하며 관점 지향 프로그래밍이라고 한다.
AOP는 여러 개의 핵심 비즈니스 로직 외에 공통으로 처리되어야 하는 로그 출력, 보안 처리, 예외 처리와 같은 코드를 별도로 분리해서
하나의 단위로 묶는 모듈화의 개념으로 생각할 수 있다. 쓰이는 곳에 필요할 때 연결함으로써, 유지보수 혹은 재사용에 용이하도록 프로그래밍한다.
스프링 애플리케이션은 대부분 특별한 경우를 제외하고는 MVC(Model, View, Controller) 웹 애플리케이션에서는 Web Layer, Business Layer, Data Layer로 정의한다.
- Web Layer : REST API를 제공하며, Client 중심의 로직을 적용한다.
- Business Layer : 서비스를 담당. 내부 정책에 따른 logic를 개발하며, 주로 해당 부분을 개발한다.
- Data Layer : 데이터베이스 및 외부와의 연동을 처리한다.
횡단 관심
주요 어노테이션
Annotation | 의미 |
@Aspect | 자바에서 널리 사용하는 AOP 프레임워크에 포함되며, AOP를 정의하는 Class에 할당 |
@Pointcut | 기능을 어디에 적용시킬지, method, annotation 등 AOP를 적용 시킬 지점을 설정 |
@Before | method 실행하기 이전 |
@After | method가 성공적으로 실행 후, 예외가 발생되더라도 실행 |
@AfterReturing | method 호출 성공 실행 시(Not Throws) |
@AfterThrowing | method 호출 실패 예외 발생(Throws) |
@Around | Before / after 모두 제어 |
// RestApiController.java
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public void get(@PathVariable Long id, @RequestParam String name) {
System.out.println("get method");
System.out.println("get method : " + id);
System.out.println("get method : " + name);
}
@PostMapping("/post")
public void post(@RequestBody User user) {
System.out.println("post method : " + user);
}
}
다음과 같이 각 method에 대한 로그를 작성해야 한다고 할 때 개수가 적으면 괜찮을 수 있지만 10개, 20개가 넘어가면 이렇게 하나하나 관리하기가 힘들어진다. 그럴 때 AOP를 사용하여 각 method마다 로그를 찍는 부분을 한 곳으로 모을 수 있다. AOP로 동작할 수 있도록 aop package를 만들고 ParameterAop라는 클래스를 하나 생성하도록 하자.
# ParameterAop
package com.example.aop.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect // AOP 를 정의하는 class 에 할당 (공통 부분을 모듈화)
@Component // 스프링에서 관리되어야 하므로
public class ParameterAop {
// @Pointcut 은 AOP 를 적용시킬 지점을 설정
// com.example 의 aop 프로젝트의 controller 패키지의 하위 method(파라미터가 0개 이상) 모두를 aop 로 보낸다
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
// @Pointcut은 JoinPoint의 상세 스펙을 정의한다.
private void cut() {} // 요청을 받으면 cut() 실행?
// method 실행하기 전, 언제 실행 시킬 것인지
@Before("cut()") // @Pointcut에 해당하는 joinpoint cut()이 실행 전
public void before(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for(Object obj : args) {
System.out.println("type : " + obj.getClass().getSimpleName());
System.out.println("valeu : " + obj);
}
}
// 정상 실행이 되면 해당 object 값을 확인할 수 있다.
// Target method 성공적 실행 후, 결괏값을 반환한 뒤에 적용
@AfterReturning(value = "cut()", returning = "returnObj") // cut() 실행 후 returning에 결괏값 반환
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
System.out.println("return obj");
System.out.println(returnObj);
}
}
// RestApiController.java
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
System.out.println("get method");
System.out.println("get method : " + id);
System.out.println("get method : " + name);
return id + " " + name;
}
@PostMapping("/post")
public User post(@RequestBody User user) {
System.out.println("post method : " + user);
return user;
}
}
위 코드를 살펴보기에 앞서 AOP의 용어와 PointCut의 표현식을 정리하고 넘어가고자 한다.
- Target : 핵심 기능을 담고 있는 모듈로 타겟은 부가기능을 부여할 대상이 된다.
- Advice : 어드바이스는 타겟에 제공할 부가기능을 담고 있는 모듈이다.
- Join Point : 어드바이스가 적용될 수 있는 위치를 말한다. 타겟 객체가 구현한 인터페이스의 모든 메서드는 조인 포인트가 된다.
- Pointcut : 어드바이스를 적용할 타겟의 메서드를 선별하는 정규표현식이다. 포인트컷 표현식은 execution으로 시작하고 메서드의 Signaature를 비교하는 방법을 주로 이용한다.
- Aspect : 애스펙트는 AOP의 기본 모듈이다. Advice + Pointcut이다. 애스팩트는 싱글톤 형태의 객체로 존재한다.
- Advisor : Advice + Pointcut. 어드바이저는 Spring AOP에서만 사용되는 특별한 용어이다.
- Weaving : 위빙은 포인트컷에 의해 결정된 타겟의 조인 포인트에 부가기능(어드바이스)을 삽입하는 과정이다. 위빙은 AOP가 핵심기능(타깃)의 코드에 영향을 주지 않으면서 필요한 부가기능(어드바이스)을 추가할 수 있도록 해주는 핵심적인 처리 과정이다.
execution | Advice를 적용할 메서드를 명시할 때 사용한다. |
within | 특정 타입에 속하는 메서드를 JoinPoint로 설정되도록 명시할 때 사용한다. |
bean | 스프링 버전 2.5 버전부터 지원하기 시작했으며, 스프링 빈을 이용하여 JoinPoint를 설정한다. |
밑 내용 출처 : https://icarus8050.tistory.com/8
execution 명시자
execution([수식어] 리턴 타입 [클래스 이름]. 이름(파라미터)
- 수식어 : public, private 등 수식어를 명시합니다. (생략 가능)
- 리턴 타입 : 리턴 타입을 명시합니다.
- 클래스 이름 및 이름 : 클래스 이름과 메서드 이름을 명시합니다. (클래스 이름은 풀 패키지명으로 명시해야 합니다. 생략도 가능)
- 파라미터 : 메서드의 파라미터를 명시합니다.
- " * " : 모든 값을 표현합니다.
- " .. " : 0개 이상을 의미합니다.
Ex)
execution(public Integer com.edu.aop.*.*(*))
- com.edu.aop 패키지에 속해있고, 파라미터가 1개인 모든 메서드
execution(* com.edu..*.get*(..))
- com.edu 패키지 및 하위 패키지에 속해있고, 이름이 get으로 시작하는 파라미터가 0개 이상인 모든 메서드
execution(* com.edu.aop..*Service.*(..))
- com.edu.aop 패키지 및 하위 패키지에 속해있고, 이름이 Service르 끝나는 인터페이스의 파라미터가 0개 이상인 모든 메서드
execution(* com.edu.aop.BoardService.*(..))
- com.edu.aop.BoardService 인터페이스에 속한 파라미터가 0개 이상인 모든 메서드
execution(* some*(*, *))
- 메서드 이름이 some으로 시작하고 파라미터가 2개인 모든 메서드
within 명시자
Ex)
within(com.edu.aop.SomeService)
- com.edu.aop.SomeService 인터페이스의 모든 메서드
within(com.edu.aop.*)
- com.edu.aop 패키지의 모든 메서드
within(com.edu.aop..*)
- com.edu.aop 패키지 및 하위 패키지의 모든 메서드
bean 명시자
Ex)
bean(someBean)
- 이름이 someBean인 빈의 모든 메서드
bean(some*)
- 빈의 이름이 some으로 시작하는 빈의 모든 메서드
위 코드를 살펴보면 @Poincut으로 controller 패키지 하위의 파라미터가 0개 이상인 모든 method들에 대해 aop를 수행할 수 있도록 지정해주었음을 알 수 있다. @Before와 @AfterReturning에 "cut()"을 명시하여 @Poincut으로 지정한 조인포인트에 대해 수행 전, 후 처리해야 할 로직을 작성했다. 작성을 완료하고 POST method를 수행해본 결과는 다음과 같다.
POST method를 실행하기 전 조인포인트에 의해 cut()이 수행된다. cut 수행 전, @Before로 지정한 로직이 실행되면서 조인포인트의 아규먼트를 객체 배열에 담아 해당 객체에 대한 type과 value를 출력한다. type은 'User', value는 User에 대한 정보가 찍힌 것을 볼 수 있다. 전처리 후, POST method를 수행하고 user를 반환시킨다. @AfterReturning에서 반환받은 user 객체를 통해 'return obj'를 찍고 반환받은 객체를 그대로 출력한다. GET method도 이러한 방식과 동일하게 동작한다. 이번엔 method의 이름까지 출력하는 실습을 진행해보도록 한다. 아까의 코드를 가져와서 수정을 한다.
# ParameterAop
package com.example.aop.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
@Aspect // AOP 를 정의하는 class 에 할당 (공통 부분을 모듈화)
@Component // 스프링에서 관리되어야 하므로
public class ParameterAop {
// @Pointcut 은 AOP 를 적용시킬 지점을 설정
// com.example 의 aop 프로젝트의 controller 패키지의 하위 method(파라미터가 0개 이상) 모두를 aop 로 보낸다
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
// @Pointcut은 JoinPoint의 상세 스펙을 정의한다.
private void cut() {} // 요청을 받으면 cut() 실행?
// method 실행하기 전, 언제 실행 시킬 것인지
@Before("cut()") // @Pointcut에 해당하는 joinpoint cut()이 실행 전
public void before(JoinPoint joinPoint) {
// method 의 이름 가져오기
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
System.out.println(method.getName());
Object[] args = joinPoint.getArgs();
for(Object obj : args) {
System.out.println("type : " + obj.getClass().getSimpleName());
System.out.println("valeu : " + obj);
}
}
// 정상 실행이 되면 해당 object 값을 확인할 수 있다.
// Target method 성공적 실행 후, 결괏값을 반환한 뒤에 적용
@AfterReturning(value = "cut()", returning = "returnObj") // cut() 실행 후 returning에 결괏값 반환
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
System.out.println("return obj");
System.out.println(returnObj);
}
}
// RestApiController.java
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
return id + " " + name;
}
@PostMapping("/post")
public User post(@RequestBody User user) {
return user;
}
}
Method의 이름을 알아낼 수 있도록 하는 코드를 aop로 몰아서 실제 서비스 로직에는 해당 코드가 모두 빠지게 만들 수 있다. 이러한 것을 aop를 통해 디버깅을 할 수도 있다.
그리고 이번에는 GET method를 수행해본 결과, 위와 같은 결과를 도출할 수 있었다. cut() 수행 이전에 @Before에서 동작하는 것은 method의 이름을 가져온다.(get은 GET method 호출 시의 get()이 찍힌 것이다.) 또한 해당 method의 객체를 구성하는 인자의 long과 String 타입이 찍힌 것을 볼 수 있으며 GET method의 반환 객체를 @AfterReturning이 받아 @Before가 반환했던 형식 그대로 출력되었음을 알 수 있다.
'공부 > Spring boot' 카테고리의 다른 글
스프링부트 Swagger (SpringFox Boot Starter 3.0.0) 설정 관련 에러 (0) | 2021.12.16 |
---|---|
JUnit이란? (0) | 2021.12.07 |
IoC / DI (0) | 2021.12.05 |
Interceptor (0) | 2021.12.02 |
Spring Boot Filter (0) | 2021.12.01 |