割ると何が良くなり、何が悪くなるのか
한국어 원문으로 표시합니다.
한 줄 요약
마이크로서비스는 성능을 위한 선택이 아니라, 팀이 서로를 기다리지 않고 배포하기 위한 선택이다. 그 대가로 함수 호출이었던 것이 네트워크 호출이 된다.
왜 이게 필요했나
모놀리스가 나쁘다는 말을 자주 듣습니다. 하지만 실제로 무너지는 순서를 보면 코드가 먼저가 아닙니다. 배포가 먼저입니다. 한 저장소에 여덟 팀이 커밋하고, 릴리스 브랜치를 자르는 데 이틀이 걸리고, 한 팀의 버그로 전체 배포가 멈추는 상황 — 여기서 사람들이 "쪼개자"고 말하기 시작합니다.
그 진단은 대체로 맞습니다. 문제는 그다음입니다. 쪼개는 순간 잃는 것들의 목록이 대개 준비돼 있지 않습니다. 모놀리스 안에서 inventory.reserve(sku, 3) 은 나노초 단위의 함수 호출이었고 실패할 수 없었습니다. 서비스로 나눈 순간 그것은 밀리초 단위의 HTTP 호출이 되고, 타임아웃될 수 있고, 반쯤 성공할 수 있고, 상대가 재시작 중일 수 있습니다. 그리고 그 호출을 감싸는 트랜잭션은 더 이상 존재하지 않습니다.
어떻게 동작하나
판단의 기준은 세 가지입니다.
첫째, 배포 독립성이 실제로 필요한가. 두 기능이 항상 같이 배포된다면 나누는 이득이 거의 없습니다. 나눠 놓고도 늘 함께 릴리스한다면 그건 분산 모놀리스이고, 모놀리스의 단점에 분산 시스템의 단점을 더한 것입니다.
둘째, 데이터가 갈라지는가. 두 서비스가 같은 테이블에 쓰기를 계속한다면 경계가 잘못 그어진 것입니다. 서비스 경계는 코드 경계가 아니라 데이터 소유권 경계입니다.
셋째, 팀이 있는가. 서비스마다 온콜을 설 사람이 필요합니다. 세 명짜리 팀이 아홉 개 서비스를 운영하면 배포가 빨라지는 대신 새벽 호출이 세 배가 됩니다.
전환 방법은 빅뱅이 아니라 교살 무화과(Strangler Fig)입니다. 앞에 파사드를 두고, 한 기능씩 새 서비스로 옮기고, 트래픽을 비율로 넘기고, 안정되면 모놀리스에서 그 코드를 지웁니다. 변환 → 공존 → 제거를 반복합니다. 이 방식의 핵심은 언제든 되돌릴 수 있다는 점입니다. 파사드의 라우팅 한 줄만 되돌리면 됩니다.
현장에서 만나는 모습
파사드는 대개 API 게이트웨이(Kong, Envoy, NGINX)가 맡습니다. 여기에 부패 방지 계층(Anti-Corruption Layer)을 함께 둡니다. 레거시의 데이터 모델이 새 서비스로 스며들지 않게 번역해 주는 층입니다. 이걸 생략하면 새 서비스가 6개월 만에 레거시 스키마를 그대로 닮아 갑니다. 그러면 쪼갠 의미가 사라집니다.
가장 자주 보는 실패는 "재고 서비스를 뗐는데 재고 테이블은 그대로 공유"입니다. 이렇게 하면 배포는 나뉘었지만 스키마 변경은 여전히 두 팀의 합의가 필요합니다. 결합은 코드가 아니라 데이터에 있었기 때문입니다.
쪼개기 전에 치를 비용의 목록
서비스 하나를 늘리면 코드만 늘어나는 것이 아닙니다. 실제로 따라오는 것들입니다.
| 항목 | 늘어나는 것 |
|---|---|
| 저장소·CI | 파이프라인 하나, 빌드 시간, 시크릿 관리 |
| 배포 | 매니페스트, 롤아웃 전략, 롤백 절차 |
| 관측 | 대시보드, 경보, 로그 라벨, 트레이스 전파 |
| 온콜 | 런북 하나, 담당자 지정 |
| 통신 | 네트워크 왕복, 직렬화, 재시도·타임아웃 설계 |
| 데이터 | 트랜잭션 경계가 끊긴다 — 사가나 최종 일관성 |
마지막 줄이 가장 비쌉니다. 한 트랜잭션 안에서 끝나던 일이 보상 트랜잭션과 상태 기계 가 됩니다. 코드가 세 배가 되고 버그가 그 안에 숨습니다.
그래도 쪼개는 것이 옳은 신호
비용이 큰데도 쪼개야 하는 경우가 있습니다. 셋 중 하나면 값을 합니다.
배포 주기가 크게 다르다. 하루에 열 번 나가는 부분과 분기에 한 번 나가는 부분이 한 저장소에 있으면, 느린 쪽이 빠른 쪽을 붙잡습니다.
자원 특성이 다르다. GPU 를 쓰는 추론과 평범한 CRUD 를 같은 파드에 두면 GPU 노드에 CRUD 가 함께 올라갑니다. 확장 축이 다르면 나누는 것이 맞습니다.
장애 격리가 필요하다. 리포트 생성이 메모리를 다 써서 결제까지 죽는다면, 그 둘은 분리되어야 합니다.
팀이 다르다 는 것만으로는 부족합니다. 팀은 바뀌고, 조직도를 따라 시스템을 나누면 조직이 바뀔 때마다 시스템을 다시 나눠야 합니다.
되돌리는 것도 설계다
나눈 것을 다시 합치는 일은 생각보다 자주 옳습니다. 그런데 대부분의 팀이 이것을 실패로 여겨 미룹니다.
합쳐야 한다는 신호입니다.
- 두 서비스가 항상 함께 배포 된다
- 한 기능을 만들 때 양쪽을 모두 고쳐야 한다
- 사이의 호출이 동기이고 필수 다(하나가 죽으면 다른 하나도 무용지물)
- 데이터가 한 트랜잭션이어야 하는데 사가로 흉내 내고 있다
셋 이상 해당하면 합치는 편이 낫습니다. 모듈러 모놀리스로 되돌리는 것은 후퇴가 아니라 정정 입니다.
쪼개는 순서 — 무엇을 먼저 떼는가
쪼개기로 정했다면 다음 질문은 순서다. 아무 데나 먼저 떼면 가장 어려운 것을 가장 먼저 하게 된다.
먼저 뗄 후보는 세 조건을 만족한다. 데이터를 거의 공유하지 않고, 배포 주기가 본체와 다르고, 실패해도 본체가 계속 도는 것. 알림 발송, 파일 변환, 리포트 생성, 검색 색인 같은 것이 여기 해당한다.
나중에 뗄 것은 거래의 중심이다. 주문·결제·재고처럼 하나의 트랜잭션으로 묶여 있던 것을 나누면, 그 순간 분산 트랜잭션 문제를 떠안는다. 보상 트랜잭션이나 사가 패턴이 필요해지고, 그것은 팀이 준비된 뒤에 할 일이다.
데이터를 먼저 나눈다. 서비스만 나누고 데이터베이스를 공유하면 최악이다. 배포는 따로 하는데 스키마는 함께 바뀌므로, 양쪽 배포를 동시에 맞춰야 하는 상태가 된다. 이럴 바에는 모놀리스가 낫다.
떼기 전에 경계를 코드 안에서 먼저 만든다. 모듈로 나누고, 그 모듈 사이의 호출을 인터페이스 하나로 좁히고, 그 인터페이스가 안정될 때까지 기다린다. 그 상태가 되면 프로세스를 나누는 것은 기계적인 작업이 된다. 경계를 모른 채 프로세스부터 나누면 그 경계를 네트워크 너머에서 고치게 된다.
되돌릴 수 있게 나눈다. 새 서비스로 트래픽을 조금씩 보내고, 문제가 있으면 본체의 코드로 되돌린다. 본체에서 코드를 지우는 것은 몇 주 동안 문제가 없었을 때 한다.
측정할 것을 미리 정한다. 배포 빈도, 변경 실패율, 그리고 한 기능을 고치는 데 건드리는 저장소 수. 마지막 것이 늘고 있다면 경계가 틀린 것이므로, 더 쪼개기 전에 멈추고 다시 본다.
다음 실습에서 할 것
주문과 재고가 한 프로세스에 들어 있는 모놀리스를 받아, 계약을 먼저 문서로 고정하고, 두 개의 서비스로 나눕니다. 나눈 뒤 두 구현의 응답이 같은지 자동으로 비교하는 스크립트를 만들고, 마지막에 "이건 쪼개지 말았어야 했다"에 해당하는 근거도 한 줄 적습니다.