MSA 로의 작은 한 걸음 : 도메인 간 배포 단위 분리와 분산 트랜잭션 - leonroars/slam GitHub Wiki

MSA 로의 작은 한 걸음 : 도메인 간 배포 단위 분리와 분산 트랜잭션

목차

I. 서론: MSA 도입 배경과 도메인별 배포 단위의 필요성

II. Database-per-Service: 데이터 격리와 서비스 독립성

III. 분산 트랜잭션과 보상 트랜잭션

IV. Saga 패턴: 분산 환경에서의 트랜잭션 관리 대안

V. 결론

I. MSA 도입 배경과 도메인별 배포 단위의 필요성

처음 웹이 소개된 이후부터 지금까지도 웹 그리고 웹 기반의 시장은 빠르게 성장하고 변화해갑니다.

어제는 팔렸던 것이 오늘도 팔릴 것이라고 장담을 하지 못하는 경영 환경에서, 많은 기업들이 새로운 서비스를 출시했다가 빠르게 변하는 이러한 환경에 발 맞추어 가지 못하여 사장되어 왔습니다.

그렇다고 이렇게 빠르고 다채로운 변화를 쉽사리 예측할 수 있냐한다면 그것 또한 아닙니다.

우리는 예측을 할 수도 없고, 변화와 무관하게 시장 가치를 갖는 금 같은 서비스를 개발하여 출시할 수도 없습니다.

그렇다면 서비스를 개발하고 출시하는 우리는 어떻게 해야할까요? 그냥 손을 놓아야할까요?


애자일 선언을 여기서 잠시 살펴보겠습니다.

애자일 소프트웨어 개발 선언


우리는 소프트웨어를 개발하고,  다른 사람의 개발을
도와주면서 소프트웨어 개발의  나은 방법들을 찾아가고
있다.  작업을 통해 우리는 다음을 가치 있게 여기게 되었다:

공정과 도구보다 개인과 상호작용을
포괄적인 문서보다 작동하는 소프트웨어를
계약 협상보다 고객과의 협력을
계획을 따르기보다 변화에 대응하기를

가치 있게 여긴다.  말은, 왼쪽에 있는 것들도 가치가 있지만,
우리는 오른쪽에 있는 것들에  높은 가치를 둔다는 것이다.

특히 이 부분이 지금 우리가 다루고자 하는 이 '변화'와 연관지어 생각해볼 수 있는 대목이라고 생각합니다.

계획을 따르기보다 변화에 대응하기를


이렇게 빠르게 변화하는 시장 환경 속에서 완벽한 계획이라는 것은 사실 존재할 수가 없습니다.

우리가 할 수 있는 건 그저 유연하게 대처할 수 있고 - 즉, 시장의 요구에 따라 서비스 자체의 방향 전환 혹은 새로운 것 도입 -, 확장하기에 용이한 형태를 갖춘 무언가를 만드는 것입니다.

실제로 우리가 개발하여 출시하는 서비스는 논리적으로 구분된 경계 를 갖는 여러 기능 요소들의 합으로 구성되어 있습니다.

이러한 부분에 주목하여 개발하는 것이 바로 도메인 주도 개발 의 시작점이라고 저는 이해했습니다.

예를 들어 지금 현행 프로젝트를 가져와 생각해본다면 예약, 결제, 사용자, 공연과 같은 단위들이 구분된 경계를 가짐을 인식할 수 있습니다.

이런 구분된 기능 단위들 중 어떤 것들은 시장의 소비 트렌드 혹은 수요 변화에 의해 없어질 수도 있고 새로운 것들이 추가될 수도 있습니다.

우리의 공연 예매 서비스가 안정적으로 사용자에게 효용감을 전달하는데에 성공하여 이제 병원 예약, 항공권 예약과 같은 것들을 하게 될 지도 모르는 것입니다.

그런 기능 요소들이 언젠가는 공연 예약 서비스보다 더 커져버릴 수도 있겠습니다.


이러한 서비스 기능 요소들의 변화에 따라 이를 개발하는 사내 조직 구조 또한 영향을 받습니다.

또 각각의 기능요소들이 저마다의 방향으로 발전해나가며, 기존에 함께 공유하며 활용하던 인프라가 더 이상 적합하지 않을 수도 있습니다.

