JPetStore-6의 경우 주문 처리를 할 때 다른 서비스의 데이터베이스를 변경해야 하기 때문에 트랜잭션의 원자성을 보장하기 위해서 분산 트랜잭션이 필요하다. 이전 개발 게시글에서 토스의 분산 트랜잭션 영상을 꼼꼼히 보고 해당 내용대로 구현하려고 하고 있다. 해당 게시글에서는 현재 어디까지 개발을 완료했는지를 다루고 있다.
JPetStore-6에서 주문 처리 분산 트랜잭션 구현
주문 처리 흐름
- 기본적인 주문 처리 흐름을 그림으로 나타내면 아래와 같다.
- 모놀리식일 때의 로직을 그대로 보존하고 있으며, 주문 요청이 들어오면 해당 주문량만큼 재고를 감소시킨 후, 주문 정보를 삽입하는 형태이다.
- 이 주문 처리 흐름을 완벽하게 변경하기 위해서 몇 가지 변경 사항을 추가했고, 또 더 추가할 계획이다.
- 이 내용은 앞서도 얘기했듯이 토스의 분산 트랜잭션 컨퍼런스 내용과 내가 최근에 읽은 마이크로서비스 아키텍처 구축 가이드 책을 기반으로 한다.
HTTP 통신은 한 번만 이루어질 것
- 애초에 서비스 간 통신이 HTTP였기 때문에 주문 처리 분산 트랜잭션에서도 Order service와 Product service는 HTTP 통신을 하게 된다.
- 토스에서는 환전과 관련된 분산 트랜잭션이 수행될 때 응답을 확실히 받고 그 다음 로직을 수행해야 하기 때문에 HTTP 통신을 쓴다고 언급했다.
- 우리의 주문 처리 로직도 비슷한데, 수량 변경이 제대로 이루어진 후에 주문을 삽입할 수 있기 때문에 응답을 확실하게 받아야 한다. 따라서 HTTP 통신을 하는 것이 적절하다.
- 그런데 HTTP 통신의 경우 비용이 상당하다.
- 연결을 맺고 끊는 과정이 존재하기 때문이다.
- 그렇기 때문에 최대한 HTTP 통신 수를 줄이는 것이 좋다.
- 하지만 기존 모놀리식의 로직을 그대로 옮기다 보니 불필요하게 HTTP 통신을 많이 하는 식으로 구현되었다.
- 위의 그림처럼 상품 종류 별로 따로따로 HTTP 요청을 날려서 재고를 변경하는 식이다.
- 따라서, 이를 개선하기 위해서 수량 변경이 필요한 상품의 ID와 해당 상품의 수량을 몇 개를 변경해야 하는지 한꺼번에 보냈다.
- 이러면 한 번의 HTTP 요청으로 한꺼번에 수량 변경이 가능하다.
수량 변경 시 락을 걸기
- 일단 기존 모놀리식에서는 트랜잭션이라는 개념도 없었기 때문에 AOP를 이용해서 트랜잭션을 관리했다.
- @Transactional 어노테이션을 추가한 것이다.
- 하지만 하나의 자원에 대해 동시 접근을 막는 것이 아니기 때문에 수량 변경이 일어날 때 락을 걸도록 구현했다.
@Transactional
public boolean updateItemQuantity(List<String> itemId, List<Integer> increment){
try{
// lock 획득
itemMapper.lockItemsForUpdate(itemId);
// 각각 업데이트
for(int i = 0; i < itemId.size(); i++) {
itemMapper.updateInventoryQuantity(itemId.get(i), increment.get(i));
}
}catch (Exception e){
return false;
}
return true;
}
- 위의 코드에서 보이듯이, 트랜잭션 최상단에서 변경될 아이템들에 대해서 전부 락을 걸고 있다.
- 이 락의 경우 베타락이기 때문에 내가 접근 중일 때 그 누구도 해당 자원에 대해 수정할 수 없다.
- 그렇다면 해당 쿼리는 어떻게 작성되어 있을까?
<select id="lockItemsForUpdate" resultType="string">
SELECT ITEMID FROM INVENTORY WHERE ITEMID IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
FOR UPDATE
</select>
- MyBatis를 쓰고 있기 때문에 XML에 쿼리가 작성되어 있다.
- Select for update 문을 사용해서 내가 변경해야 할 모든 상품들에 락을 걸었다.
- 락의 해제는 트랜잭션의 Commit() 이후에 자연스럽게 종료된다고 한다.
- 이를 통해, 여러 주문이 밀려올 때 동시성 문제를 해결했다.
- 재고의 경우 음수가 나오면 절대 안되기 때문에 엄격한 락을 걸었다고 할 수 있다.
보상 트랜잭션
* 정상적인 실패의 경우
- 먼저, 아래와 같은 경우에는 보상 트랜잭션이 필요가 없다.
- 이때, 실패 응답은 5xx, Timeout이 아니라 진짜 재고가 없어서 실패를 하는 등 정상적인 실패를 의미한다.
- 비정상적인 실패는 더 아래에서 다루도록 한다.
* 보상 트랜잭션이 일어나는 경우
- 보상 트랜잭션은 아래와 같은 상황에서 일어날 수 있다.
- 예를 들어, 재고는 성공적으로 변경했는데 주문을 삽입하는 과정에서 실패를 겪었다면 재고를 원상복구 시켜야 한다.
- 이때, 재고를 원상복구하는 요청은 HTTP 통신을 하지 않는데, 그 이유는 토스에서도 얘기한 것과 같이 사용자가 그 결과를 기다릴 필요가 없기 때문이다.
- 이를 위해서 이전 내 블로그 게시글에서 Kafka를 도입해 Order service에서 보상 트랜잭션 메시지를 발행하고 Product service가 해당 메시지를 소비하도록 구현했다.
- 하지만, 아직 메시지 발행을 실패하거나 메시지 소비를 실패하는 경우에 대한 처리를 하지 않은 상태이다.
* 비정상 실패를 겪는 경우
- 앞서 봤던 재고 변경 실패 응답의 경우 비정상적인 실패 응답이 존재한다.
- 예를 들면, 5xx 응답이나 Timeout과 같이 서버에 문제가 생긴 경우이다.
- 이런 경우 실제로 내 요청이 수행되었는데 응답이 지연처리 된 것인지 아예 내 요청이 먹히지 않은 것인지 알 수 없기 때문에 재요청을 보내게 된다.
- 토스에서도 마찬가지이다.
- 하지만 내 요청이 처리되었는지 아닌지를 어떻게 알 수 있을까?
- 한참을 고민하다가 트랜잭션을 로깅하는 데이터베이스를 만들었다.
- Order service에서는 재고 변경을 요청할 때, 일시적으로 사용될 UUID를 만들어서 넘겨준다.
- Product service에서는 요청이 정상적으로 처리되어 트랜잭션이 마무리되면 넘겨받은 UUID 값으로 데이터베이스에 기록을 남긴다.
- 만약 Order service에서 지연 응답을 받아, 요청이 적용되었는지를 확인하려면 UUID 값을 다시 보낸다.
- Product service에서는 트랜잭션 로그가 담긴 데이터베이스를 조회해 로그가 있는지 없는지 여부를 체크하여 요청의 수행 여부를 판단할 수 있다.
- 아래의 그림과 같이 나타낼 수 있다.
- 위의 그림에서 UUID가 존재해 재고가 변경된 것이 확인이 되면, 주문 삽입 로직을 이어나간다.
- 재고 변경이 실패했다면, 주문은 실패로 종료된다.
- 하지만, 아직도 큰 산이 남았다.
- 애초에 5xx, Timeout을 반환한다는 것은 서버가 불안정하다는 것을 의미하기 때문에 재요청을 하더라도 정상적인 응답을 받기는 어려울 수 있다.
- 따라서, 재요청을 실패할 경우 지연 재요청을 보내야 하며, 한 번만으로는 부족하기 때문에 여러 번 요청을 해야 하는 로직이 필요하다.
- 요약하자면, 보상 트랜잭션에서 메시지 발행이 실패하는 경우, 해당 메시지 수신 처리가 실패하는 경우, 그리고 비정상실패 시 재고 변경 여부를 지속적으로 재확인하는 로직 구현이 더 필요하다.
- 또, Order service에서 하나의 Order에 대해 트랜잭션을 기록하면서 해당 주문이 어떤 상태인지 끊임없이 체크해야 하며 올바르게 처리되지 않는 주문에 대해서 모니터링할 수 있는 기능들을 구현하면 좋을 것 같다.
추천글
2025.02.15 - [개발] - [Legacy to Microservices] JPetStore-6 웹 앱의 마이크로서비스 전환기
[Legacy to Microservices] JPetStore-6 웹 앱의 마이크로서비스 전환기
2025년 1월 1일부터 레거시 모놀리식 웹 앱인 JPetStore-6 웹 앱을 마이크로서비스로 전환하는 프로젝트를 진행하고 있다. 이 웹 앱의 경우 오픈 소스 웹 앱으로 비교적 접근이 쉽고 지금 내가 해왔던
se-dobby.tistory.com
'Legacy to Microservices' 카테고리의 다른 글
[Legacy to Microservices] 분산 트랜잭션 설계 최종 (2) | 2025.03.05 |
---|---|
[Legacy to Microservices] 설계의 변경 (0) | 2025.02.25 |
[Legacy to Microservices] 분산 트랜잭션에서 상대 서버가 정상 응답을 제공하지 않는 경우, 재요청 지연시키기 (0) | 2025.02.16 |
[Legacy to Microservices] JPetStore-6 개선: CSRF 토큰을 JSP에 추가하기 (0) | 2025.02.16 |
[Legacy to Microservices] JPetStore-6 웹 앱의 마이크로서비스 전환기 (0) | 2025.02.15 |