공유된 기술노트 · 스프링부트 · 스프링부트 API
1-3. AOP와 트랜잭션 — 스프링이 마법을 부리는 원리
1. ncafe 주문 시나리오: 비즈니스 로직에 섞인 '잡일'
ncafe 백엔드에서 OrderService의 createOrder 메서드를 다시 떠올려 봅시다. 1-2에서 우리는 OrderRepository를 주입받아 주문을 저장하는 코드를 작성했습니다. 하지만 실제 운영 환경에서는 단순히 주문을 저장하는 것만으로는 부족합니다.
주문이 생성되는 시점에 다음과 같은 일들이 동시에 일어나야 합니다.
- 로그 기록: "사용자 1001이 라떼를 주문했습니다."
- 권한 확인: 이 사용자가 실제로 주문 권한이 있는지 확인.
- 재고 차감: 커피 원두 재고를 1개 줄임.
- 데이터 저장: 주문 내역을 데이터베이스에 기록.
만약 이 모든 로직을 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는 "주문을 만드는 것"이 본업인데, 로깅, 권한, 재고 관리라는 부수적인 일까지 하고 있습니다.- 만약
PaymentService나RefundService에서도 로깅과 권한 확인이 필요해지면, 그 코드에도 똑같은 로깅과 권한 코드를 복사해서 붙여넣어야 합니다. - 복사-붙여넣기(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 동작 흐름 (개념)
- 스프링 컨테이너는
OrderService빈을 생성할 때, AOP가 적용되는지 확인합니다. OrderService의 실제 객체 대신, 프록시(Proxy) 객체를 생성합니다.- 클라이언트가
orderService.createOrder()를 호출하면, 실제로는 프록시 객체의 메서드가 호출됩니다. - 프록시는 Before Advice (예: 로깅 시작)를 실행합니다.
- 실제
OrderService의createOrder메서드(핵심 로직)를 호출합니다. - 핵심 로직이 끝나면 After Advice (예: 로깅 종료, 트랜잭션 커밋)를 실행합니다.
개발자는 OrderService 코드에 로깅이나 트랜잭션 코드를 넣지 않아도, AOP 설정(또는 애너테이션)만으로 이 모든 처리가 자동으로 이루어지도록 할 수 있습니다.
4. 트랜잭션이 왜 필요한가: 원자성 문제
이제 ncafe의 더 복잡한 시나리오를 생각해 봅시다.
주문 처리 과정:
inventoryRepository.decrease(productId, 1): 재고 1개 차감orderRepository.save(order): 주문 내역 DB 저장
만약 이 두 작업이 동시에 실패하거나, 한쪽만 실패하면 어떻게 될까요?
- 케이스 1: 재고 차감 성공, 주문 저장 실패
- 재고는 1개 줄었지만, 주문 기록은 없습니다.
- 고객은 "주문했는데 왜 없지?"라고 항의합니다.
- 재고는 줄었지만 매출은 발생하지 않아 손해를 봅니다.
- 케이스 2: 재고 차감 실패, 주문 저장 성공
- 주문 기록은 있는데, 재고는 줄지 않았습니다.
- 재고가 없는 상품을 주문한 상태가 됩니다.
이 문제를 해결하기 위해 트랜잭션(Transaction)이 필요합니다.
트랜잭션의 ACID 속성 중 가장 중요한 'A' (Atomicity, 원자성):
트랜잭션에 포함된 모든 작업은 모두 성공하거나, 모두 실패해야 합니다. 중간에 실패하면, 실행된 모든 작업이 취소(Rollback)되어 이전 상태로 돌아갑니다.
즉, createOrder 메서드 안의 재고 차감과 주문 저장은 하나의 트랜잭션으로 묶여야 합니다.
- 둘 다 성공 → Commit (데이터베이스에 영구 저장)
- 하나라도 실패 → Rollback (재고 차감도 취소, 주문 저장도 취소)
5. AOP가 트랜잭션을 가능하게 하는 원리
그렇다면 스프링은 어떻게 @Transactional 애너테이션 하나로 이 원자성을 보장할까요?
핵심: @Transactional은 결국 AOP 기반의 프록시입니다.
- 개발자는
OrderService의createOrder메서드에@Transactional을 붙입니다. - 스프링 컨테이너는 이 메서드가 트랜잭션 처리가 필요함을 감지합니다.
- AOP를 이용해
OrderService의 프록시 객체를 생성합니다. - 프록시는
createOrder호출 시:- Before:
Connection에서 트랜잭션을 시작 (BEGIN) - Execute: 실제
createOrder메서드 실행 (재고 차감, 주문 저장) - After:
- 예외가 발생하지 않았다면 →
COMMIT - 예외가 발생했다면 →
ROLLBACK
- 예외가 발생하지 않았다면 →
- Before:
@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에 걸쳐 자세히 다루겠습니다.
이 주소는 글의 저작자가 공개한 링크입니다. 뉴렉처 스터디의 다른 자료는 로그인 후 볼 수 있어요.