Backend/Spring AOP

[Spring] AOP(Aspect-Oriented Programming)란?

persi0815 2024. 12. 14. 22:09
'초보 웹 개발자를 위한 스프링 5 프로그래밍 입문' 챕터 7을 읽고 정리한 내용입니다. 

 

🌊 AOP란? 

Aspect-Oriented Programming, 관점 지향 프로그래밍
소스 코드의 비즈니스 로직과 부가적인 공통 관심사를 분리하여, 관점을 기준으로 각각 모듈화하여 코드의 모듈성과 가독성을 향상시키는 프로그래밍 패러다임.

* ’관점’이라는 말이 너무 어색해서 알아봤더니, ‘초보 웹 개발자를 위한 스프링 5 프로그래밍 입문 158p’에서 ‘관점’이라는 말 대신 ‘기능’ 내지 ‘관심’이라고 표현하는 것이 더 알맞다고 한다.
* 모듈화: 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것

 

즉, 여러 객체에 공통으로 적용할 수 있는 기능을 분리해서 Aspect로 모듈화하여 재사용성을 높여주는 기법이다. 핵심 기능과 공통 기능(흩어진 관심사)의 구현을 분리함으로써 핵심 기능을 구현한 코드의 수정 없이 공통 기능을 삽입할 수 있게 만들어 준다.

  • 흩어진 관심사(Crosscutting Concerns): 소스 코드 상 다른 부분에 계속 반복해서 사용하는 코드들

 

핵심 기능에 공통 기능을 삽입하는 방법

1. 컴파일 시점에 코드에 공통 기능을 삽입하는 방법

: AOP 개발 도구가 소스 코드를 컴파일 하기 전에 공통 구현 코드를 소스에 삽입하는 방식으로 동작

 

2. 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법

: 클래스를 로딩할 때 바이트 코드에 공통 기능을 클래스에 삽입하는 방식으로 동작

 

3. 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법

: 스프링이 제공하는 방식

 

1,2번은 스프링 AOP에서 지원하지 않으며, AspectJ와 같이 aop 전용 도구를 사용해서 적용할 수 있다.

 

🌿 Spring AOP

Spring AOP는 Aspect 클래스와 Target Object를 기반으로 프록시 객체를 자동으로 생성”하여 원래의 대상 객체를 감싸고, 메서드 호출 시 프록시가 가로채서 부가적인 로직을 실행한다. (관리도 알아서 해줌)
→ 인터페이스가 있다면, 인터페이스 기반의 프록시 객체를 생성해준다.
→ 인터페이스가 없다면, 구체 클래스 기반의 프록시 객체를 생성해준다.

⇒ 공통 기능을 구현한 클래스만 알맞게 구현하면 되고, 프록시 클래스는 직접 구현할 필요가 없다.
그저, 공통기능을 구현한 클래스를 별도로 작성한 뒤, AOP의 Aspect로 등록만 하면 된다.

 

스프링은 프록시를 이용해서 메서드 호출 시점에 Aspect를 적용한다.

  • Proxy 객체: 핵심 기능의 실행은 다른 객체에 위임하고, 부가적인 기능을 제공하는 객체 (대상 객체의 대리인. 대상 객체의 동작을 확장하거나 변경)
    • proxy 객체: 접근 제어 관점에 초점.
    • decorator 객체: 기능 추가와 확장에 초점
  • Target 객체: 실제 핵심 기능을 실행하는 객체

 

