라이브러리&프레임워크/Spring

AOP란 무엇이고 필요한 이유, 프록시패턴/데코레이터 패턴

youngble 2025. 11. 9. 21:37
 

요약

코드의 횡단 관심사와 핵심 관심사가 섞이면 코드 중복과 결합도 증가하고 핵심 의도를 파악하기 어려워 가독성 과 유지보수가 저하된다. 따라서 핵심 관심사로부터 횡단 관심사를 분리하기 위한 목적으로 AOP를 적용한다.

AOP는 실제 핵심 관심사를 프록시로 감싸 부가기능을 주입하는 구조를 가지고,
AOP가 어떤 패턴인지에 대해서는 의도에 따라 달라지는데, 제어가 목적이면 프록시패턴, 부가 기능 확장이 목적이면 데코레이터 패턴으로 본다.

이러한 개념을 바탕으로 스프링에서는 @Aspect, @Around 같은 어노테이션을 통해 AOP를 구현할 수있으며, @Transactional또한 프록시 패턴기반의 AOP가 적용된 사례이다.

필요한 이유

횡단 관심사


여러부분에 공통적으로 적용되는 (부가적인) 기능

횡단 관심사와 핵심 관심사 코드 구조

횡단 관심사코드가 섞인 예시코드

public class UserService {

    public void signUp(UserRequest request) {
        log.info("회원가입 시도: {}", request.email());

        try {
            transactionManager.begin();
            userRepository.save(request.toEntity());
            transactionManager.commit();
        } catch (Exception e) {
            transactionManager.rollback();
            log.error("회원가입 실패", e);
        }
    }
}

여러개의 클래스 객체의 핵심 로직에서 부가적으로 반복해서 사용되는 횡단 관심사 코드(로깅, 보안 검사, 트랜잭션 관리 등)는 중복코드가 발생할뿐아니라 해당 관심사에 관한 로직만 있는게아닌 여러 관심사가 섞어서 다루기 때문에 비대해지고 강한 결합도를 가지고 응집도를 낮추어 좋지않다.
또한 하나의 모듈에 여러 관심사가 섞이니 무엇이 핵심 로직(행동)인지 보기어렵게 하기때문에 코드가독성, 유지보수성에도 좋지않다.

AOP 적용 전 후

따라서 이러한 부가적인 공통 관심사를 핵심 관심사 코드로부터 분리하여 aspect로 모듈화하여 사용한다. 이렇게 핵심 관심사와 공통 관심사를 나누기때문에 각각의 관심사만 집중한 코드를 작성할수있고 흐터져있던 코드를 하나의 장소에 응집되기때문에 재사용이 가능하고 하나의 관심사로 응집도가 높아지기에 유지보수하기 좋게해주기때문에 객체지향 프로그래밍에 적합(여러가지 원칙중 SRP 단일 책임 원칙이 해당)하게해준다.

AOP(Aspect Oriented Programming)
관점지향 프로그래밍, 핵심 관심사, 부가 관심사(횡단 관심사)를 분리하는 관점을 기준으로 프로그래밍 한다는 의미를 강조하기위해 붙여진 이름

그래서 어떻게 분리할것이냐?

AOP 구성

AOP는 프록시(Proxy)구조 기반으로 동작한다.

프록시

Proxy는 대리인, 대변인이라는 뜻으로 실제 대상(타깃)인것처럼 위장하여 요청에 대해서 대신 받아 수행하는 것이다.

즉, 타깃에는 핵심 관심사 코드만 수행하도록 코드가 작성하게 되고, 프록시에는 횡단관심사(부가기능)을 추가적으로 넣음으로써 클라이언트입장에서는 마치 실제 타깃이 하는것처럼 보이는 것이다.

여기서 중요한 것은 프록시를 어떤 목적으로 사용하는지에 따라 프록시패턴 또는 데코레이터 패턴이 된다.

프록시패턴?

프록시 패턴은 프록시의 대리수행의 목적이 클라이언트가 타깃에 접근 제어(지연, 캐싱, 보안 등)의 목적을 가진다.

예를들어 보안이라는 대리수행을 한다면, 클라이언트가 타깃에 요청해 작업을 수행할 수있는 권한이 있는지 미리 검사하고 긍정적일때만 클라이언트 요청을 타깃에게 전달하는 것이다.

데코레이터 패턴 ?

데코레이터 패턴은 대리수행의 목적이 타깃에 부가기능 혹은 확장기능을 감싸서 사용하는 목적을 가진다.

 

두가지의 공통된 구조

public interface Service {
    void execute();
}

