본문 바로가기

프로그래밍/Spring

[Spring] AOP

AOP(Aspect Oriented Programming)란

 AOP(Aspect Oriented Programming)는 OOP(Object Oriented Programming)에서 발생하는 문제 중 하나인, 관심사의 분리를 해결해주는 프로그래밍 패러다임이다.

 

 관심사의 분리란, 비즈니스 로직 외에 로깅, 보안, 트랜잭션과 같은 부가적인 기능이 필요할 때 이를 모든 메소드마다 일일이 추가하게 되면 코드가 중복되고 유지보수성이 떨어지기 때문에, 부가적인 기능들을 한 곳에서 관리하고 필요한 메소드들에 일괄적으로 적용하는 것을 얘기한다.

 

AOP 관심사 분리

 위 그림에서 Class A, Class B, Class C는 저마다 부가적인 로직(색깔 블록)이 중복되고 있다.

이때 중복되는 소스코드으로 인해 소스코드가 비대해 질 뿐만 아니라 부가적인 기능으로 인해 주된 비즈니스 로직(회색 블록)의 가독성이 떨어지게 된다.

 이러한 문제를 해결하기 위해 부가적인 로직을 분리하고 모듈화시킴(관심사 분리)으로서 문제를 해결할 수 있게된다.

 

  예를 들어, AOP를 사용하지 않고 로그인 기능을 구현한다고 할 때 모든 메소드에 로그인 체크 코드를 추가해야 하는데 이를 모든 메소드마다 일일이 추가하는 것은 개발자도 번거로울 뿐더러 코드의 중복성도 높아진다.

 이럴 때 AOP를 사용하면 이러한 공통적인 기능을 한 곳에서 처리하는 것이 가능해져서 코드 중복을 줄이고 비즈니스 로직을 헤지지 않아 추후에 유지보수성 또한 높일 수 있게 된다.

AOP 관련 용어

  • Target: Aspect가 적용되는 대상으로, 클래스, 메서드 등이 될 수 있다.

  • Advice: 부가기능을 구현한 구현체로, Target에 적용될 실질적인 기능을 담고 있다.

  • Aspect: 관심사 분리를 위해 모듈화한 것으로, 부가기능의 구현체(Advice)와 적용 대상(Target)을 포함한다.

  • Join Point: Advice가 적용될 수 있는 위치 또는 시점으로, 메서드 실행 시점, 생성자 호출 시점, 필드 접근 시점 등이 될 수 있다.

  • Pointcut: Join Point의 상세한 스펙을 정의한 것으로, Advice가 적용될 Join Point를 선별하는 역할을 한다. "A 메서드의 진입 시점에서 호출할 것"과 같이 구체적인 Join Point를 정의한다.

AOP 어노테이션 종류

 Spring에서는 어노테이션을 통해 AOP를 보다 쉽게 구현할 수 있도록 해준다.

종류 내용
@Before 타겟 메소드 실행 이전에 Advice를 실행한다.
@After 타겟 메소드 실행 이후에 Advice를 실행한다.
@AfterReturning  타겟 메소드의 정상적인 반환 이후에 Advice를 실행한다.
@AfterThrowing 타겟 메소드에서 예외가 발생한 이후에 Advice를 실행한다.
@Around 타겟 메소드 실행 전/후에 Advice를 실행하며, 타겟 메소드를 직접 실행하는 책임을 가진다.
@Pointcut Advice를 적용할 Join point를 정의한다.
@Aspect 여러 Advice와 Pointcut을 하나로 모아놓은 Aspect를 정의한다.
@Aspect 여러 Advice와 Pointcut을 하나로 모아놓은 Aspect를 정의한다.
@DeclareParents 동적으로 인터페이스를 구현한 프록시 객체를 만드는데 사용된다.

 

코드 예제

 간단한 계산기 클래스를 생성 한 후 계산 메소드 실행 전에 어떤 함수와 어떤 수를 대입했는지에 대한 로그와 계산 메소드 실행 후에 계산 메소드가 끝났음을 알리는 로그을 찍는 예시이다.

 

1. 의존성 추가

 

maven

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

gradle

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

 

 

2. AOP를 적용할 Calculator 클래스 생성

public class Calculator {

   public int add(int a, int b) {
       return a + b;
   }

   public int subtract(int a, int b) {
       return a - b;
   }

   public int multiply(int a, int b) {
       return a * b;
   }

   public int divide(int a, int b) {
       return a / b;
   }
}

 

 

3. 계산 메소드 실행 전 후에 각각 로그를 찍을 클래스 생성

@Aspect
@Component
public class LoggingAspect {

   private final Logger logger = LoggerFactory.getLogger(this.getClass());

   @Before("execution(* com.example.demo.Calculator.*(..))")
   public void logBefore(JoinPoint joinPoint) {
       logger.info("The method " + joinPoint.getSignature().getName() + "() begins with " + Arrays.toString(joinPoint.getArgs()));
   }

   @After("execution(* com.example.demo.Calculator.*(..))")
   public void logAfter(JoinPoint joinPoint) {
       logger.info("The method " + joinPoint.getSignature().getName() + "() ends");
   }
}

 위 소스코드에서 사용된 Pointcut은 com.example.demo.Calculator 클래스의 모든 메소드를 대상으로 적용되는 것을 의미한다.

 *는 임의의 리턴 타입을 의미하고, com.example.demo.Calculator.*는 Calculator 클래스 내부의 임의의 메소드를 나타낸다.

 (..)는 메소드 인자가 없는 메소드에서부터 임의의 인자를 가지는 메소드 까지의 모든 경우에 적용되는 것을 의미한다.

 

 즉, Calculator 클래스 내부의 모든 메소드가 대상이 된다.

 

 

4. 메인클래스에서 AOP가 적용된 Calculator 객체를 만들어서 사용

@SpringBootApplication
public class DemoApplication {

   public static void main(String[] args) {

       SpringApplication.run(DemoApplication.class, args);

       Calculator calculator = new Calculator();
       int result = calculator.add(1, 2);
       System.out.println("Result: " + result);
   }
}

 

 

5. 로그 확인

2023-03-28 20:00:00.000  INFO 12345 --- [main] c.e.demo.aspect.LoggingAspect: The method add() begins with [1, 2]
Result: 3
2023-03-28 20:00:00.000  INFO 12345 --- [main] c.e.demo.aspect.LoggingAspect: The method add() ends