본문 바로가기

spring

AOP와 스프링 AOP란?

AOP(Aspect-Oriented Programmig)는 관점 지향 프로그래밍으로 프로그램의 모듈성을 높이기 위해 횡단 관심사(Cross-Cutting Concern)를 분리하는 프로그래밍 패러다임입니다.

 

애플리케이션의 핵심 비즈니스 로직과 상관없이 공통으로 적용되는 부가 기능(예시로는 로깅, 트랜잭션 관리, 보안 등등)을 별도의 모듈로 관리함으로써 코드 중복을 줄이고, 핵심 비즈니스 로직에만 집중할 수 있게 합니다.

 

 

사실 말로만 들었을 땐, 이해도 안 가고 무슨 말인지 감도 안 잡혔습니다, 뭐 모듈화에 중점을 둔 매커니즘이라는 것은 알겠는데 하나의 방법론인가? 라는 생각까지 했지만 알아보니 그런 단순한 개념은 아니였었습니다.

 

위에서 말하는 횡단 관심사는 여러 모듈에서 반복적으로 발생하는 로직이나 기능을 의미하는 데요, 예를 들면 여러 서비스 메서드에서 실행 시간을 측정해야 한다면 이를 각각의 메서드에 구현하게 된다고 합시다.

 

그렇다면 우선 코드 중복이 발생하게 됩니다. 이러한 코드 중복은 전염성을 가지고 있어서 이를 필요로 하는 구현체들마다 해당 코드를 반복하게 되고 결국 요구사항의 추가로 수정이 필요하게 될 시, 수많은 수정 작업을 거쳐야 하게 되죠

 

이러한 부가 로직을 하나의 모듈로 분리하고 이를 필요한 곳에 적용하는 방식이 바로 AOP의 핵심이라고 이해하시면 됩니다. 그렇다면 한 번 코드로 알아보죠

 

문제 상황 속 코드

public interface Calculator {
    int calculate(int a, int b);
}

public class SimpleCalculator implements Calculator {
    @Override
    public int calculate(int a, int b) {
        return a + b;
    }
}

 

 

간단한 인터페이스와 구현체를 두어봤는데요, 단순한 계산 기능을 수행할 뿐인 단순 로직입니다. 그런데 만약 회사에서 해당 구현체의 실행 시간을 알고싶다는 요구사항이 추가가 되었다고 가정해봅시다.

 

public interface Calculator {
    int calculate(int a, int b);
}