공연 예약 서비스는 MySQL 과 같은 관계형 데이터베이스 모델에 근거한 데이터베이스 시스템을 활용하기에 적절하지만 새롭게 추가되어 변화해나가는 다른 서비스는 NoSQL 기반의 데이터베이스가 더 적합하게 될 지도 모르는 것입니다.


이 시점에서 지금 현행 프로젝트의 설계를 한 번 살펴보겠습니다.

Screenshot 2025-02-14 at 6 30 54 AM

비록 도메인 주도 개발을 시도하여 "공연 예약 서비스" 라는 본래의 도메인을 그 관심과 책임에 따라 관리 가능한 단위(서브 도메인)으로 나누고 이들 간 의존을 0에 수렴하도록 설계하는데엔 성공했습니다.

하지만 여전히 이들은 같은 인프라를 함께 사용하고, 같은 서버 애플리케이션의 핸들러 메소드, Application 계층의 Facade 아래에서 협력하며 비즈니스 로직을 수행하고 있습니다.

이러한 설계를 모놀리식(Monlithic) 한 설계라고 말합니다.

이러한 설계는 전체 서비스에 대한 테스트가 용이하므로 견고한 서비스를 설계하는데에 유리할 수 있습니다.

또, 비교적 작은 규모의 조직에서는 방금 막 캐치한 시장 트렌드에 근거하여 아이디어를 빠르게 구현하여 다음 변화 이전에 사용자의 반응을 살피는 것이 중요하고, 모놀리식은 이러한 빠른 제품 개발 및 출시가 용이하다는 장점이 있습니다.


그럼에도 불구하고 우리는 기업과 서비스가 무럭무럭 성장해나가길 바랍니다.

모두가 이를 위해서 열심히 개발하고 토론합니다.

이러한 노력이 빛을 발해 성장을 하는 순간이 다가오고 이에 발 맞춰가기 위한 과정에서 모놀리식의 한계를 마주하는 상황이 다가오기 마련입니다.

빠르게 유입되는 사용자 수와 해당 사용자들의 니즈에 수렴하기 위해선 중단 없이 꾸준히 신뢰할 수 있는 서비스를 제공할 수 있는 역량이 요구되기 때문입니다.

앞서 기술한 상황이 요즈음의 새로운, 그리고 성장하는 기술 기업들이 마주하고 있는 시장 상황인 것입니다.

따라서 다른 설계를 찾을 필요가 생깁니다.


1. MSA 개념 소개

MSA하나의 거대한 애플리케이션을 여러 개의 작고 독립적인 서비스들로 나누어 관리하는 아키텍처 입니다.

각각의 서비스는 독립적으로 개발, 배포, 운영될 수 있기 때문에 전체 시스템의 유연성이 매우 높아집니다.

가령, 한 번에 전체 시스템을 업데이트하는 대신, 개별 서비스만 수정하거나 추가할 수 있어서 장애 발생 시 영향을 최소화할 수 있습니다.

또한, 각 서비스는 서로 다른 기술 스택을 사용할 수도 있어, 팀마다 자신들이 가장 잘 아는 기술 혹은 해당 도메인에 가장 적합한 기술을 정하여 개발할 수 있다는 점도 큰 장점입니다.

이러한 MSA 의 구조적 특징은 MSA 아키텍처라고 불리기 위해 준수해야 하는 원칙으로 정리해볼 수 있겠습니다.

주요 원칙

  • 독립적 배포: 각 서비스는 별도로 배포할 수 있으므로, 하나의 서비스에 문제가 생겨도 다른 서비스에 영향을 주지 않습니다.
  • 기술 다양성: 각 서비스가 서로 다른 프로그래밍 언어나 데이터베이스, 프레임워크를 사용할 수 있어, 팀의 전문성과 상황에 맞게 선택할 수 있습니다.
  • 장애 격리: 문제가 발생했을 때, 문제가 되는 서비스만 격리해서 처리할 수 있기 때문에 전체 시스템의 안정성을 높일 수 있습니다.

2. 도메인별 배포 단위를 위한 준비

그렇다면 이렇게 바로 서비스 처음부터 MSA 아키텍처로 개발하기엔 배보다 배꼽이 더 큰 것이 사실입니다.

처음 서비스를 개발하는 상황이라면 인력, 자금과 같은 자원도 절대적으로 부족하고 이때문에 구성원 한 명 한 명에게 지워지는 부담 자체가 커지기 때문입니다.

