공유된 기술노트 · 스프링부트 · 스프링부트 API

newlec · 2026. 9. 20.

1-3. AOP와 트랜잭션 — 스프링이 마법을 부리는 원리

1. ncafe 주문 시나리오: 비즈니스 로직에 섞인 '잡일'

ncafe 백엔드에서 OrderServicecreateOrder 메서드를 다시 떠올려 봅시다. 1-2에서 우리는 OrderRepository를 주입받아 주문을 저장하는 코드를 작성했습니다. 하지만 실제 운영 환경에서는 단순히 주문을 저장하는 것만으로는 부족합니다.

주문이 생성되는 시점에 다음과 같은 일들이 동시에 일어나야 합니다.

  1. 로그 기록: "사용자 1001이 라떼를 주문했습니다."
  2. 권한 확인: 이 사용자가 실제로 주문 권한이 있는지 확인.
  3. 재고 차감: 커피 원두 재고를 1개 줄임.
  4. 데이터 저장: 주문 내역을 데이터베이스에 기록.

만약 이 모든 로직을 createOrder 메서드 안에 직접 코딩한다면 어떻게 될까요?

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryRepository inventoryRepository;
    private final Logger logger;

    public OrderService(OrderRepository orderRepository, 
                        InventoryRepository inventoryRepository,
                        Logger logger) {
        this.orderRepository = orderRepository;
        this.inventoryRepository = inventoryRepository;
        this.logger = logger;
    }

    public void createOrder(Order order) {
        // 1. 로깅
        logger.info("주문 시작: {}", order.getId());
        
        // 2. 권한 확인 (가정)
        if (!hasPermission(order.getUserId())) {
            throw new SecurityException("권한 없음");
        }

        // 3. 재고 차감
        inventoryRepository.decrease(order.getProductId(), 1);

        // 4. 주문 저장
        orderRepository.save(order);
        
        // 5. 로깅
        logger.info("주문 완료: {}", order.getId());
    }
}

이 코드는 동작합니다. 하지만 문제가 있습니다.

  • OrderService는 "주문을 만드는 것"이 본업인데, 로깅, 권한, 재고 관리라는 부수적인 일까지 하고 있습니다.
  • 만약 PaymentServiceRefundService에서도 로깅과 권한 확인이 필요해지면, 그 코드에도 똑같은 로깅과 권한 코드를 복사해서 붙여넣어야 합니다.
  • 복사-붙여넣기(Copy-Paste)는 유지보수의 적입니다.

2. 관심사 분리: 횡단 관심사(Cross-Cutting Concern)

소프트웨어 설계에서 우리는 문제를 관심사(Concern)로 나누어 생각합니다.

  • 핵심 관심사(Core Concern): 도메인의 본질적인 비즈니스 로직. (예: 주문 생성 규칙, 가격 계산)
  • 횡단 관심사(Cross-Cutting Concern): 여러 핵심 관심사에 걸쳐 반복적으로 나타나는 부수적인 로직. (예: 로깅, 보안, 트랜잭션, 성능 측정)

위 예제에서 로깅, 권한 확인, 트랜잭션 관리가 바로 횡단 관심사입니다.

관심사 분리(Separation of Concerns) 원칙에 따라, 횡단 관심사는 핵심 비즈니스 로직과 물리적으로 분리되어야 합니다.

  • OrderService는 오직 "주문 생성"에만 집중.
  • 로깅, 보안, 트랜잭션은 별도의 모듈이나 메커니즘으로 처리.

하지만 자바의 전통적인 OOP에서는 이 분리가 쉽지 않았습니다. 메서드 호출은 직렬적이며, 특정 메서드 실행 전후에 다른 코드를 끼워 넣는 것이 언어 수준에서 지원되지 않았기 때문입니다.

3. AOP가 무엇인지: ncafe 예제로 풀어보기

AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)는 횡단 관심사를 핵심 로직에서 분리하여 독립적인 모듈(Aspect)로 관리하는 기법입니다.

스프링은 AOP를 통해 다음과 같은 개념을 제공합니다.

용어 설명 ncafe 예시
Join Point (조인 포인트) 관심사가 적용될 수 있는 지점. 보통 메서드 실행 시점. OrderService.createOrder() 메서드가 실행되는 순간
Pointcut (포인트컷) 어떤 Join Point에 관심사를 적용할지 정의하는 표현식. "OrderService의 모든 public 메서드" 또는 "createOrder 메서드"
Advice (어드바이스) 실제로 실행될 횡단 관심사 로직. 로깅 코드, 권한 확인 코드, 트랜잭션 시작/커밋 코드
Aspect (어스펙트) Pointcut과 Advice를 결합한 모듈. LoggingAspect, SecurityAspect, TransactionAspect

AOP 동작 흐름 (개념)

  1. 스프링 컨테이너는 OrderService 빈을 생성할 때, AOP가 적용되는지 확인합니다.
  2. OrderService의 실제 객체 대신, 프록시(Proxy) 객체를 생성합니다.
  3. 클라이언트가 orderService.createOrder()를 호출하면, 실제로는 프록시 객체의 메서드가 호출됩니다.
  4. 프록시는 Before Advice (예: 로깅 시작)를 실행합니다.
  5. 실제 OrderServicecreateOrder 메서드(핵심 로직)를 호출합니다.
  6. 핵심 로직이 끝나면 After Advice (예: 로깅 종료, 트랜잭션 커밋)를 실행합니다.

