본문 바로가기
스프링/스프링부트

[Spring Boot] AOP란? 스프링부트에서 AOP 사용하기

by 책 읽는 개발자_테드 2021. 11. 10.
반응형

학습 목표

· 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: 소스 코드를 객체 코드르 변경함

https://www.javatpoint.com/spring-boot-aop

 

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: 예시 추가

 

https://www.javatpoint.com/spring-boot-aop

https://howtodoinjava.com/spring-aop-tutorial/

반응형

댓글