그렇기 때문에 보통 현행 프로젝트처럼 도메인 단위로 기능 의존을 최소화하여 언제든 분리할 수 있는 형태로 설계하고 개발하는 것이 아마 가장 유효한 전략일 것입니다.


비즈니스 도메인은 곧 회사의 비즈니스 로직이나 핵심 기능을 의미합니다.

이를 기반으로 서비스를 분리하면 각 도메인(또는 비즈니스 기능)이 서로 독립적으로 관리될 수 있습니다.



II. 분산 트랜잭션과 보상 트랜잭션

우리 애플리케이션을 이제 도메인별 배포단위까지 분리하는 단계에 이르렀다고 가정해보겠습니다.

앞서 기술하였듯, 배포 단위가 분리된다는 것은 서로 비즈니스 로직이라는 큰 목적을 이루기 위한 정도의 관계만 가질 뿐, 서로 어떤 형태로도 의존하고 있지 않음을 의미합니다.

이런 상황에서 예약 확정 기능에 대한 요청이 서비스로 들어온 상황을 생각해봅시다.

  1. 사용자로부터 "예약 확정" 요청이 들어온다.

  2. 우선 해당 사용자의 예약이 존재하는지 확인한다. - (DB #1 조회)

  3. 이후 존재할 경우 사용자의 포인트 잔액을 확인한다. - (이후 누군가에 의해 PointService.checkBalance() 호출)

  4. PointService.checkBalance() 호출 시 DB로부터 조회가 발생 - (DB #2 조회)

  5. 잔액이 존재하는 경우 예약 확정 절차에 돌입한다. - (DB #1 UPDATE)

  6. 사용자에게 성공을 알리는 Response 를 반환한다.

이러한 원자적 연산의 실행 주체가 Facade 일 경우 각 단계 중 실패가 발생했을 때 예외를 핸들링하는 방식으로 명시적 Rollback 과 같은 과정을 통해 여전히 비즈니스 로직의 원자성을 유지할 수 있습니다.

하지만 만약 우리는 이미 석범 코치님의 조언에 따라 Facade 를 없애고 이제 구분된 Microservice 로 거듭나고있는 우리의 분리된 도메인 서비스들이 자신들의 트랜잭션을 관리하도록 하게 변경하는 과정에 있습니다. - 이를 Local Transaction 이라 하겠습니다.

그렇다면 각 도메인 서비스가 의도한 순서대로 호출되고, 자신의 차례를 인지할 수 있도록 서로 메시지를 주고받아야 합니다. 이렇게 상호 간에 메시지를 통해 협업하는 구조를 _Event Driven Architecture_라고 부릅니다.


이제 트랜잭션이 애플리케이션 인스턴스의 경계를 넘어 분산되어 존재하기 시작합니다. 본래 비즈니스 로직이라는 하나의 트랜잭션 스코프 내에서 동작하던 여러 구성요소들이, 배포 단위가 분리되면서 물리적으로는 분리된 환경에서 작동하게 되는 것입니다. 이를 _분산 트랜잭션(Distributed Transaction)_이라 합니다.

그리고 분산된 각 DB의 개별 트랜잭션이 모여 하나의 큰 트랜잭션을 이루는 경우 — 바로 지금과 같은 상황 — 이를 _Long-Lived Transaction_이라고 부르기도 합니다.


1. 분산 트랜잭션 개요

전통적인 분산 트랜잭션 방식에서는 2PC(Two-Phase Commit)나 3PC(Three-Phase Commit)와 같은 프로토콜을 사용합니다.

2PC는 Coordinator가 모든 참여자(각 도메인의 트랜잭션)에 변경 사항을 커밋할 준비가 되었는지 확인한 후, 전부 커밋하도록 지시합니다.

왜 Coordinator 라는 구분된 컴포넌트가 필요할까요?

  • 이와 같이 외부의 독립된 관계자를 두지 않을 경우 Application 간 직접 소통을 통해 문제를 해결해야하는데, 이 경우 한 서비스에서 다른 서비스의 세부 사항을 알아야하는, 즉 발생해선 안될 의존 관계가 생성될 수 있기 때문입니다.
2pc

위처럼 이 Coordinator 는 자신이 원자성을 보장해야 하는 긴 트랜잭션(LLT, Long-live Transaction)을 이루는 각 로컬 트랜잭션들이 성공적으로 잘 진행되는지 관리감독합니다.

바로 각 트랜잭션이 잘 커밋되었는지 직접 하나하나 확인하면서 말입니다.

확실하긴 하지만 이런 방식은 다음과 같은 문제점이 있습니다:

동기화 문제

  • 모든 참여자가 동시에 준비 상태에 있어야 하므로 네트워크 지연이나 일시적인 장애에도 취약합니다.

락(Lock) 문제

  • 각 참여자에서 자원을 락(lock)한 채로 기다리게 되어, 전체 시스템의 성능 저하를 가져올 수 있습니다.
  • 심지어 락을 보유한 채로 애플리케이션이 종료될 경우 해당 자원에 대한 락이 해제가 되지 않을 수 있습니다... 상당히 골치아픕니다.

성능 저하

  • 모든 트랜잭션의 결과를 모아 하나의 결정으로 만드는 과정에서 응답 시간이 길어질 수 있습니다.

단일실패지점으로의 진화

  • 이 Coordinator 또한 별도의 애플리케이션으로 구현되어 제공되어야 하는 요소입니다.
  • 바로 이 요소가 단일실패지점이 될 가능성이 높습니다.
  • 단순히 실패만 하면 다행이지만... 만약 첫 번째 작업이 완료되었음(ex. 가예약 존재 여부 확인)을 확인하고 두 번째 로컬 트랜잭션을 주관하는 서비스(ex. 결제 서비스)에 메세지를 보내고 다운되는 경우, 롤백도 안되고 전체 트랜잭션의 정합성이 그대로 깨져버리는 불상사가 발생합니다.

이러한 한계 때문에, 분산 환경에서는 전통적인 2PC나 3PC 방식을 그대로 적용하기 어려운 상황이 많습니다.


2. 보상 트랜잭션의 개념

보상 트랜잭션 은 전통적인 전역 트랜잭션(즉, 2PC 방식) 대신에 각 도메인 서비스가 자체적으로 트랜잭션을 관리(로컬 트랜잭션)하면서, 실패한 경우에 각 서비스에서 반대의 작업(보상 작업)을 수행하여 원래 상태로 되돌리는 방식을 의미합니다.

즉, 전통적인 분산 트랜잭션 관리 프로토콜이 실패에 대응하는 방식이 긴 트랜잭션(LLT) 을 야기하여 시스템 전반의 안정성과 고가용성을 저해하는 점에 주목하여, 로컬 트랜잭션 단위로 커밋이 되도록하여 이러한 문제를 해결함과 동시에 실패 시엔 명시적으로 각 부분 로컬 트랜잭션들의 롤백이 필요함을 알림으로써 복구하는 방식인 것입니다.

예를 들어, 예약 확정 과정 중 문제가 발생하여 예약을 완료하지 못한 경우(예약 상태 변경 실패), 이미 완료된 다른 도메인의 작업(예: 포인트 차감)을 취소하는 _보상 트랜잭션_을 실행함으로써, 전체 비즈니스 로직의 일관성을 유지합니다.

1_AFELiMKy0crqBaF4ElD2CA

위의 도식은 심플하지만 이 과정을 상당히 명료히 보여줍니다.

왜 필요할까요?
보상 트랜잭션은 분산된 서비스들이 서로 독립적으로 트랜잭션을 관리할 때,

어느 한 쪽에서 실패가 발생하면 전체 시스템을 롤백하는 대신, 각 서비스가 자체적으로 취소 작업을 수행하도록 함으로써 의존성을 낮추고 복구 과정을 간소화할 수 있습니다.


  • 보상 트랜잭션
    • 장점:
      • 각 서비스가 독립적으로 트랜잭션을 관리하므로, 한 도메인의 지연이 다른 도메인에 미치는 영향을 최소화할 수 있습니다.
      • 시스템의 확장성과 복원력이 향상됩니다.
    • 단점:
      • 보상 작업이 제대로 설계되지 않으면 데이터 불일치가 발생할 수 있습니다.
      • 복잡한 비즈니스 로직에서는 보상 트랜잭션이 오히려 구현과 유지보수를 어렵게 만들 수도 있습니다.


그렇다면 계속해서 같은 예제 상황에서 보상 트랜잭션 방식을 적용할 경우, 실패 상황에 어떤 일이 발생하는지 살펴보도록 하겠습니다.

  1. 예약 서비스는 예약을 진행하고, 포인트 서비스는 사용자의 포인트 차감을 실행합니다.

  2. 예약 서비스가 성공적으로 예약을 완료했으나, 포인트 서비스에서 예기치 못한 오류가 발생했다고 가정합시다.

  3. 전통적 분산 트랜잭션 방식에서는 포인트 서비스의 오류로 인해 전체 트랜잭션이 롤백되어, 예약 서비스 역시 이전 상태로 복구되어야 합니다. 이 과정에서 모든 서비스가 동시에 잠금 상태에 들어가거나, 커뮤니케이션 지연으로 인해 전체 시스템의 응답 속도가 떨어질 수 있습니다.

  4. 반면, 보상 트랜잭션 방식에서는 예약 서비스가 먼저 예약을 완료한 후, 포인트 서비스가 실패할 경우, 예약 서비스에서 "예약 취소"라는 보상 작업을 실행합니다. 즉, 포인트 서비스의 오류를 감지하면 예약 서비스가 자체적으로 예약 취소 처리를 진행하여, 사용자에게 오류를 알리고 전체 시스템의 일관성을 유지할 수 있습니다.

  5. 이때, 각 서비스는 자체적인 로컬 트랜잭션을 사용하며, 실패 발생 시 보상 트랜잭션을 트리거하는 _Coordinator_가 존재하게 됩니다. 이 Coordinator는 각 서비스가 자신의 작업 결과를 보고하고, 문제가 발생했을 때 어떤 보상 작업을 실행할지 결정하는 역할을 합니다.

이와 같은 방식은 각 서비스가 서로의 세부 사항을 몰라도, 전반적인 비즈니스 로직의 원자성을 유지할 수 있도록 도와줍니다.

즉, 도메인 간의 의존성을 최소화하면서도 분산 환경에서의 복잡한 트랜잭션 처리를 효과적으로 관리할 수 있게 해주며, 심지어 LLT 없이도 이러한 일들이 가능하게 해줍니다.


III. Saga 패턴: 분산 환경에서의 트랜잭션 관리 대안

Saga 패턴은 이와 같은 분산 시스템 혹은 MSA 아키텍처가 보상 트랜잭션 처리 방안을 포함하도록 하는 디자인 패턴입니다.

Saga 라는 이름이 조금 궁금해서 여러가지 자료를 뒤져보았는데 가장 유력한 이름의 유래는 이렇습니다.

우리가 익히 들어 알고있는 문학의 한 갈래인 사가(Saga)는 고유한 몇 가지 특징을 지닙니다.

  • 긴 시간에 걸쳐 일어난 서사를 다룰 것.
  • 서사가 온전히 완결될 것. 즉, 명백히 밝혀지지 않거나 향후의 일들이 정확하게 정해지지 않는 열린 결말의 형태를 취하지 않을 것.

저는 이 두 가지 특징이 Saga 패턴의 '보상 트랜잭션을 포함하여 분산된 서비스들 전반에 걸친 트랜잭션이 온전히 완결되도록 하는 것'이라는 핵심과 맥을 함께 한다는 생각이 들었습니다.

조금 더 Saga Pattern 이 제시하는 설계 원칙과 구조는 다음과 같습니다.

  • 각 서비스는 자체적인 로컬 트랜잭션을 실행하고, 성공적으로 완료되면 다음 단계로 진행합니다.

  • 만약 어느 한 단계에서 문제가 발생하면, 해당 단계 이후의 작업이 실행되지 않고, 이미 커밋된 이전 단계의 작업에 대해 보상 트랜잭션을 수행하여 원래 상태로 복구합니다.
    예를 들어, 가예약 조회 → 결제 → 예약 확정 이라는 프로세스가 있다면, 결제 단계에서 실패했을 때 "예약 확정 취소"라는 보상 작업이 수행되도록 설계하는 것입니다.


2. Saga Pattern : 대표적인 두 가지 구현 방식

Saga Pattern을 실제로 구현하는 방식 중 대표적인 두 가지를 소개하려 합니다.

두 가지 구현방식은 결국 "보상 트랜잭션을 어떤 방식으로 구현할 것인가?" 에 대한 차이에 따라 구분됩니다.

두 가지 방식을 묘사하는 도식 모두 [microservices.io] 에서 가져왔음을 밝힙니다.

Choreography-based Saga

choreograph

위의 도식과 같이 각 서비스가 이벤트를 발행하고, 다른 서비스가 해당 이벤트를 구독하여 자신의 작업을 수행하는 방식입니다. 중앙의 제어자가 없고, 각 서비스가 독립적으로 반응하기 때문에 분산 처리에 유연성을 제공합니다.

Orchestration-based Saga

orcestration

중앙의 오케스트레이터가 전체 워크플로우를 제어하며, 각 서비스에 순서대로 작업을 지시하는 방식입니다. 이 방식은 흐름을 명확하게 관리할 수 있지만, 앞서 2PC 에서 기술하였듯, 오케스트레이터 자체가 단일 장애점(Single Point of Failure)이 될 수 있는 위험이 있습니다.


3. 장점과 한계

Saga Pattern 과 이를 활용한 MSA 아키텍처는 뚜렷하게 구분되는 장점과 단점을 모두 지니고 있습니다.

장점

  • 확장성: 각 서비스가 독립적으로 동작하기 때문에, 전체 시스템이 수평적으로 확장되기 용이합니다.

  • 장애 격리: 한 서비스의 문제로 인해 전체 트랜잭션이 중단되지 않으므로, 장애가 발생해도 다른 서비스에 미치는 영향을 최소화할 수 있습니다.

  • 유연한 복구: 실패 시 전체 롤백이 아닌 보상 트랜잭션을 통해 부분적인 복구가 가능하여, 비즈니스 연속성을 유지할 수 있습니다.

한계

  • 쉽지 않은 테스팅 : E2E 와 같은 테스팅을 제외한다면, 이렇게 완전하다시피 분리된 서비스 전반에 걸친 단위 혹은 통합 테스트를 실시할 수 있는 방법이 많지 않습니다.

  • 복잡성: 보상 트랜잭션의 설계와 구현이 복잡할 수 있습니다. 각 서비스가 독립적으로 수행한 작업을 취소하는 과정에서 예상치 못한 데이터 불일치가 발생할 수 있습니다.

  • 최종 일관성 문제: Saga 패턴은 즉각적인 강한 일관성을 보장하지 않고, 최종적으로 일관성을 맞추는 방식이기 때문에, 실시간 일관성이 중요한 경우에는 추가적인 설계가 필요합니다.

  • 운영 및 모니터링: 여러 서비스 간의 트랜잭션 흐름을 추적하고 문제를 진단하는 것이 어려울 수 있으며, 이를 위해 효과적인 모니터링과 로깅 시스템이 필요합니다.



IV. MSA : 정말로 E2E 테스팅이 유일한 테스트 방법일까?

여기서 보고서를 끝내기가 아쉬워 정말로 테스팅 방법이 E2E 방법 밖에 없을까, 하는 생각에 아주 조금 더 조사를 해보았습니다.

이 과정에서 발견한 것이 바로 Contract Testing 입니다.

Contract testing 의 핵심은 바로 '나의 테스트를 위해 다른 서비스를 중단하지 않는 것' 이라고 이해했습니다.

하지만 분산된 서비스라고 해도, 실제 내 서비스 내에 다른 서비스 API 에 호출하고 그 값을 받아 로직을 진행해야하는 구간이 분명 있을 것입니다.

이를 위해 "내가 이 요청을 보내면 당신은 이런 걸 보내줘" 라는 명세를 Contract - 두 분리된 Service 간 협의한 요청/응답 명세 - 라고 지칭하는 것으로 이해했습니다.

조금 더 제대로 이해하기 위해 Spring Cloud Contract 의 테스팅 과정을 살펴본 결과 다음과 같은 형태로 이루어집니다.

  1. 현재 내 서비스는 소비자, 그리고 내 서비스가 로직 수행을 위해 호출하여 응답을 받아야 하는 API 를 제공자 라고 합니다.
  2. 그리고 제공자 측에 내 서비스에서 테스트를 위해 제공자가 받을 요청과 이에 대해 응답할 값을 Groovy 스크립트로 작성하여 전달합니다.
  3. 그리고 해당 테스트가 성공할 경우 stub.jar 라는 서비스 자체에 대한 Stubbing 코드가 생성된다.
  4. 이를 제공자에 대한 stubbing 삼아 테스트를 진행한다.

IV. 현재 프로젝트에의 적용 : To Be Continued

⚠️ **GitHub.com Fallback** ⚠️