📌 주요 개념

  • Aspect: 공통적으로 적용되는 부가적인 기능(예: 로깅, 권한 검사). 실제로 구현된 코드
  • Advice: 언제 공통 관심 로직을 핵심 로직에 적용할지를 정의
    • Before: 메서드 실행 전에 실행
    • After: 메서드 실행 후에 실행
    • AfterReturning: 메서드가 정상적으로 종료된 후 실행
    • AfterThrowing: 메서드에서 예외가 발생한 후 실행
    • Around: 메서드 실행 전후에 실행 (가장 널리 사용됨)
      • 대상 객체의 메서드를 실행하기 전/후, 익셉션 발생 시점 등 다양한 시점에 원하는 기능을 삽입 가능
  • Join Point: Advice가 적용 가능한 지점(예: 메서드 호출, 필드 접근 등)
    • 스프링은 프록시를 이용해서 AOP를 구현하기 때문에 메서드 호출에 대한 Join point만 지원
    • AspectJ는 바이트코드 레벨에서 동작하는 완전한 aop 프레임워크로, join point의 범위가 훨씬 넓음
  • Pointcut: Join Point를 필터링하는 표현식. 실제 advice가 적용되는 join point를 나타냄
  • Weaving: Advice를 핵심 로직 코드에 적용하는 것

 

execution 명시자

: Advice를 적용할 메서드를 지정할 때 사용

@Pointcut("execution(* LuckyVicky.backend..*.controller.*.*(..)) || "
        + "execution(* LuckyVicky.backend..*.service.*.*(..)) ||"
        + " execution(* LuckyVicky.backend..*.repository.*.*(..))")
public void applicationLayerPointcut() {
}

 

