SY 개발일지
article thumbnail

스프링의 특징을 생각해보면 보통 DI와 IoC, 그리고 AOP를 주로 큰 특징으로 떠올립니다. 이번 포스팅에서는 AOP가 무엇인지에 대해 중점적으로 알아보도록 하겠습니다.

 

AOP란?

AOP(Aspect Oriented Programming)이란, 관점 지향 프로그래밍으로, 관점을 기준으로 다양한 기능을 분리하여 보는 프로그래밍입니다. 여기서 관점(Aspect)이란, 부가 기능과 그 적용처를 적용하고 합쳐서 모듈로 만든 것입니다.

Java는 OOP(Object Oriented Programming) 언어로서, OOP 에서 모듈화의 핵심 단위는 클래스인 반면, AOP에서 모듈화의 단위는 관점 Aspect 입니다. 따라서 어떠한 행위의 이전 시점, 이후 시점, 해당 시점 등이 중요합니다. 이러한 Aspect는 여러 타입과 객체에 거쳐서 사용되는 기능(Cross Cutting, 트랜젝션 관리 등)의 모듈화로 주 목적은 한번만 만들어놓고 재사용하고자 하는 것입니다.

AOP는 Spring 프레임워크의 필수요소는 아니지만, Spring 의 IoC를 보완하고 있습니다.

 

Cross Cutting과 Core Concern
Cross Cutting(횡단 관심사) 이란 공통 관심사항으로, 애플리케이션 전체를 관통하는 부가 기능 로직입니다.
Core Concern이란 핵심 관심사항으로, 말 그대로 핵심 비지니스 로직을 의미합니다. 

 

이러한 AOP는 다음과 같은 상황에서 주로 사용이 됩니다.

  • DB에서 트랜잭션 작업을 할 때, before에서 트랜젝션을 걸고, returning에서 commit하고, throwing에서 rollback하는 등의 작업
  • 로그인 시, before에서 로그인 여부를 확인하고, 로그인 사이트를 띄워주거나, 로그인이 되어 있다면 정상적으로 사이트를 보여주는 작업
  • 로그 기록

 

용어 정리

  • Aspect: 여러 클래스에 공통적으로 구현되는 관심사(Concern)의 모듈화
  • Join Point: 메서드 실행이나 예외처리와 같은 프로그램 실행 중의 특정 지점. Spring 에서는 메서드 실행을 의미합니다. 간단히 말해 공통 관심사들이 들어갈 수 있는 위치를 의미합니다.
  • PointCut: Join Point 에 Aspct를 적용하기 위한 조건을 서술하는 부분입니다. Aspect는 지정한 Pointcut에 일치하는 모든 Join Point에서 실행됩니다.
  • Advice: 특정 Joint Point에서 Aspect에 의해 취해진 행동입니다. Around, Before, After 등의 Advice 타입이 존재합니다.
  • Target 객체: 하나 이상의 Advice가 적용될 객체입니다. Spring AOP 는 Runtime Proxy를 사용하여 구현되므로 객체는 항상 Proxy객체가 됩니다.
  • AOP Proxy: AOP를 구현하기 위해 AOP 프레임워크에 의해 생성된 객체로써 Spring 프레임워크에서 AOP 프록시는 JDK Dynamic Proxy 또는 CGLIB Proxy가 존재합니다.
  • Weaving: Aspect 를 다른 객체와 연결하여 Advice 객체를 생성합니다. 런타임 또는 로딩 시 수행할 수 있지만, Spring AOP의 경우 런타임에 Weaving을 수행합니다.

 

Spring AOP 동작 원리

Spring AOP Proxy

실제 기능이 구현된 Target 객체를 호출하면, target이 호출되는 것이 아니라, Advice가 적용된 Proxy 객체가 호출됩니다. Spring AOP는 기본적으로 표준 JDK Dynamic Proxy를 사용하지만 인터페이스를 구현한 클래스가 아닌 경우 CGLIB 프록시를 이용한다고 합니다.

Spring AOP는 Proxy를 기반으로 한 Runtime Weaving 방식입니다. Spring AOP에서는 JDK Dynamic Proxy와 CGLIB을 통해 Proxy화 합니다. 

프록시 방식 (런타임 위빙)
프록시 방법에는 두 가지가 있습니다.
- JDK Dynamic Proxy: 동적 프록시 방법
Spring AOP의 기본 동작 방식으로서 Java의 Reflection 을 활용하여 동적으로 생성하게 됩니다. 즉, Reflection을 활용해 인터페이스를 탐색하고, 그만큼 동적으로 프록시를 생성합니다.
인터페이스를 기준으로 Proxy 인스턴스를 생성하는데, Reflection 은 AOP를 적용할 메소드가 아니라고 해도 인터페이스에 있는 메소드는 모두 프록시 객체 생성에 불러오기 때문에 상대적으로 CGLIB보다 성능이 낮습니다.
- CGLIB
Spring Boot AOP의 기본 동작 방식으로 상속을 기반으로 이루어집니다.

 

 

