본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로웹 개발 › 백엔드 › Spring Cloud

0. 마이크로서비스와 Spring Cloud

목차

마이크로서비스와 Spring Cloud

서비스를 여러 프로세스로 나누면 주문 기능만 따로 배포하거나 확장할 수 있다. 대신 기존에는 같은 프로세스 안의 함수 호출이던 작업이 네트워크 요청이 되고, 다른 서비스의 지연·장애·데이터 변경을 함께 다뤄야 한다. Spring Cloud는 이때 반복해서 필요한 설정 관리, 서비스 발견, 라우팅, 로드 밸런싱, 서킷 브레이커 등의 도구를 제공한다. 서비스를 나누는 기준과 장애 시 업무 규칙은 여전히 개발자가 결정해야 한다.

마이크로서비스란

마이크로서비스는 기능을 무조건 작게 쪼갠다는 뜻보다, 하나의 업무 능력을 담당하고 독립적으로 변경·배포할 수 있는 서비스에 가깝다. 온라인 상점이라면 주문, 결제, 배송이 후보가 될 수 있다. 다만 주문과 결제의 업무 규칙이 강하게 얽혀 매번 함께 변경된다면, 처음부터 별도 서비스로 분리하는 것이 오히려 복잡도를 높일 수 있다.

서비스 경계를 정할 때는 다음을 확인한다.

  1. 어느 팀이 업무 규칙과 변경을 책임지는가?
  2. 다른 기능과 독립적으로 배포·확장해야 할 이유가 있는가?
  3. 다른 서비스의 데이터베이스 테이블을 직접 고치지 않고 공개된 API나 이벤트로 협력할 수 있는가?
  4. 서비스 하나가 멈췄을 때 사용자에게 어떤 결과를 반환해야 하는가?

각 서비스가 자신의 데이터 변경을 책임지더라도, 여러 서비스에 걸친 작업이 자동으로 하나의 트랜잭션이 되지는 않는다. 서비스 사이의 데이터 일관성과 실패 후 복구 방법을 업무 흐름에 맞춰 설계해야 한다.

SOA와의 관계

SOA(Service-Oriented Architecture)도 기능을 서비스로 나누고 계약을 통해 연결하는 접근이다. 마이크로서비스는 SOA와 완전히 반대되는 방식이라기보다 작은 업무 경계와 독립적인 배포·운영을 강조하는 서비스 지향 접근으로 볼 수 있다. 특정 ESB를 반드시 쓰거나, 서비스마다 반드시 다른 언어를 써야 마이크로서비스가 되는 것은 아니다. 이름보다 중요한 것은 팀이 실제로 각 서비스를 독립적으로 변경할 수 있는지다.

주문 요청으로 보는 통신 흐름

아래 그림은 주문 서비스가 결제 서비스에 동기 요청을 보내는 예시 구성이다. 게이트웨이, 서비스 레지스트리, 중앙 설정 서버를 모든 시스템에 반드시 넣어야 한다는 의미는 아니다. 실행 환경이 Kubernetes라면 플랫폼의 서비스 이름과 DNS를 발견 수단으로 사용할 수도 있다.

flowchart LR
  사용자 --> 게이트웨이[API Gateway]
  게이트웨이 --> 주문[주문 서비스]
  주문 -->|결제 요청| 결제[결제 서비스]
  결제 -->|성공·실패 응답| 주문
  주문 -->|주문 결과| 게이트웨이
  발견[서비스 발견·로드 밸런싱] -.-> 주문
  설정[외부 설정] -.-> 주문
  설정 -.-> 결제

예를 들어 사용자가 주문을 요청하면 주문 서비스는 주문을 PENDING으로 기록하고 결제 서비스에 처리를 요청할 수 있다. 결제가 성공하면 PAID로 진행한다. 결제 서비스가 응답하지 않았을 때는 단순히 “실패”로 단정하기 어렵다. 결제는 처리됐는데 응답만 유실됐을 수도 있기 때문이다. 타임아웃, 결제 상태 조회, 멱등성 키, 재시도·보상 절차를 함께 설계해야 중복 청구를 피할 수 있다.

동기 호출은 처리 결과를 즉시 반환하기 쉽지만 결제 서비스의 지연이 주문 응답에도 전파된다. 메시지 기반 비동기 처리로 바꾸면 응답을 먼저 줄 수 있는 대신, PENDING 상태와 최종 결과를 사용자에게 어떻게 보여줄지 정해야 한다.

Spring Cloud가 맡는 부분

Spring Cloud는 하나의 서버 제품이 아니라 여러 프로젝트의 묶음이다. 필요한 문제에 맞는 구성 요소만 선택한다.

문제관련 도구확인할 점
환경별 설정을 서비스 밖에서 관리Spring Cloud Config설정 원본의 접근 권한과 변경 반영 방식
외부 요청을 내부 서비스로 전달Spring Cloud Gateway라우팅, 인증 책임, 타임아웃과 필터 순서
바뀌는 서비스 인스턴스 주소 찾기DiscoveryClient, Spring Cloud LoadBalancer레지스트리 또는 실행 플랫폼의 발견 기능 중 무엇을 쓸지
하위 서비스 장애가 연쇄적으로 번지지 않게 하기Spring Cloud Circuit Breaker언제 차단하고 어떤 오류·대체 결과를 돌려줄지
이벤트를 메시지 브로커와 연결Spring Cloud Stream중복 메시지, 처리 순서, 실패 시 재처리