execution(수식어패턴 ? 리턴타입패턴 클래스이름패턴?메서드이름패턴(파라미터패턴)

- 수식어패턴: 생략 가능. public, protected가 옴. spring에서는 public 메서드에만 적용 가능.
- 리턴타입패턴: 리턴타입 명시.
- 클래스 이름패턴, 메서드 이름 패턴: 클래스 이름 및 메서드 이름을 패턴으로 명시
- 파라미터패턴: 매칭될 파라미터에 대해 명시

 

@Pointcut 애노테이션이 아닌 @Around 애노테이션에 execution 명시자를 직접 지정할 수도 있다.

@Around("execution(* LuckyVicky.backend..*.controller.*.*(..)) || "
        + "execution(* LuckyVicky.backend..*.service.*.*(..)) ||"
        + " execution(* LuckyVicky.backend..*.repository.*.*(..))")
public Object logError(ProceedingJoinPoint joinPoint) throws Throwable {

 

만약, 같은 Pointcut을 여러 Advice가 함께 사용한다면, 공통 pointcut을 재사용할 수도 있다. 

아래 코드 처럼 pointcut 애노테이션이 붙은 메소드를 public으로 정의해주면 된다!

@Aspect
@Component
public class ErrorLoggingAspect {

    private static final Logger logger = LoggerFactory.getLogger(ErrorLoggingAspect.class);

    @Pointcut("execution(* LuckyVicky.backend..*.controller.*.*(..)) || "
            + "execution(* LuckyVicky.backend..*.service.*.*(..)) ||"
            + " execution(* LuckyVicky.backend..*.repository.*.*(..))")
    public void applicationLayerPointcut() { 
    }
    // public이라 다른 클래스에 위한 Advice에서도 해당 pointcut 사용할 수 있음!

    @AfterThrowing(pointcut = "applicationLayerPointcut()", throwing = "exception")
    public void logError(JoinPoint joinPoint, Throwable exception) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getMethod().getName();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        Object[] args = joinPoint.getArgs();

        logger.error("!! [ERROR] Exception in {}.{}() with arguments: {}", className, methodName, args);
        logger.error("Exception: {}", exception.getMessage(), exception);
    }
}

 

📌 Aspect가 여러개라면?

어떤 Aspect가 먼저 적용될지는 스프링 프레임워크나 자바 버전에 따라 달라질 수 있기에 적용 순서가 중요하다면, 직접 순서를 지정해야 한다. 이때 @Order를 사용할 수 있다. 

  • @Order(_) 값이 작으면 먼저 적용하고, 크면 나중에 적용한다.

 

📌 @Aspect와 컴포넌트 스캔

@Aspect가 컴포넌트 스캔 대상이기에 @Aspect가 붙기만 하면, 해당 클래스는 컨테이너가 빈으로 등록해준다.

그래서, Spring AOP를 사용할 때 @Aspect 애노테이션을 붙인 클래스는 자동으로 컴포넌트 스캔 대상이 되지만, @EnableAspectJAutoProxy 애노테이션을 통해 AOP 설정이 활성화가 된 경우에 작동이 된다.

 

다만, Spring Boot에서는 기본적으로 활성화되어 있기에 따로 신경 써주지 않아도 된다. 하지만, Spring Boot를 사용하지 않는 순수 Spring Framework 프로젝트에서는 @EnableAspectJAutoProxy를 명시적으로 선언해야 AOP 기능이 활성화된다.

@EnableAspectJAutoProxy 애노테이션
- Spring AOP를 활성화하는 애노테이션
- @Aspect 클래스가 스캔되고 프록시가 생성 

 

@Aspect만으로도 Spring AOP가 활성화되면 해당 클래스가 컴포넌트 스캔의 대상이 되지만, 명시적으로 빈 등록을 보장하거나 혼동을 줄이기 위해 @Component를 추가하는 것이 일반적이라고 한다.

 

이에 대한 장점이..

1. AOP 설정이 비활성화되어도 해당 클래스가 스프링 빈으로 등록되고, AOP가 아닌, 다른 방식으로도 이 클래스의 기능을 활용할 수 있다.

2. @Aspect 클래스도 일반적인 스프링 빈처럼 컴포넌트 스캔 대상임을 명확히 하기 위해 @Component를 함께 사용하는 것이 관례라고 한다.

* @Controller, @Service, @Repository, @Configuration은 모두 @Component의 특수 애노테이션이다. 그래서 @Component를 함께 사용하면 더 명시적으로 스캔 대상임을 보일 수 있다.

 

📌 AOP 사용 사례

  1. 로깅(Logging)
    메서드 호출 및 응답 시간을 기록하거나, 예외가 발생했을 때 해당 정보를 로그로 남기는 데 활용된다. 이를 통해 디버깅이나 문제 분석 시 유용한 정보를 손쉽게 수집할 수 있다.
  2. 트랜잭션 관리(Transaction Management)
    데이터베이스 트랜잭션의 시작, 커밋, 롤백과 같은 처리를 메서드 실행 전후에 자동으로 적용할 수 있다. 이를 통해 코드 중복을 줄이고 트랜잭션 관리 로직을 일관되게 유지할 수 있다.
  3. 권한 검사(Authorization)
    특정 메서드가 호출되기 전에 사용자 권한을 확인하여, 권한이 없는 사용자의 접근을 차단하는 로직을 분리하여 구현할 수 있다. 이를 통해 보안 로직을 중앙 집중화할 수 있다.
  4. 성능 모니터링(Performance Monitoring)
    메서드의 실행 시간을 측정하여 성능 병목 지점을 파악하거나 최적화가 필요한 부분을 식별할 수 있다. 주로 애플리케이션의 성능을 모니터링하고 개선하는 데 사용된다.

이러한 사례들은 애플리케이션의 핵심 로직에 영향을 주지 않으면서도, 부가적인 로직을 깔끔하게 관리할 수 있도록 도와준다. AOP를 적절히 활용하면 코드의 가독성과 재사용성을 크게 향상시킬 수 있다.

 

AOP를 이용한 "로깅" 예시 코드를 보려면 아래 포스팅을 참고하자!

https://persi0815.tistory.com/118

 

참고하면 좋을 자료

https://tecoble.techcourse.co.kr/post/2022-11-07-transaction-aop-fact-and-misconception/

 

AOP에 대한 사실과 오해 그런데 트랜잭션을 사알짝 곁들인..

들어가며 트랜잭션을 공부하다보면서 자연스럽게 AOP에 대해 접하게 되었습니다. AOP를 사용하게 되면서 직접 경험했던 사실과 오해에 대해서 공유하려고 합니다. 첫 번째 사실: public이외의 메서

tecoble.techcourse.co.kr