Spring에서 AOP 사용하기

먼저, AOP를 사용하기 위해서는 다음 의존성을 추가해야 합니다. 저는 build.gradle을 이용하였기 때문에 다음과 같이 추가해주었습니다.

implementation 'org.springframework.boot:spring-boot-starter-aop'

 

AOP를 사용하기 위해서는 다음 Annotation들을 알아야 합니다.

@Aspect AOP로 정의하는 클래스를 지정함
@Pointcut AOP기능을 메소드, Annotation 등 어디에 적용시킬지 지점을 설정 지점을 설정하기 위한 수식들이 매우 많음
@Before 메소드 실행하기 이전
@After 메소드가 성공적으로 실행 후
(예외가 발생하더라도 실행됨)
@AfterReturning 메소드가 정상적으로 종료될 때
@AfterThrowing 메소드에서 예외가 발생했을 때
@Around Before + After 모두 제어
(예외가 발생하더라도 실행됨)

 

AOP를 사용하는 간단한 예제인 Logging 기능을 한번 구현해보도록 하겠습니다.

 

AOP 적용 전

만약 AOP를 사용하지 않는다면 어떻게 해야할까요 ?

이 컨트롤러에서는 메서드로 들어오는 파라미터에 대해 로그를 남기고자 합니다. 이 때, 파라미터 뿐만 아니라 리턴되는 값에 대한 타입, 값 까지 모두 로그로 찍고 싶고, 이를 모든 메서드에 적용하고자 합니다.

@RestController
@Slf4j
public class MainController2 {

    @GetMapping("/api/test1")
    public String test1(@RequestBody DataDto dto) {

        log.info("Arg dto.id = {}", dto.getId());
        log.info("Arg dto.content = {}", dto.getContent());

        // [ 비즈니스로직 수행 ... ]

        log.info("Return dto.id = {}", dto.getId());
        log.info("Return dto.content = {}", dto.getContent());

        return dto.getContent();
    }

    @GetMapping("/api/test2")
    public String test2(@RequestParam String content) {

        log.info("Arg dto.content = {}", content);

        // [ 비즈니스로직 수행 ... ]

        log.info("Return dto.content = {}", content);

        return content;
    }

}

 

다음과 같이 로그를 작성하려 할 때, 모든 controller에 동일한 코드를 작성해야 하기 때문에 생산성이 좋지 않습니다. 따라서 관점으로 나누어 AOP 를 적용한 로그를 작성해보도록 하겠습니다.

 

AOP 적용 후

먼저 AOP 클래스를 작성해보겠습니다.

controller의 경우에는 AOP에 공통 로직을 모듈화시킬 것이기 때문에 다음과 같이 간략해지며 비지니스 로직에만 신경쓰면 됨을 확인할 수 있습니다.

@RestController
@Slf4j
public class MainController {

    @GetMapping("/api/test1")
    public String test1(@RequestBody DataDto dto) {

        // [ 비즈니스로직 수행 ... ]

        return dto.getContent();
    }

    @GetMapping("/api/test2")
    public String test2(@RequestParam String content) {

        // [ 비즈니스로직 수행 ... ]

        return content;
    }

}

 

다음은 AOP 클래스입니다.

@Slf4j
@Aspect
@Component
public class LogAop {

    // com.example.test.controller 이하 패키지의 모든 클래스 이하 모든 메서드에 적용
    @Pointcut("execution(* com.example.test.controller..*.*(..))")
    private void cut(){}

    // Pointcut에 의해 필터링된 경로로 들어오는 경우 메서드 호출 전에 적용
    @Before("cut()")
    public void beforeParameterLog(JoinPoint joinPoint) {
        // 메서드 정보 받아오기
        Method method = getMethod(joinPoint);
        log.info("======= method name = {} =======", method.getName());

        // 파라미터 받아오기
        Object[] args = joinPoint.getArgs();
        if (args.length <= 0) log.info("no parameter");
        for (Object arg : args) {
            log.info("parameter type = {}", arg.getClass().getSimpleName());
            log.info("parameter value = {}", arg);
        }
    }

    // Poincut에 의해 필터링된 경로로 들어오는 경우 메서드 리턴 후에 적용
    @AfterReturning(value = "cut()", returning = "returnObj")
    public void afterReturnLog(JoinPoint joinPoint, Object returnObj) {
        // 메서드 정보 받아오기
        Method method = getMethod(joinPoint);
        log.info("======= method name = {} =======", method.getName());

        log.info("return type = {}", returnObj.getClass().getSimpleName());
        log.info("return value = {}", returnObj);
    }

    // JoinPoint로 메서드 정보 가져오기
    private Method getMethod(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        return signature.getMethod();
    }

}

 

그렇다면 Postman으로 확인해보겠습니다.

다음과 같이 api 요청을 보내게 되면

로그가 잘 찍히는 것을 확인할 수 있습니다. 

이러한 결과는 test2 메서드에서도 잘 나온다는 것을 확인할 수 있습니다.

 

참고

profile

SY 개발일지

@SY 키키

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!