public class SimpleCalculator implements Calculator {
    @Override
    public int calculate(int a, int b) {
    
    long start = System.currentTimeMails();
    try {
        return a + b;
    } finally {
    long end = System.currentTimeMails();
 	System.out.printf("계산에 걸린 시간: %d", (end - start));
}

 

요구사항 대로, 시간을 구하는 로직을 추가하게 되었네요, 그렇다면 문제가 끝났을까요? 만약 여기서 다양한 계산 구현체들이 확장 된다고 생각해봅시다.

 

그리고 회사의 요구가 이어져요, 실행시간을 Simple 계산기에서 구현했던 것처럼, 모든 구현체들의 실행시간도 구해주세요! 라고 가정해봅시다. 현재는 단순해보이겠지만, 구현체들이 많아지면 많아질수록 모든 구현체들에게 시간을 구하는 로직을 추가하게 될 것입니다. 

 

추가 작업도 오래걸릴텐데, 마일 초에서 나노 초로 변경해달라는 요구사항까지 생긴다면 수정 작업도 오래걸릴테고, 결국 손이 많이 가게 되는 비 효율적인 코드가 탄생한 순간입니다 🤔


 

문제 파악과 해결 방안

 

우리는 여기서 "프록시"를 사용하여서 해결할 수 있습니다. 우선 프록시는 대리자라는 뜻으로, 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 대리자 역할을 하게 됩니다.

 

프록시는 실제 대상인 것처럼 위장함으로서 이를 사용하는 클라이언트는 구체 클래스를 알 필요가 없으며 클라이언트의 요청을 받아서 원래 요청 대상에게 바로 넘겨주는 게 아닌 다양한 부가기능을 지원할 수 있습니다.

 

프록시는 어떠한 사용 목적이냐에 따라서 패턴이 달라지게 되는 데,

클라이언트가 타깃에 접근하는 방법을 제어하기 위해서는 프록시 패턴
타깃에 부가적인 기능을 부여해주기 위해서는 데코레이터 패턴

 

우리는 해당 데코레이터 패턴이라고 칭해지는 방법을 사용하면 됩니다, 먼저 현재 문제는 중복된 코드 구현과, 핵심 비즈니스 로직에 집중하지 못하고 있었습니다.

 

그렇다면 프록시 구현체를 만들어, 계산기 구현체에 주입하여 시간을 같이 구해주는 프록시 구현체를 구현한다면, 각 유형의 계산기는 시간을 구현하는 기타 인프라 로직에 신경쓰지 않고, 핵심 비즈니스 로직만을 보면 되며, 클라이언트는 프록시 구현체만을 보면 되기에 훨씬 계층 분리가 수월하게 진행된 코드가 됩니다.

 

public class TimedCalculator implements Calculator {
    private final Calculator calculator;

    public TimedCalculator(Calculator calculator) {
        this.calculator = calculator;
    }

    @Override
    public int calculate(int a, int b) {
        long start = System.nanoTime(); // 나노초로 요구사항 추가
        try {
            return calculator.calculate(a, b);
        } finally {
            long end = System.nanoTime();
            System.out.printf("계산에 걸린 시간: %d", (end - start));
        }
    }
}

 

이렇게 코드가 추가가 된다면, 더 이상 중복 로직이 발생하지 않고 수정 사항이 생겨도 한 객체만을 수정하면 되기에 유지보수성도 크게 향상이 된 것을 볼 수 있습니다. 👍

 

결국 AOP는 횡단 관심사의 분리를 허용한다란, 인프라 로직 같이 핵심 비즈니스 로직을 제외한 부가로직들의 분리를 허용함으로 모듈성을 증가시키며 반복 작업을 줄이고 개발자는 핵심 기능 개발에만 집중할 수 있게 해주는 매커니즘이죠


 

AOP와 스프링 AOP?

 

AOP는 위에서 설명한 개념과 동일하고, 스프링에서도 해당 AOP를 차용하고 있는데요, 스프링 AOP는 AOP 개념을 스프링 프레임워크 안에서 구현한 것입니다.

 

먼저 AOP가 핵심 기능에 공통 기능을 삽입하는 방법엔 종류가 있는데요.

 

컴파일 시점에 코드에 공통기능 삽입, 

 

클래스 로딩 시점에 바이트 코드에 공통기능 삽입,

 

런타임 시점에 프록시 객체를 생성하여 공통 기능 삽입

 

Aop 프레임워크인 aspectJ가 제공하는 컴파일러나, 클래스 로더 조작기 같은 새로운 것을 추가하여서 좀 더 유연성을 가질 수 있지만 부가적인 의존성을 지녀야 하기에, 스프링 AOP는 주로 프록시 기반으로 동작하여 런타임 시점에만 AOP를 적용합니다.  

시점 컴파일 타임, 클래스 로딩 타임, 런타임 모두 가능 런타임 시점 (프록시 기반)
대상 모든 JoinPoint (메서드, 필드 접근, 객체 생성 등) 메서드 실행 시점에만 적용 가능
범위 필드 접근, 객체 생성 등 광범위한 적용 가능 스프링 빈에 한정된 메서드 실행 시점에서만 적용
종속성 특정 언어나 프레임워크에 종속되지 않음 스프링 프레임워크에 종속됨
대표 구현체 AspectJ, JBoss AOP 등 스프링 프레임워크 내에서만 사용

 

 

그렇다면 AOP의 용어들에는 뭐가 있을까요?

Aspect 부가 기능을 모듈화한 것으로 @Aspect 어노테이션을 사용하여 스프링에서 정의
JoinPoint 부가 기능이 적용될 수 있는 지점. 주로 메서드 호출, 객체 생성 등이 JoinPoint가 될 수 있다
Advice 실제로 실행되는 부가 기능의 로직
PointCut 부가 기능이 적용될 위치를 선별하는 표현식입니다. 특정 패키지나 클래스, 메서드 이름 등을 기준으로 PointCut을 설정
Target 부가 기능을 적용할 대상 객체

 

 

@Aspect
@Component
public class ExecutionTimeAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.nanoTime();
        try {
            return joinPoint.proceed(); 
        } finally {
            long end = System.nanoTime();
            System.out.println("Execution time: " + (end - start) + "ns");
        }
    }
}