테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

분해와 분산 데이터

서비스를 얼마나 잘게 나눌지, 데이터를 어떻게 쪼갤지, 여러 서비스에 걸친 일을 사가로 어떻게 마무리할지 다룬다.

목차 (6)
  1. 얼마나 잘게 나눌까: 쪼개는 힘과 합치는 힘
  2. 데이터 쪼개기: 데이터 도메인과 소유권
  3. 분산 트랜잭션: 2PC와 사가
  4. 사가와 보상: 오케스트레이션과 코레오그래피
  5. 통신과 일관성: 사가를 가르는 세 축
  6. 계약과 코드 재사용

얼마나 잘게 나눌까: 쪼개는 힘과 합치는 힘

서비스를 나누기로 했다면 다음 질문은 크기다. 너무 굵으면 모놀리스의 문제가 그대로 남고, 너무 잘면 서비스끼리 끊임없이 부르고 트랜잭션이 여러 서비스에 걸친다. 서비스가 맡는 범위의 크기를 세분성(granularity)이라 한다. 코드 줄 수나 클래스 수는 좋은 잣대가 아니다. 대신 서비스를 쪼개자고 미는 힘과 합치자고 당기는 힘을 함께 따진다. 이 나눔은 포드·리처즈·사달라지·데그하니의 Software Architecture: The Hard Parts가 세분성 논의의 뼈대로 쓰는 생각을 이 노트의 말로 옮긴 것이다.

서비스 세분성을 정하는 힘. 쪼개는 힘은 하는 일이 둘 이상일 때, 한 부분만 자주 바뀌거나 부하가 클 때, 장애를 격리해야 할 때, 보안 수준이 다를 때 생긴다. 합치는 힘은 함께 성공해야 하는 트랜잭션, 서로 자주 부르는 업무 흐름, 함께 바뀌는 공유 코드, 강하게 얽힌 데이터에서 생긴다쪼개는 힘AB범위하는 일이 둘 이상이다변동성한 부분만 자주 바뀐다확장성한 부분만 부하가 크다장애 허용한 부분의 장애가 전체를 멈춘다보안한 부분만 더 엄격한 접근이 필요합치는 힘AB트랜잭션함께 성공하거나 실패해야 한다업무 흐름서로 자주 부르고 기다린다공유 코드같은 코드를 함께 바꾼다데이터 관계데이터가 서로 강하게 얽혔다두 힘이 맞서는 자리에 경계를 긋는다
쪼개는 힘과 합치는 힘. 한쪽 목록만 보고 결정하면 지나치게 잘거나 지나치게 굵은 서비스가 나온다.

예컨대 알림 서비스가 문자, 이메일, 앱 푸시를 모두 보낸다고 하자. 앱 푸시만 하루에 수백만 건이고 나머지는 적다면 확장성이 쪼개자고 민다. 하지만 세 채널이 같은 템플릿과 수신 거부 목록을 쓰고 늘 함께 바뀐다면 공유 코드와 데이터가 합치자고 당긴다. 이때는 푸시 발송기만 떼고 나머지는 둔다는 식의 중간 답이 나온다. 결정에는 이유를 붙여 ADR로 남긴다(아키텍처 특성과 결정 노트).

경계를 찾는 출발점으로는 업무 영역이 가장 믿을 만하다. 도메인 주도 설계의 하위 도메인과 경계 맥락(BoundedContext)에 맞춰 나누는 방법이 Decompose by subdomain이다. 데그하니가 마틴 파울러 사이트에 쓴 How to break a Monolith into Microservices는 의존이 적은 가장자리 기능부터 떼고, 코드보다 데이터와 함께 떼라고 권한다.

데이터 쪼개기: 데이터 도메인과 소유권

