학습 목표
· AOP란?
· AOP 관련 용어
· AOP vs OOP
· Spring AOP vs AspectJ
· 여러 가지 AOP 어드바이스
· 의존성 추가
· 스프링 부트에서 AOP 사용하기
- Before Advice
- After Advice
- Around Advice
- After Returning Advice
- After Throwing Advice
· 스프링에서 AOP를 구현한 방법
AOP란?
· Aspect Oriented Programming(관점 지향 프로그래밍)의 약어
· 여러 클래스에 나뉜 책임을 애스팩트라고 부르는 별도의 클래스에 캡슐화하는 접근 방식
· AOP의 필요성:
- 일반적인 자바 애플리케이션은 웹 계층, 비즈니스 계층, 데이터 계층 등 여러 계층으로 응용 프로그램을 개발한다.
이때 각 계층의 책임은 다르지만 로깅, 트랜잭션, 보안, 인증, 캐싱 등 공통적인 로직이 필요한다.
이러한 공통적인 로직을 횡단 관심사(cross-cutting concern)이라고 부른다.
공통 로직을 각 계층에서 개별적으로 구현하면, 코드 유지관리가 어려워진다.
이 문제를 극복하기 위해 AOP는 횡단 관심사 구현하는 해결책을 제공한다.
· AOP의 장점
1. 각 문제에 대한 논리가 소스코드 전체에 흩어져 있는 대신 한 곳에 존재한다.
2. 비즈니스 모듈에는 주요 관심사에 대한 코드만 포함된다.
3. 비즈니스 코드를 수정하지 않고도 추가 동작을 더할 수 있음
· AOP는 횡단 관심사의 분리를 통해 공통 기능을 한 곳에 정의하여 모듈성을 증가시킴
· 스프링은 내부에서 트랜잭션 관리, 캐싱, 보안 등의 선언적인 서비스를 구현하기 위해 Spring AOP 프레임워크를 제공한다.
스프링 프레임워크 대신 AspectJ를 애플리케이션에서 AOP 프레임워크로 사용할 수도 있다.
TODO: Spring AOP vs AspectJ https://www.baeldung.com/spring-aop-vs-aspectj
AOP 관련 용어
· 애스팩트(Aspect):횡단 관심사의 동작과 그 횡단 관심사를 적용하는 소스 코드상의 포인트를 모은 것이다.
즉, 하나 이상의 어드바이스(동작)와 포인트컷(동작을 적용하는 조건)을 조합한 것이다.
@Aspect 애너테이션이 달린 일반 클래스를 사용하여 aspect를 구현할 수 있다.
· 조인포인트(Joinpoint): 어드바이스가 실행하는 동작을 끼워 넣을 수 있는 때를 말한다.
조인 포인트는 개발자가 고려해서 만들어 넣을 수 없는 AOP(제품)의 사양이다. 스프링에서는 메서드가 호출될 때와
메서드가 원래 호출한 곳으로 돌아갈 떄가 어드바이스를 끼워 넣을 수 있는 조인 포인트다.
· 어드바이스(Advice):조인 포인트에서 실행되는 코드를 말한다. 로그 출력, 트랜잭션 관리 등의 코드가 기술된다.
· 포인트컷(Pointcut): 어드바이스가 실행되는 하나 이상의 조인 포인트를 선택하는 표현식이다.
표현식이나 패턴을 사용하여 포인트컷을 정의할 수 있다. 조인 포인트와 매칭되는 다양한 종류의 표현식을 사용할 수 있으며,
스프링 프레임워크는 AspectJ pointcut 표현 언어를 사용한다.
· 대상 객체(Targe object): 어드바이스가 적용되는 객체를 말한다. 대상 객체는 항상 프록시된다.
이는 대상 메서드가 재정의되는 런타임에 하위 클래스가 생성되고, 어드바이스들은 해당 설정에 따라 포함됨을 의미한다.
· 위빙(Weaving): 애스팩트를 다른 애플리케이션 타입과 연결하는 프로세스다.
런타임, 로드 타임 및 컴파일 타임에 위빙을 수행할 수 있다.
AOP vs OOP
AOP | OOP |
Aspect: 포인트컷, 어드바이스 및 속성을 캡슐화하는 코드 단위 | Class: 메서드와 속성을 캡슐화하는 코드 단위 |
Pointcut: 어드바이스가 실행되는 진입점들을 정의 | Method Signature: 메서드 바디 실행을 위한 진입점들을 정의 |
Advice: 횡단 관심사의 구현 | Methid bodies: 비즈니스 로직 관심사의 구현 |
Weaver: 어드바이스로 코드(소스 또는 개체)를 구성함 | Compier: 소스 코드를 객체 코드르 변경함 |
Spring AOP vs AspectJ
Spring AOP | AspectJ |
별도의 컴파일 과정이 필요 | AspectJ 컴파일러 필요 |
메서드 실행 포인트컷만 지원 | 모든 포인트컷을 지원 |
스프링 컨테이너에서 관리하는 빈에서 구현 가능 | 모든 도메인 객체에서 구현 가능 |
메서드 레벨 위빙만 지원 | 필드, 메서드, 생성자, 정적 초기자(static initializer), final 클래스 등을 위빙 |
여러 가지 AOP 어드바이스
· Before Advice: 조인 포인트 이전에 호출되는 어드바이스, @Before 어노테이션을 통해 표시한다.
· After Advice: 조인 포인트 이후에 호출되는 어드바이스, @After 어노테이션을 통해 표시한다.
· Around Advice: 조인 포인트 이전과 이후에 호출되는 어드바이스, @Around 어노테이션을 통해 표시한다.
· After Throwing: 조인 포인트가 예외를 던질 때 호출되는 어드바이스, @AfterThrowing 어노테이션을 통해 표시한다.
· After Returning: 메서드가 성공적으로 실행될 때 호출되는 어드바이스, @AfterReturning 어노테이션을 통해 표시한다.
의존성 추가
maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.5.5</version>
</dependency>
gradle
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-aop', version: '2.5.5'
스프링 부트에서 AOP 사용하기
Before Advice
1. 가장 먼저 스프링 부트 프로젝트에 위에서 설명한 의존성을 추가한다.
2. 스프링 부트 프로젝트의 main 메서드가 선언된 xxxApplication.java 파일에 @EnableAspectJAutoProxy 애너테이션을 추가한다.
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class AopBeforeAdviceExampleApplication
{
public static void main(String[] args) {
SpringApplication.run(AopBeforeAdviceExampleApplication.class, args);
}
}
이렇게 하면 AspectJ의 @Aspect 주석으로 표시된 구성 요소를 처리할 수 있다. proxyTargetClass 속성을 사용하여 프록시 유형을 제어할 수 있고, 기본값은 false이다.
3. Employee라는 이름의 클래스를 생성한다.
public class Employee
{
private String empId;
private String firstName;
private String secondName;
public String getEmpId() {
return empId;
}
public void setEmpId(String empId) {
this.empId = empId;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getSecondName() {
return secondName;
}
public void setSecondName(String secondName) {
this.secondName = secondName;
}
}
3. employee를 추가하기 위한 api 경로를 매핑하는 Controller를 추가한다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@RequestMapping(value = "/add/employee", method = RequestMethod.GET)
public Employee addEmployee(@RequestParam("empId") String empId, @RequestParam("firstName") String firstName, @RequestParam("secondName") String secondName) {
return employeeService.createEmployee(empId, firstName, secondName);
}
}
4. employee를 추가하는 메서드가 정의된 Service를 추가한다.
@Service
public class EmployeeService {
public Employee createEmployee( String empId, String fname, String sname) {
Employee emp = new Employee();
emp.setEmpId(empId);
emp.setFirstName(fname);
emp.setSecondName(sname);
return emp;
}
}
5. before advice 로직이 정의된, 애스팩트 클래스를 생성한다.
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class EmployeeServiceAspect
{
@Before(value = "execution(* com.flab.realestateinvest.service.EmployeeService.*(..)) and args(empId, fname, sname)")
public void beforeAdvice(JoinPoint joinPoint, String empId, String fname, String sname) {
System.out.println("Before method:" + joinPoint.getSignature());
System.out.println("Creating Employee with first name - " + fname + ", second name - " + sname + " and id - " + empId);
}
}
· execution(expression): 어드바이스가 적용되는 메서드
· @Before: 포인트컷에서 다루는 메서드 이전에 실행될 함수를 어드바이스로 표시
6. 스프링부트 프로젝트를 실행하고, employee 추가 요청을 한다.
http://localhost:8080/add/employee?empId={id}&firstName={fname}&secondName={sname}
ex) http://localhost:8080/add/employee?empId=100&firstName=Ted&secondName=mosby
요청을 하면 위의 이미지 처럼 대상 메서드의 실행 전 콘솔에 메세지가 출력된다.
After Advice
1. 위에서 작성한 EmployeeServiceAspect 애스팩트 클래스에 after advice 로직을 추가한다.
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class EmployeeServiceAspect
{
@Before(value = "execution(* com.flab.realestateinvest.service.EmployeeService.*(..)) and args(empId, fname, sname)")
public void beforeAdvice(JoinPoint joinPoint, String empId, String fname, String sname) {
System.out.println("Before method:" + joinPoint.getSignature());
System.out.println("Creating Employee with first name - " + fname + ", second name - " + sname + " and id - " + empId);
}
@After(value = "execution(* com.flab.realestateinvest.service.EmployeeService.*(..)) and args(empId, fname, sname)")
public void afterAdvice(JoinPoint joinPoint, String empId, String fname, String sname) {
System.out.println("After method:" + joinPoint.getSignature());
System.out.println("Creating Employee with first name - " + fname + ", second name - " + sname + " and id - " + empId);
}
}
2. 스프링부트 프로젝트를 실행하고, employee 추가 요청을 한다.
http://localhost:8080/add/employee?empId=100&firstName=Ted&secondName=mosby
요청을 하면 위의 이미지 처럼 after advice가 추가되어 콘솔에 메세지가 출력된다.
Around Advice
1. 위에서 작성한 EmployeeServiceAspect 애스팩트 클래스를 수정하여, around advice 로직을 추가한다.
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class EmployeeServiceAspect {
@Pointcut(value= "execution(* com.flab.realestateinvest.service.EmployeeService.*(..))")
private void logDisplaying() {
}
@Around(value = "logDisplaying()")
public void aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("The method aroundAdvice() before invokation of the method " + joinPoint.getSignature().getName() + " method");
joinPoint.proceed();
System.out.println("The method aroundAdvice() after invokation of the method " + joinPoint.getSignature().getName() + " method");
}
}
2. 스프링부트 프로젝트를 실행하고, employee 추가 요청을 한다.
http://localhost:8080/add/employee?empId=100&firstName=Ted&secondName=mosby
요청을 하면 위의 이미지 처럼 around advice가 추가되어 콘솔에 메세지가 출력된다.
After Returing Advice
· 조인 포인트의 실행이 정상적으로 완료된 후에 호출되는 어드바이스다.
- 즉, 예외가 발생하면 호출되지 않는다.
1. 위에서 작성한 EmployeeServiceAspect 애스팩트 클래스에 After Returning Advice 로직을 추가한다.
@Aspect
@Component
public class EmployeeServiceAspect {
@AfterReturning(value="execution(* com.flab.realestateinvest.service.EmployeeService.*(..)) and args(empId, fname, sname)")
public void afterReturningAdvice(JoinPoint joinPoint, String empId, String fname, String sname) {
System.out.println(joinPoint.getSignature().getName() + " 메서드 실행히 정상적으로 완료하였습니다.");
}
}
2. 스프링부트 프로젝트를 실행하고, employee 추가 요청을 한다.
http://localhost:8080/add/employee?empId=100&firstName=Ted&secondName=mosby
요청을 하면 위의 이미지 처럼 createEmployee 메서드가 정상적으로 종료되어, 콘솔에 메세지가 출력되는 것을 볼 수 있다.
3. 이번에는 EmployeeService 클래스를 다음과 같이 NumberFormatException이 발생하도록 수정한다.
import com.flab.realestateinvest.Employee;
import org.springframework.stereotype.Service;
@Service
public class EmployeeService {
public Employee createEmployee(String empId, String fname, String sname) {
int errNum = Integer.parseInt(fname);
Employee emp = new Employee();
emp.setEmpId(empId);
emp.setFirstName(fname);
emp.setSecondName(sname);
return emp;
}
public void deleteEmployee(String empId) {
}
}
요청을 하면 위의 이미지 처럼 createEmployee 메서드가 실행되던 도중 예외가 발생하여, After Returning Advice가 호출되지 않는 것을 알 수 있다.
이 처럼 After Returning Advice는 메서드가 정상적으로 실행 종료되는 경우에만 호출된다.
After Throwing Advice
· 메서드가 예외를 throw하는 경우 실행되는 어드바이스
TODO: 예시 추가
'스프링 > 스프링부트' 카테고리의 다른 글
[Spring Boot] 스프링 MVC 인터셉터란? 스프링 부트에서 사용하기 (0) | 2021.11.04 |
---|---|
스프링 부트 AutoConfigure가 작동하는 원리 (0) | 2021.10.26 |
[Spring Boot]스프링부트와 Gradle을 통해 Swagger 2 시작하기 (0) | 2021.05.22 |
[JUnit] 스프링부트 + junit5 환경에서 MockMvc로 컨트롤러 테스트하기 (0) | 2021.04.29 |
[Spring Boot] 오류 해결: (org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing) (2) | 2021.04.28 |
댓글