서킷 브레이커를 추가했다고 실패한 결제가 저절로 취소되지는 않는다. 폴백으로 PAID를 반환하면 오히려 잘못된 주문 상태를 만들 수 있다. 어떤 기능은 읽기 전용 캐시로 응답할 수 있지만, 결제처럼 확정이 필요한 작업은 불확실한 상태를 명시하고 나중에 확인하는 편이 안전하다.

프로젝트를 시작할 때는 Spring Cloud 공식 프로젝트 페이지에서 사용 중인 Spring Boot와 호환되는 release train을 확인한다. 의존성 버전을 각각 임의로 섞기보다 공식 spring-cloud-dependencies BOM을 적용하는 방식이 권장된다.

Cloud Native Architecture와 운영 비용

기존 메모의 자동 확장, Chaos Engineering, 지속 배포, 컨테이너는 마이크로서비스를 운영할 때 자주 함께 쓰는 실천이지만 Spring Cloud가 자동으로 제공하는 결과는 아니다.

  • 확장성: 수평 확장이 가능한 서비스라도 상태 저장 방식과 데이터베이스 용량이 병목이면 인스턴스만 늘려서는 해결되지 않는다. 자동 확장은 Kubernetes나 클라우드 플랫폼의 정책과 지표가 담당한다.
  • 탄력성: 타임아웃, 자원 제한, 재시도 기준, 서킷 브레이커를 조합해야 장애의 영향을 줄일 수 있다. 모든 오류를 무조건 재시도하면 하위 서비스에 부하를 더한다.
  • 장애 격리: 결제 서비스가 멈춰도 상품 조회까지 멈추지 않도록 의존 관계를 분리할 수 있다. 그러나 주문 요청이 결제 응답을 기다리는 구조라면 그 요청 자체는 영향을 받는다. 독립 배포만으로 완전한 격리가 보장되지는 않는다.
  • 지속 배포와 관측: 서비스별 빌드·테스트·배포 흐름, 로그·지표·추적 정보를 연결해야 변경과 장애를 따라갈 수 있다. Chaos Engineering은 이런 복구 가정을 실험으로 검증하는 방법이지, 배포만으로 얻는 성질이 아니다.

규모가 작은 시스템에서는 단일 애플리케이션 안에서 모듈 경계를 먼저 분명히 하는 편이 운영하기 쉬울 수 있다. 특정 부분의 독립 배포와 확장이 실제 요구가 되었을 때 분리하면, 네트워크와 데이터 일관성 문제를 감당할 이유도 명확해진다.

참고 자료

결제 응답이 사라진 한 건을 처리하기

위 주문 흐름에서 결제 요청이 타임아웃되었다면 주문 서비스는 결제를 바로 FAILED로 확정할 수 없다. 결제 서비스가 돈을 청구했지만 응답이 네트워크에서 사라졌을 수 있기 때문이다. 상태를 PAYMENT_UNKNOWN으로 두고 같은 결제 요청 ID로 상태를 조회하거나 재요청한다. 재요청을 허용하려면 결제 서비스가 요청 ID를 저장하고 중복 요청에 같은 결과를 돌려줘야 한다.

stateDiagram-v2
  [*] --> PENDING: 주문 접수
  PENDING --> PAID: 결제 성공 확인
  PENDING --> PAYMENT_UNKNOWN: 응답 타임아웃
  PAYMENT_UNKNOWN --> PAID: 상태 조회에서 성공
  PAYMENT_UNKNOWN --> FAILED: 상태 조회에서 실패
  FAILED --> [*]: 주문 취소 처리

이 그림은 업무 규칙의 예다. PAYMENT_UNKNOWN을 화면에 어떻게 보여줄지, 언제까지 조회할지, 끝내 확인할 수 없을 때 운영자 개입이 필요한지 정해야 한다. Circuit Breaker의 “열림”은 당분간 원격 호출을 차단한다는 기술 상태이며 주문의 결제 실패와 같은 뜻이 아니다.

데이터 변경과 이벤트 발행 사이의 틈

주문 DB에 PAID를 저장한 뒤 배송 이벤트를 메시지 브로커에 보내는 경우를 생각하자. 두 단계 사이에서 프로세스가 멈추면 DB에는 결제가 완료됐지만 배송 서비스는 아무 소식을 못 받는다. DB 트랜잭션 안에 주문 변경과 발행 예정 이벤트(outbox)를 함께 기록하고, 별도 발행자가 outbox를 브로커로 전달하는 패턴을 검토할 수 있다.

이 방식에서도 이벤트가 중복 전달될 수 있으므로 배송 소비자는 주문 ID와 이벤트 ID로 중복 처리를 막는다. “메시지 브로커를 쓴다”는 사실만으로 정확히 한 번의 업무 처리가 보장되지 않는다. Spring Cloud Stream은 브로커 연결을 단순하게 해주지만 이런 데이터 경계와 멱등성은 애플리케이션이 결정한다.

성능 측정도 서비스별 평균 지연만으로 끝내지 않는다. 게이트웨이→주문→결제 요청을 하나의 추적 ID로 따라가고, 각 단계의 타임아웃·재시도 횟수·최종 업무 상태를 함께 본다. 호출이 세 번 재시도돼 최종 성공했더라도 결제가 세 번 청구됐다면 기술적 성공률은 잘못된 지표다.

처음 서비스 분리를 결정할 때는 이 실패 흐름을 그림으로 그려 본다. 한 프로세스 안의 모듈로 둘 때와 비교해 독립 배포의 이익이 네트워크·관측·복구 비용을 감당할 만큼 큰지 판단할 수 있다.

참고: Spring Cloud Stream, Spring Cloud Circuit Breaker.