개발자는 OrderService 코드에 로깅이나 트랜잭션 코드를 넣지 않아도, AOP 설정(또는 애너테이션)만으로 이 모든 처리가 자동으로 이루어지도록 할 수 있습니다.

4. 트랜잭션이 왜 필요한가: 원자성 문제

이제 ncafe의 더 복잡한 시나리오를 생각해 봅시다.

주문 처리 과정:

  1. inventoryRepository.decrease(productId, 1) : 재고 1개 차감
  2. orderRepository.save(order) : 주문 내역 DB 저장

만약 이 두 작업이 동시에 실패하거나, 한쪽만 실패하면 어떻게 될까요?

  • 케이스 1: 재고 차감 성공, 주문 저장 실패
    • 재고는 1개 줄었지만, 주문 기록은 없습니다.
    • 고객은 "주문했는데 왜 없지?"라고 항의합니다.
    • 재고는 줄었지만 매출은 발생하지 않아 손해를 봅니다.
  • 케이스 2: 재고 차감 실패, 주문 저장 성공
    • 주문 기록은 있는데, 재고는 줄지 않았습니다.
    • 재고가 없는 상품을 주문한 상태가 됩니다.

이 문제를 해결하기 위해 트랜잭션(Transaction)이 필요합니다.

트랜잭션의 ACID 속성 중 가장 중요한 'A' (Atomicity, 원자성):

트랜잭션에 포함된 모든 작업은 모두 성공하거나, 모두 실패해야 합니다. 중간에 실패하면, 실행된 모든 작업이 취소(Rollback)되어 이전 상태로 돌아갑니다.

즉, createOrder 메서드 안의 재고 차감과 주문 저장은 하나의 트랜잭션으로 묶여야 합니다.

  • 둘 다 성공 → Commit (데이터베이스에 영구 저장)
  • 하나라도 실패 → Rollback (재고 차감도 취소, 주문 저장도 취소)

5. AOP가 트랜잭션을 가능하게 하는 원리

그렇다면 스프링은 어떻게 @Transactional 애너테이션 하나로 이 원자성을 보장할까요?

핵심: @Transactional은 결국 AOP 기반의 프록시입니다.

  1. 개발자는 OrderServicecreateOrder 메서드에 @Transactional을 붙입니다.
  2. 스프링 컨테이너는 이 메서드가 트랜잭션 처리가 필요함을 감지합니다.
  3. AOP를 이용해 OrderService의 프록시 객체를 생성합니다.
  4. 프록시는 createOrder 호출 시:
    • Before: Connection에서 트랜잭션을 시작 (BEGIN)
    • Execute: 실제 createOrder 메서드 실행 (재고 차감, 주문 저장)
    • After:
      • 예외가 발생하지 않았다면 → COMMIT
      • 예외가 발생했다면 → ROLLBACK
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryRepository inventoryRepository;

    public OrderService(OrderRepository orderRepository, 
                        InventoryRepository inventoryRepository) {
        this.orderRepository = orderRepository;
        this.inventoryRepository = inventoryRepository;
    }

    @Transactional // <- 이 애너테이션이 AOP를 트리거
    public void createOrder(Order order) {
        inventoryRepository.decrease(order.getProductId(), 1);
        orderRepository.save(order);
        // 만약 여기서 예외가 발생하면, 위의 decrease도 롤백됨
    }
}

개발자는 트랜잭션 시작/커밋/롤백 코드를 직접 작성할 필요가 없습니다. 스프링의 AOP 인프라가 이를 자동으로 처리합니다. 이것이 AOP가 트랜잭션을 가능하게 하는 원리입니다.

6. 13부 예고: 트랜잭션의 심층 탐구

이번 노트에서는 AOP와 트랜잭션의 개념적 기반왜 필요한지를 이해했습니다.

  • AOP: 횡단 관심사를 분리하는 기법
  • 트랜잭션: 데이터의 일관성을 보장하는 원자성
  • @Transactional: AOP 기반의 트랜잭션 관리

하지만 실무에서는 트랜잭션이 항상 단순하지 않습니다.

  • 여러 트랜잭션이 겹칠 때 어떻게 처리할 것인가? (전파 타입, Propagation)
  • 다른 트랜잭션과 데이터 접근 시 충돌을 어떻게 방지할 것인가? (격리 수준, Isolation Level)
  • @Transactional이 적용되지 않는 경우 (자기 호출, final 메서드 등)는?

이러한 트랜잭션의 실무적 깊이와 주의사항13부. 트랜잭션 관리에서 13-1~13-3에 걸쳐 자세히 다루겠습니다.

이 주소는 글의 저작자가 공개한 링크입니다. 뉴렉처 스터디의 다른 자료는 로그인 후 볼 수 있어요.