서비스는 나눴는데 데이터베이스는 하나라면, 테이블 구조라는 결정이 모든 서비스에 새어 나간다. 한 팀이 열 이름을 바꾸면 다른 팀 서비스가 깨지고, 누가 어떤 행을 바꿨는지 알 수 없다. 그래서 서비스마다 자기 데이터를 가지는 Database per service가 마이크로서비스의 기본 규칙이다. 꼭 데이터베이스 서버를 따로 둘 필요는 없고, 스키마를 나누고 다른 서비스의 스키마에 접근 권한을 주지 않는 것으로도 시작할 수 있다.

데이터 쪼개기. 공유 데이터베이스의 테이블을 주문, 회원, 상품 도메인으로 묶은 다음, 도메인마다 그 데이터를 소유하는 서비스와 스키마로 떼어 낸다. 주문 서비스는 재고 테이블을 직접 쓰지 않고 상품 서비스의 API로 요청한다공유 데이터베이스주문 도메인주문주문 항목회원 도메인회원배송지상품 도메인상품재고떼기도메인마다 소유자 하나주문 서비스주문 스키마회원 서비스회원 스키마상품 서비스상품 스키마주문은 재고 테이블을 직접 쓰지 않고 상품 서비스의 API로 "재고를 줄여 달라"고 요청한다
테이블을 먼저 도메인으로 묶고, 도메인마다 쓰는 서비스를 하나로 정한다. 여러 서비스가 같은 테이블을 쓰고 있다면 그 테이블의 주인을 하나로 정하는 일이 쪼개기의 대부분이다.

순서는 대략 이렇다. 먼저 테이블을 업무 영역별로 묶은 데이터 도메인을 정한다. 다음으로 도메인 사이를 잇는 외래 키와 조인을 찾아 끊을 방법을 정한다. 조인은 서비스 API 호출이나 복제된 읽기 전용 사본으로 바꾸고, 외래 키 제약은 애플리케이션이 검사하게 한다. 그다음 도메인마다 스키마를 떼어 낸다.

가장 까다로운 것은 소유권이다. 원칙은 "테이블에 쓰는 서비스는 하나"다. 위 그림에서 주문과 상품이 둘 다 재고를 바꾸고 있었다면, 재고는 상품 서비스가 갖고 주문은 상품 서비스에 "재고를 줄여 달라"고 요청한다. 여러 서비스가 같은 데이터에 정말 함께 써야 한다면, 그 데이터만 맡는 서비스를 따로 두거나 서비스들을 다시 합치는 것이 맞는지 따진다. 읽기만 한다면 소유 서비스가 이벤트로 바뀐 내용을 알리고, 읽는 쪽이 자기 사본을 둘 수도 있다. 이때 사본은 늦게 맞춰지므로 일관성 선택이 따라온다(일관성과 시간 노트).

분산 트랜잭션: 2PC와 사가

데이터를 나누면 트랜잭션도 나뉜다. 모놀리스에서는 "주문 저장, 재고 감소, 결제 기록"을 트랜잭션 하나로 묶으면 끝이었지만, 이제 세 서비스가 각자 자기 데이터베이스에 커밋한다. 이를 하나처럼 다루는 고전적인 방법이 2단계 커밋(2PC)이다. 조정자가 모두에게 "커밋할 준비가 됐나"를 묻고, 모두 "예"면 커밋을 명령한다. 동작과 함정은 복제와 합의 노트에 있다.

2PC와 사가 비교. 2PC는 조정자가 두 서비스에 준비를 묻고 모두 준비되면 커밋하며, 그동안 두 서비스는 잠금을 쥐고 기다린다. 사가는 T1, T2를 각자 바로 커밋하고 T3가 실패하면 보상 C2, C1을 거꾸로 실행해 되돌린다2단계 커밋 (2PC)서비스 A잠금 유지서비스 B잠금 유지조정자① 준비됐나? ② 모두 예 → 커밋원자적이지만, 느리고 조정자가 멈추면 막힌다사가단계마다 바로 커밋T1T2T3 실패C1C2실패하면 보상을 거꾸로잠금이 짧지만, 중간 상태가 남에게 보인다T: 각 서비스의 로컬 트랜잭션 · C: 그것을 되돌리는 보상 트랜잭션
2PC와 사가. 2PC는 원자성을 지키는 대신 참가자 모두가 잠금을 쥐고 기다린다. 사가는 기다리지 않는 대신 중간 상태를 드러내고 되돌리는 일을 직접 짠다.