public class RealService implements Service {
    @Override
    public void execute() {
        System.out.println("실제 로직 수행");
    }
}

public class ServiceProxy implements Service { // 또는 Decorator
    private final Service target;

    public ServiceProxy(Service target) {
        this.target = target;
    }

    @Override
    public void execute() {
    	// 앞뒤로 무언가 추가
        System.out.println("부가 로직: 로그 시작");
        target.execute(); // 핵심 로직 위임
        System.out.println("부가 로직: 로그 종료");
    }
}

그래서 AOP는 무슨 패턴이라는건데?

의도와 목적에 따라서 프록시패턴이 될수도 데코레이터 패턴이 될수도있다.
트랜잭션이나 로깅 등 핵심 로직을 감싸는 부가 기능이지만, AOP 프록시가 어떻게 의도적으로 사용하는지에 따라 패턴이 결정되는것이다.
따라서 트랜잭션은 Proxy패턴, 로깅은 Decorator 패턴으로 분류한다. 때문에 AOP를 설명할때는 하나의 패턴이라 하지않고 프록시구조를 사용한 프록시패턴 혹은 데코레이터 패턴으로 횡단 관심사를 핵심 관심사에서 분리하는 것이다 라고 해야한다.

실제 애플리케이션 개발 사례

사용하기 전 서비스 코드

public class OrderService {

    private final TransactionManager txManager;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public void placeOrder() {
        Transaction tx = txManager.begin();
        try {
            inventoryService.decreaseStock(...);
            paymentService.charge(...);
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            throw e;
        }
    }
}

재고를 감소시키고, 결제를 부가하고 난후 commit으로 DB를 확정짓는 코드이다.
Service라는 조율자는 도메인 로직을 어떤 순서로 어떻게 실행할지 조율하는 것이지 트랜잭션과같은 실행 제어까지는 다루는 목적의 레이어가 아니기때문에 이를 분리해야한다.

스프링 AOP 의 @Transactional
트랜잭션 관리를위한트랙잭션 Proxy AOP

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public OrderService(InventoryService inventoryService,
                        PaymentService paymentService) {
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
    }

    @Transactional
    public void placeOrder(OrderRequest request) {
        // 오직 비즈니스 흐름만
        inventoryService.decreaseStock(request.itemId(), request.quantity());
        paymentService.charge(request.userId(), request.totalPrice());
    }
}

AOP가 대신 트랙잭션 제어시
@Transactional의 AOP(프록시패턴) 실제 내부구조 코드

class OrderServiceProxy implements OrderService {
    private final OrderService target;
    private final TransactionManager txManager;

    public void placeOrder() {
        TransactionStatus tx = txManager.begin();
        try {
            target.placeOrder(); // 핵심 비즈니스 수행
            txManager.commit(tx);
        } catch (Exception e) {
            txManager.rollback(tx);
            throw e;
        }
    }
}

더나아가 Advanced...

다중 부가기능 조합 AOP를 원한다면?

스프링 AOP 사용
@Aspect, @Around 사용 스프링 AOP 등록

@Aspect
@Component
public class LoggingAspect {

    @Around("execution(* com.example.order.service..*(..))")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("[Log] 시작");

        Object result = pjp.proceed();  // 실제 대상(OrderService) 실행

        System.out.println("[Log] 종료");
        return result;
    }
}

핵심 관심사 서비스 로직

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public OrderService(InventoryService inventoryService,
                        PaymentService paymentService) {
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
    }

    @Transactional
    public void placeOrder(OrderRequest request) {
        // 오직 비즈니스 흐름만
        inventoryService.decreaseStock(request.itemId(), request.quantity());
        paymentService.charge(request.userId(), request.totalPrice());
    }
}

스프링 AOP 내부 동작 구조 간단 예시 코드

OrderService real = new RealOrderService();

TransactionManager txManager = ...;

OrderService txProxy     = new TransactionOrderServiceProxy(real, txManager);
OrderService loggingProxy = new LoggingOrderServiceProxy(txProxy);

// 클라이언트는 loggingProxy만 봄
loggingProxy.placeOrder("ORDER-123");

흐름도

Client
 └─▶ LoggingAspect (@Aspect)
     └─▶ TransactionInterceptor (@Transactional)
         └─▶ OrderService.placeOrder()

클라이언트

(스프링이 만든 프록시 체인)

[LoggingAspect @Around]

[TransactionInterceptor (@Transactional 처리)]

[진짜 OrderService.placeOrder()]

 

레퍼런스
https://adjh54.tistory.com/133

https://leeeeeyeon-dev.tistory.com/49

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%94%84%EB%A1%9D%EC%8B%9CProxy-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90