이전 글에서 예고했듯, 앞으로의 글에서는
Transaction과 Transaction Synchronization의 추상화 진행 과정을 살펴볼 것이다.
그렇지만 그 전에 Connection과 Transaction이 어떻게 밀접한 연관을 가지고 있는지를 확인해야 한다.
이에 대해서 자세히 풀어보도록 하겠다.
1. Connection과 Transaction Synchronization의 관계
결론부터 말하자면, 원하는 대로 Transaction Synchronization이 이뤄지도록 하려면
Connection의 단위와 Synchronization의 단위가 같아져야 한다.
그래서 Connection의 생명주기와 Transaction의 생명주기가 같아지도록 하는 것이다.
Connection은 트랜잭션에서 사용하는 리소스(DB 커넥션, 쿼리 실행 등)를 직접 관리하는 주체이기 때문에,
Connection이 닫히거나 반환되면 트랜잭션 상태도 영향을 받는다.
따라서 Connection과 Transaction이 서로 일관된 범위에서 동작하도록 생명주기를 조율해야 한다.
물론, 완벽하게 같아져야 한다는 얘기는 아니며, 개발자의 재량에 따라 다르게 설정할 수도 있다.
그렇지만 최소한 Connection의 생명주기 안에 Transaction의 생명주기가 포함되도록 해줘야 한다.
이게 무슨 말인가? 싶을 것이다.
글의 예시 => 그림 설명 => 코드로 진행을 이어 보도록 하겠다.
2. 글을 통한 설명
예를 들어 어떤 커머스에서 한 유저가 물건을 구매한다고 가정하자.
그러면 구매에 대해서 2가지 DB의 업데이트가 필요하다.
- payment table에 payment 이력 추가
- product table에 재고 update
위의 예시에서 각각의 메서드를 repository단위로 메서드명만 짓자면
- paymentRepository.save
- productRepository.update가 되고,
- 이 두 개의 메서드는 PurchaseService.purchase 메서드에서 호출된다고 하자
이렇게 되었을 때, 어떤 하나의 DB조작이 실패해 rollback이 된다면 전체 서비스 또한 취소되어야 한다.
가령 payment에는 추가를 했으나 product에 조회해봤더니 재고가 모자라 rollback이 된다면,
payment또한 취소가 되어야 하는 것이다.
즉, 전체 PurchaseService.purchase에서
paymentRepository.save가 commit될 준비가 되었다고 하더라도
productRepository.update가 실패해 rollback을 요청한다면 paymentRepository.save 또한 commit되어서는 안된다.
이처럼 서로 다른 Transaction에 대해 commit과 rollback여부의 동기화를 하는 것.
원자성을 유지하도록 서로 다른 transaction을 하나의 transaction으로 적용하는 것이 Transaction Synchronization이다.
그런데, 이러한 transaction이 connection과 어떤 관계가 있는 것인가?
transaction의 commit, rollback여부는 connection을 넘지 못한다.
다시 말해서, 어느 하나의 transaction이 속해 있는 connection이 다른 connection과 연결되어 있지 않다면,
전자의 transaction의 commit, rollback 여부가 다른 transaction으로 반영되지가 않는 것이다.
그렇기 때문에 transaction을 동기화해야 할 경우, 단일한 connection을 사용해줘야 하는 것이 필수 조건이다.
3. 그림을 통한 추가 설명
또한 서로 다른 transaction을 동기화 한다고 가정하자, 간단한 수학얘기이지만,
각각의 transaction의 생명주기는 자신이 포함된 connection의 생명주기에 포함되는 것이기에,
connection의 생명주기를 합친 것은 transaction의 생명주기를 합친 것에 포함되어야 한다.
따라서, 그림을 그리자면 반드시 아래와 같은 모양이 되어야 한다.
- 서로의 connection이 다르면 rollback이 된다고 하더라도 전파되지 않기에,
반드시 동일한 connection을 가져야 하는 것이 첫 번째 원칙이 될 것이다. - 그리고, connection이 없으면 transaction도 없다는 대원칙
다시 말해 connection의 생명주기가 transaction의 생명주기를 포함해야 한다는 것이 두 번째 원칙. - 여러 transaction이 원자성을 지킨 하나의 transaction으로 진행되어야 한다는 세 번째 원칙
세 가지 원칙을 모두 지키는 상황이 바로 Transaction Synchronization이고,
정확히 오른쪽 아래 그림의 상황이 된다. (위 3개의 원칙은 어딘가에 명시되어있는 원칙은 아니다.
설명의 편의를 위해 필자가 서술한 것이니 오해말기 바란다.)
4. 코드를 통한 Transaction Synchronization의 저수준 구현.
지금은 가장 저수준, JDBC API만으로 Transaction Synchronization을 구현하는 것이기 때문에 코드 자체가 복잡하다.
하지만 그만큼 동기화의 원리를 잘 알 수 있기도 하다.
다음 글에서 차차 이를 고수준화, 추상화 할 것이기 때문에 코드의 양은 점점 줄어들 것이다.
위에 설명한 3개의 원칙을 지키기 위해 코드에서의 주의 사항은 아래와 같다.
- connection을 하나만 유지할 것.
- auto-commit을 false로 설정할 것.
- 단 한 번의 commit만을 진행할 것.
다만, 1번의 구현이 골치가 아프다. connection을 단 하나로 유지하려면 어떻게 진행해야 할까?
이에 대한 답변은 react의 props drilling처럼 파라미터로 connection을 넘겨주면 된다.
즉, connection의 생성 자체는 PurchaseService.purchase에서 진행하고
paymentRepository.save와 productRepository.update는 파라미터로 connection을 받도록 한다.
그리고 기존의 각각의 repository에 있던 connection 관련 생성 로직과 close로직은 전부 지워주도록 하고,
마지막으로 PurchaseService.purchase에 단일 connection이 close되도록 구현해야 한다.
이를 구현한 코드는 아래와 같다.
public class UserService {
IPaymentRepository<Payment, Long> paymentRepository;
IProductRepository<Product, Long> productRepository;
DataSource dataSource;
public void purchase(RequestDto request) {
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
paymentRepository.createPayment(connection, ...);
productRepository.update(connection, ...);
connection.commit();
} catch (SQLException e) {
try {
connection.rollback();
} catch (SQLException ignored) {
} throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 문제가 있습니다.");
} finally {
try {
connection.close();
} catch (SQLException e) {
}
}
}
}
앞으로의 과정은 Transaction과 관련된 위의 코드들을 어떻게 추상화하고, 숨길 것인가를 다룰 것이다.
'연재작 > Database' 카테고리의 다른 글
Persistence Context의 Entity 상태 관리, Life-Cycle (1) | 2024.11.28 |
---|---|
Transaction Deep Dive (5) (0) | 2024.11.28 |
Transaction Deep Dive (4) (0) | 2024.11.26 |
Transaction Deep Dive (2) (0) | 2024.11.22 |
Transaction Deep Dive (1) (0) | 2024.11.20 |