서비스 사이에서 2PC는 잘 쓰이지 않는다. 모든 참가자가 같은 프로토콜(XA 등)을 지원해야 하고, 준비부터 커밋까지 잠금을 쥐어 처리량이 떨어지며, 조정자가 하필 그 사이에 멈추면 참가자들이 결과를 모른 채 묶여 있다. 메시지 브로커나 많은 NoSQL 저장소는 아예 참여하지 못한다. 그래서 쓰는 대안이 사가(saga)다. 가르시아몰리나와 세일럼의 1987년 논문 Sagas가 오래 걸리는 트랜잭션을 위해 제안한 개념으로, 큰 트랜잭션을 로컬 트랜잭션의 연속으로 쪼개고 단계마다 그것을 되돌리는 보상 트랜잭션을 짝지어 둔다.

사가와 보상: 오케스트레이션과 코레오그래피

사가의 흐름을 누가 쥐느냐에 따라 두 방식이 있다. 오케스트레이션은 사가 하나를 맡은 오케스트레이터가 참가 서비스에 차례로 명령하고 결과를 받아 다음을 정한다. 코레오그래피는 중앙 없이 서비스들이 서로의 이벤트를 듣고 다음 일을 한다. microservices.io의 Saga 패턴이 두 방식을 같은 주문 예로 비교한다. 아래는 오케스트레이션 사가가 실패를 만나 보상하는 과정이다.

오케스트레이션 사가 단계 그림. 주문 사가가 주문 생성, 재고 예약, 결제 승인 요청을 차례로 보내고, 결제가 거절되자 재고 예약 해제와 주문 취소를 보상으로 거꾸로 보낸다. 끝나면 주문은 취소됨, 재고는 원래대로, 결제는 거절됨이다주문 사가오케스트레이터① 주문 생성② 재고 예약③ 결제 승인 요청④ 보상: 예약 해제⑤ 보상: 주문 취소끝: 사가 취소 완료주문 서비스주문: 대기주문: 취소됨재고 서비스재고: 10개재고: 9개 (1개 예약)재고: 10개 (예약 해제)결제 서비스결제: 없음결제: 거절됨진행 명령보상 명령
1 / 6

주문 사가(오케스트레이터)가 주문 서비스에 주문을 만들게 한다. 주문은 바로 확정되지 않고 "대기" 상태로 로컬 커밋된다.

오케스트레이션 사가와 보상 트랜잭션. 한 단계가 실패하면 앞에서 커밋한 단계들을 보상으로 거꾸로 되돌린다. 보상은 롤백이 아니라 새 트랜잭션이다.

보상에는 몇 가지 주의가 따른다. 보상은 데이터베이스 롤백이 아니라 업무적으로 되돌리는 새 트랜잭션이라, 이미 나간 이메일처럼 되돌릴 수 없는 일은 "주문이 취소됐습니다"라는 새 이메일로 바로잡을 수밖에 없다. 보상도 실패할 수 있으므로 재시도해도 안전하게 멱등으로 만든다. 그래서 되돌리기 어려운 단계(결제 승인 같은)는 사가의 뒤쪽에, 되돌리기 쉬운 단계는 앞쪽에 둔다. 마이크로소프트 아키텍처 센터의 Compensating Transaction 패턴이 이런 고려를 정리한다.

격리가 없다는 점도 기억해야 한다. 사가가 도는 동안 다른 요청은 "대기" 주문과 줄어든 재고를 본다. 그래서 상태 이름으로 중간 단계를 드러내고(대기, 확정, 취소), 대기 상태의 데이터를 다른 흐름이 함부로 쓰지 못하게 막는 식으로 대응한다. 단계 사이의 메시지가 빠지지 않게 하려면, 로컬 커밋과 메시지 발행을 한 트랜잭션으로 묶는 트랜잭셔널 아웃박스를 함께 쓴다.

통신과 일관성: 사가를 가르는 세 축

서비스 사이 흐름의 성격은 세 가지 선택으로 정해진다. 부르고 기다리는 동기인가, 메시지를 두고 떠나는 비동기인가. 모두 함께 성공하거나 실패하는 원자적 결과를 약속하는가, 잠시 어긋나도 결국 맞춰지는 최종 일관성인가. 흐름을 오케스트레이션으로 한곳에서 쥐는가, 코레오그래피로 나눠 맡기는가. The Hard Parts는 이 세 축의 여덟 조합에 각각 재치 있는 이름을 붙여 비교한다.

사가를 가르는 세 축. 통신은 동기와 비동기, 일관성은 원자적과 최종 일관성, 조정은 오케스트레이션과 코레오그래피 중에서 고른다. 셋 모두 단단한 쪽을 고르면 모놀리스를 흉내 내는 에픽 사가, 셋 모두 느슨한 쪽을 고르면 가장 독립적인 앤솔러지 사가가 된다단단하게 묶는 쪽느슨하게 푸는 쪽통신동기비동기일관성원자적최종 일관성조정오케스트레이션코레오그래피모놀리스를 흉내 낸다책의 이름: 에픽(Epic) 사가가장 독립적이다책의 이름: 앤솔러지(Anthology) 사가셋을 모두한쪽으로2 × 2 × 2 = 여덟 조합. 사이의 조합들은 한 축씩 풀거나 묶은 것이다
세 축을 모두 단단한 쪽으로 고르면 결합이 가장 강하고, 모두 느슨한 쪽으로 고르면 가장 독립적이지만 이해하고 디버깅하기 가장 어렵다. 양 끝 조합의 이름은 The Hard Parts의 것을 빌렸다.

단단한 쪽은 이해하기 쉽고 결과가 명확하지만, 모든 참가자가 동시에 살아 있어야 하고 가장 느린 참가자에게 응답 시간이 묶인다. 사실상 분산된 모놀리스다. 느슨한 쪽은 처리량과 장애 격리가 좋지만 전체 흐름이 어디에도 한곳에 적혀 있지 않고, 사용자에게 "처리 중"을 보여 줘야 한다. 대부분의 시스템은 그 사이 어딘가를 고른다. 흔한 조합은 비동기 통신과 최종 일관성에, 흐름이 복잡하면 오케스트레이션, 단순하면 코레오그래피를 얹는 것이다.

어느 축이든 판단 기준은 업무다. 사용자가 즉시 확답을 받아야 하는가(좌석 예약), 몇 초 뒤에 알게 되어도 되는가(포인트 적립). 즉시 확답이 필요한 부분만 동기로 두고 나머지를 비동기로 미루면, 원자성이 필요한 범위가 줄어 사가가 단순해진다.

계약과 코드 재사용

서비스는 서로의 코드가 아니라 계약(API 스키마, 메시지 형식)으로 엮인다. 엄격한 계약은 스키마를 정확히 지키게 해 타입 안전성과 검증을 얻지만, 필드 하나를 바꿔도 모든 소비자가 함께 바뀌어야 한다. 느슨한 계약은 소비자가 자기가 쓰는 필드만 읽고 모르는 필드는 무시하게 해 변경에 강하지만, 실수로 필요한 필드를 지워도 컴파일러가 잡아 주지 않는다.

엄격한 계약과 느슨한 계약. 왼쪽은 소비자가 제공자 스키마 전체로 만든 코드를 써서, 제공자가 memo 필드 이름을 바꾸자 소비자가 깨진다. 오른쪽은 소비자가 id와 total만 쓴다고 계약 테스트로 선언하고, 제공자 파이프라인이 그 테스트를 돌려 두 필드가 지켜지는 한 다른 필드를 바꿔도 된다엄격한 계약제공자 응답소비자가 쓰는 것ididtotaltotalcurrencycurrencymemo → notememo 없음!스키마 전체에 묶인다느슨한 계약 + 소비자 주도 테스트제공자 응답소비자가 쓰는 것ididtotaltotalcurrencymemo → note쓰는 필드만 약속한다필드 하나 바꿨는데 소비자가 깨진다계약 테스트: "id, total이 있어야 한다"제공자 파이프라인에서 통과
같은 필드 변경이 두 계약에서 다르게 끝난다. 느슨한 계약의 약점(무엇이 쓰이는지 모름)을 소비자 주도 계약 테스트가 메운다.

이 사이를 잇는 방법이 이언 로빈슨이 정리한 소비자 주도 계약(consumer-driven contracts)이다. 소비자마다 자기가 기대하는 부분을 테스트로 적어 제공자에게 넘기고, 제공자는 그 테스트를 모두 통과하는 한 자유롭게 바꾼다. 어떤 필드를 아무도 안 쓰는지도 알 수 있다.

코드 재사용의 세 방법. 공유 라이브러리는 서비스마다 빌드 때 함께 묶여 버전이 갈린다. 공유 서비스는 실행 때 네트워크로 불러 그 서비스가 죽으면 모두 영향을 받는다. 사이드카와 서비스 메시는 로그, 인증, 재시도 같은 운영 관심사만 서비스 옆의 프록시에 맡기고 제어 평면이 설정을 내려보낸다공유 라이브러리빌드 때 묶인다버전이 서비스마다 갈린다공유 서비스실행 때 묶인다죽으면 모두 영향사이드카·메시운영 관심사만 옆에업무 코드는 공유 안 함주문lib 1.2배송lib 1.4주문배송공용서비스제어 평면 (정책·설정)주문사이드카배송사이드카
코드 재사용의 세 방법. 위로 갈수록 업무 코드를 공유하고, 사이드카는 업무와 무관한 운영 코드만 공유한다.
  • 공유 라이브러리: 빌드 때 각 서비스에 들어간다. 호출이 빠르고 실행 중에 엮이지 않지만, 라이브러리를 고치면 쓰는 서비스를 모두 다시 빌드하고 배포해야 하고, 버전이 서비스마다 갈린다. 작고 잘 안 바뀌는 코드(날짜 처리, 공통 오류 형식)에 맞는다.
  • 공유 서비스: 공통 기능을 서비스 하나로 두고 네트워크로 부른다. 고치면 한 번 배포로 끝나지만, 실행 중에 모두가 그 서비스에 묶여 아키텍처 퀀텀이 커지고(모듈성과 결합 노트), 그 서비스가 느려지면 모두 느려진다.
  • 사이드카와 서비스 메시: 로그, 지표, 인증서, 재시도, 서킷 브레이커 같은 운영 관심사를 서비스 옆에 붙는 프록시(Sidecar 패턴)에 맡기고, 제어 평면이 모든 사이드카의 설정을 한꺼번에 바꾼다. 업무 코드는 공유하지 않으면서 운영은 일관되게 한다. 서비스끼리 상호 TLS로 신원을 확인하는 구조는 제로 트러스트 노트에도 나온다.

마이크로서비스에서는 업무 코드의 재사용보다 독립을 고르는 편이 대개 낫다. 두 서비스에 비슷한 코드가 있어도 바뀌는 이유가 다르면 각자 갖는 편이 결합을 줄인다. 같은 이유로 함께 바뀌는 코드라면, 그 두 서비스는 원래 하나였어야 하는지 다시 물어볼 때다.