Backend Deep Dive #3: MSA의 환상과 현실: 멀티레포, 공통 코드, 배포 전략까지

backendmonorepomultirepographQLci-cdmsa

Backend Deep Dive.
MSA(마이크로서비스 아키텍처)를 도입하면 모든 것이 해결될 것처럼 보일 때가 있다. 하지만 이론과 현실의 간극은 크다. 실제 현장에서는 이론에서는 접할 수 없는 수많은 문제와 마주하게 된다.
실제 현장에서 마주친 직간접적인 경험들과 고민을 바탕으로 MSA 환경의 현실적인 과제들을 파헤쳐 보려고 한다. 소스 코드는 어떻게 관리해야 하는지, 서비스 간의 공통 코드는 어디까지 허용해야 하는지, 그리고 자동화된 배포 파이프라인이 숨기고 있는 위험은 무엇인지 알아본다.

1. 레포지토리 관리: 모노레포 vs. 멀티레포

MSA를 시작할 때 마주하는 첫 번째 갈림길은 소스 코드 관리 전략이다. 모든 서비스의 코드를 하나의 거대한 저장소(모노레포)에 둘 것인가, 아니면 서비스별로 별도의 저장소(멀티레포)에 둘 것인가.

멀티레포의 장점: 낮은 진입장벽과 명확한 책임

MSA를 처음 도입하거나 팀의 구조상 각자의 역할과 책임을 명확히 나누고 싶을 때, 멀티레포는 매력적인 선택지가 된다.
특히 프론트엔드 개발자에게 비유하자면, 각 서비스 레포지토리는 마치 독립적인 npm 패키지나 컴포넌트 라이브러리처럼 느껴진다. 다른 프로젝트에 영향을 줄 걱정 없이 오직 내 프로젝트에만 집중할 수 있는 것이다.

  • 명확한 책임과 소유권: 개발자는 자신이 맡은 서비스의 저장소만 신경 쓰면 된다. "내 코드는 이 안에만 있다"는 사실은 심리적인 안정감을 준다. 다른 서비스의 코드를 실수로 건드리거나, 내 변경 사항이 예상치 못한 곳에서 문제를 일으킬 가능성이 원천적으로 차단되기 때문이다.

  • 단순한 빌드와 배포: 해당 서비스의 코드만 빌드하고 테스트하면 되므로 CI/CD 파이프라인이 단순하고 이해하기 쉽다. 다른 서비스의 코드가 변경되거나 빌드가 실패해도 내 서비스의 배포에는 아무런 영향이 없다. 이는 독립적인 개발과 배포 주기를 가능하게 한다.

반면, 모노레포는 모든 서비스의 코드가 한곳에 있다. 이는 마치 거대한 애플리케이션의 모든 소스코드가 하나의 폴더에 있는 것과 같다. 사소한 변경 하나가 전체 시스템에 어떤 영향을 미칠지 예측하기 어렵고, 효율적인 빌드(변경된 부분만 골라서 빌드)와 테스트를 위해서는 고도화된 빌드 시스템(예: Bazel, Gradle)에 대한 깊은 이해가 필요하다. 이는 상당한 학습 곡선을 요구한다.

모노레포 vs. 멀티레포: 장단점 분석

구분모노레포 (Mono-repo)멀티레포 (Multi-repo)
장점• 간소화된 의존성 관리: 모든 프로젝트가 동일한 라이브러리를 사용하여 버전 충돌이 없음
• 원자적 리팩토링: 여러 서비스 변경을 하나의 커밋으로 처리 가능
• 코드 공유와 일관성 유지가 쉬움
• 중앙화된 가시성: 전체 시스템 파악이 용이
• 팀의 자율성과 독립성 보장
• 단순하고 빠른 CI/CD 구축 가능
• 명확한 소유권: 서비스별 책임이 분리됨
• 유연한 기술 스택: 서비스별로 다른 언어나 프레임워크 사용 가능
단점• 복잡한 빌드 시스템 필요 (예: Bazel, Gradle)
• 빌드 시간 증가: 전체 빌드/테스트 시간이 길어질 수 있음
• 가파른 학습 곡선: 신규 입사자가 전체 구조를 익히기 어려움
• Git 성능 저하 가능성 (히스토리 증가 시)
• 의존성 충돌 위험 (서비스별 라이브러리 버전 불일치)
• 교차 서비스 리팩토링이 어려움
• 코드 중복 및 비일관성 발생 가능
• 낮은 코드 가시성: 다른 서비스 구조 파악이 어려움

"서버 유지비용은 줄었지만, 통신비용은 늘었다"

이 말은 MSA의 장단점을 표현하는데 사용될 수 있다. 서버 유지비용은 각 서비스가 필요한 만큼의 리소스만 사용하므로 최적화하기 쉽다. 하지만 MSA는 두 가지 종류의 '통신비용'을 급격히 증가시킨다.

1. 물리적 통신비용 (네트워크 비용 및 지연 시간)

모놀리식 환경에서는 서비스 A가 서비스 B의 기능이 필요할 때, 단순히 메모리 안에서 함수를 호출(functionB())한다. 이는 전기 신호의 속도로 이루어지며 거의 비용이 없다.

하지만 MSA 환경에서는 서비스 A가 네트워크를 통해 서비스 B에게 API 요청(HTTP, gRPC 등)을 보내야 한다. 이 과정에서는 다음과 같은 실제 비용이 발생한다.

  • 지연 시간 (Latency): 네트워크를 거치는 시간만큼 응답이 느려진다. 하나의 요청을 처리하기 위해 여러 서비스가 연쇄적으로 통신한다면, 이 지연 시간은 곱절로 늘어난다.

  • 데이터 전송 비용: 클라우드 환경에서는 네트워크를 통해 오고 가는 데이터의 양(Egress/Ingress)에 따라 실제 돈을 지불해야 한다. 서비스가 많아질수록 이 비용은 무시할 수 없는 수준이 된다.

2. 조율 통신비용 (사람과 코드의 소통 비용)

이것이 바로 '보이지 않는 비용'이다. API 명세를 맞추기 위한 회의 시간, 버전 충돌을 해결하기 위한 노력, 여러 저장소를 넘나들며 전체 비즈니스 흐름을 파악하는 데 드는 리소스 비용 등이 모두 여기에 포함된다.

그렇다면 왜 많은 사람들이 MSA의 단점을 이야기할 때, 실제 네트워크 비용보다 이 '보이지 않는' 조율 비용을 더 강조할까? 그 이유는 '보이지 않는 비용'이 프로젝트의 속도와 성공에 훨씬 더 치명적인 영향을 미치기 때문이다.

네트워크 지연 시간이나 데이터 전송 비용은 캐싱, 더 빠른 네트워크, 효율적인 프로토콜 사용 등 기술적으로 해결하거나 예측하기가 비교적 쉽다. 하지만 개발자 간의 소통 문제, 깨진 일관성, 복잡한 의존성으로 인한 버그 등은 예측하기 어렵고, 한번 발생하면 해결하는 데 엄청난 시간과 노력이 필요하며, 최악의 경우 프로젝트 전체를 실패로 이끌 수 있다.

결국 "통신비용이 늘었다"는 말은 눈에 보이는 네트워크 비용도 늘었지만, 그보다 훨씬 더 고통스럽고 해결하기 어려운 '사람과 코드의 소통 비용'이 폭발적으로 증가했다는 의미로 해석하는 것이 더 정확하다.

2. 공통 코드의 함정: 스키마 공유는 왜 재앙인가?

멀티레포의 가장 큰 숙제는 '공통 코드 관리'이다. 이때 반드시 지켜야 할 원칙이 있다.

"공통 코드는 순수한 유틸리티(Utility) 성격만 가져야 한다."

데이터베이스 스키마(예: JPA의 @Entity 클래스)나 서비스 간 데이터 전송에 사용되는 DTO(Data Transfer Object)를 공통 모듈에 넣는 것은 최악의 안티패턴 중 하나이다. 왜 그럴까? 이는 MSA가 추구하는 핵심 가치를 정면으로 위배하기 때문이다.

  • 느슨한 결합 (Loose Coupling) vs. 강한 결합 (Tight Coupling)
    MSA의 가장 중요한 목표 중 하나는 느슨한 결합이다. 각 서비스가 독립적으로 개발, 테스트, 배포될 수 있으려면 서로에 대해 최대한 영향도가 적어야 한다. 만약 비즈니스 로직이나 데이터 스키마를 공통 모듈로 공유하면, 그 모듈의 작은 변경 하나가 모든 서비스를 마비시키는 강한 결합을 만들어 버린다.

  • 도메인 주도 설계 (Domain-Driven Design, DDD) 위배
    DDD에서는 각 마이크로서비스를 독립적인 경계가 설정된 컨텍스트(Bounded Context)로 본다. 각 컨텍스트는 자신만의 모델과 언어를 가져야 한다. 예를 들어, '주문 컨텍스트'에서의 '사용자'와 '회원 컨텍스트'에서의 '사용자'는 이름만 같을 뿐, 필요한 데이터와 역할이 다르다. 스키마를 공유하는 것은 이 경계를 허물고 여러 서비스가 하나의 모델에 종속되게 만들어, DDD의 원칙을 정면으로 위배한다.

따라서 다음과 같이 명확히 구분하는 것이 좋다.

  • Good Case (공통 모듈에 포함 가능한 것):

    • 날짜/시간 포맷 변환 유틸리티
    • 전사적으로 통일된 에러 코드 Enum
    • 문자열 암호화/복호화 로직
  • Bad Case (포함하면 안 되는 것):

    • DB 테이블과 매핑되는 스키마/Entity 클래스
    • 서비스 간 통신에 사용되는 DTO 클래스

프론트엔드 개발자의 시선: 왜 스키마 공유가 필요한가

이 문제는 특히 GraphQL 환경에서 프론트엔드 개발자에게 더 큰 고통으로 다가온다. 백엔드의 내부 사정 때문에 프론트엔드의 개발이 막히는 경험은 결코 유쾌하지 않다.

그렇다고 스키마 공유 자체를 완전히 부정할 수는 없다. 프론트엔드 개발자 입장에서 보면, 스키마 공유는 분명한 이점이 있다.

  • 네이밍과 구조의 통일: 백엔드와 프론트엔드 간의 필드명이나 데이터 구조에 대한 약속(Contract)은 필수적이다. 이것이 어긋날 때 발생하는 소통 비용과 버그는 무시할 수 없다.

  • 애플리케이션의 유연성: 특히 GraphQL 환경에서는, 모든 데이터의 관계와 흐름을 담은 스키마 철학을 공유하는 것이 더 안정적이고 유연한 애플리케이션을 만드는 데 도움이 된다.

문제는 '어떻게' 공유하느냐이다. 모든 것을 하나의 거대한 모듈이나 중앙 저장소에 묶어두는 방식은 강한 결합을 낳는다. 우리가 원하는 것은 느슨하게 연결된 공유(Loosely Coupled Sharing)이다.

올바른 해법: GraphQL Federation

이 '느슨하게 연결된 공유'를 구현하는 개념이 바로 GraphQL Federation이다.

각 마이크로서비스는 자신만의 GraphQL 스키마를 독립적으로 소유하고 개발한다.

GraphQL Federation은 API 게이트웨이가 각각의 서비스로부터 스키마 조각들을 수집하여, 이를 지능적으로 조합해 프론트엔드에게는 마치 하나의 거대한 스키마처럼 보이게 제공한다.

가장 대표적인 구현체는 Apollo Federation이지만, 유사한 문제를 해결하기 위한 GraphQL Mesh 같은 도구들도 존재한다. 핵심은 '중앙 집중적인 스키마 관리'에서 벗어나 '각 서비스가 자신의 스키마를 책임지는 분산적인 관리'로 전환하는 것이다. 이 방식을 통해 각 서비스와 프론트엔드는 진정한 독립성을 되찾을 수 있다.

CI/CD와 배포 전략: 자동화의 명과 암

블랙박스가 된 CI/CD 파이프라인

많은 조직에서는 모든 개발자가 CI/CD의 복잡성을 알 필요는 없다고 생각한다. 이로 인해 CI/CD 파이프라인을 표준화하고 자동화하는 역할은 소수의 전문가나 특정 팀(플랫폼팀, DevOps팀 등)에게 집중되는 경우가 많다. 실제 우리 회사의 경우, "CI/CD를 주입해주는 repo"를 통해 이 과정을 관리하고 있다.

이러한 접근은 개발자가 인프라에 신경 쓰지 않고 비즈니스 로직에만 집중하게 해 생산성을 높인다는 플랫폼 엔지니어링 관점의 장점이 있다. 하지만 여기에는 명확한 단점이 존재한다.

  • 장애 대응의 어려움: 배포 과정에서 문제가 발생하면, 개발자는 원인을 파악하기 어렵다. "제 코드는 문제없는데, 파이프라인에서 에러가 나요"라며 플랫폼팀에 문의하고 기다려야만 한다.

  • 개발자의 시야 제한: 자신의 코드가 어떤 과정을 거쳐 사용자에게 전달되는지 모르는 개발자는 장기적으로 시스템 전체를 보는 아키텍트 역량을 키우기 어렵다.

이상적인 방향은, CI/CD를 플랫폼으로 제공하여 편의성을 높이되, 개발자들이 원할 때 언제든지 그 내부 동작을 학습하고 이해할 수 있도록 문서화와 교육을 병행하는 것이라고 생각한다.

배포 전략: 롤링, 블루/그린, 그리고 카나리

안정적인 서비스를 위해서는 배포 전략을 신중하게 선택해야 한다. 각 전략은 장단점이 명확하다.

  • 롤링 배포 (Rolling Update)

    • 방식: 구버전 서버를 한 대씩 신버전으로 점진적으로 교체하는 방식. 가장 일반적이다.
    • 한계: 배포 과정에서 일정 시간 동안 구버전(v1)과 신버전(v2)의 서버가 공존한다. 만약 DB 스키마나 API 명세가 변경된 배포라면, 버전 차이로 인해 일시적인 오류가 발생할 수 있다. 따라서 완벽한 '무중단' 배포는 아니다.
  • 블루/그린 배포 (Blue-Green Deployment)

    • 방식: 현재 사용 중인 환경(블루)과 완전히 똑같은 유휴 환경(그린)을 준비하고, 신규 버전을 그린 환경에 미리 완벽하게 배포한다. 테스트가 끝나면 로드 밸런서가 트래픽을 블루에서 그린으로 한 번에 돌려버린다.
    • 장점: 버전 공존 문제가 없어 매우 안정적이다. 문제가 생기면 즉시 블루로 롤백할 수 있다.
    • 단점: 동일한 환경을 2세트 유지해야 하므로 비용이 더 든다.
  • 카나리 배포 (Canary Deployment)

    • 방식: 과거 광부들이 유독가스를 감지하기 위해 카나리아 새를 데리고 들어간 것에서 유래했다. 신규 버전을 전체 서버가 아닌, 아주 일부 서버(예: 1~5%)에만 먼저 배포한다.
    • 동작: 카나리 버전의 서버에 소량의 실제 트래픽을 보내보고, 에러율, 응답 시간 등 주요 지표를 모니터링한다. 문제가 없다고 판단되면 점진적으로 배포를 전체 서버로 확대한다.
    • 장점: 실제 사용자 트래픽으로 신규 버전의 안정성을 미리 테스트해볼 수 있어 가장 안전한 방식이다. 장애가 발생해도 피해를 최소화할 수 있다.
    • 단점: 라우팅, 모니터링 등 기술적으로 가장 복잡하고 관리하기 어렵다.
    • 확장: 카나리 배포는 특정 사용자 그룹(예: 내부 직원, 베타 테스터)에게만 신규 기능을 노출하는 기능 플래그(Feature Flag)와 결합될 때 더욱 강력해진다. 이를 통해 위험을 통제하며 점진적으로 새로운 기능을 릴리스할 수 있다.
전략전환 방식리스크비용/복잡도롤백
롤링 배포점진적 교체중간낮음복잡함
블루/그린 배포즉시 전환낮음높음간단함
카나리 배포점진적 확대매우 낮음매우 높음간단함

마치며

결국 MSA의 모든 기술적 과제는 하나의 질문으로 수렴한다고 생각한다.
"어떻게 하면 각 서비스의 '독립성'을 최대한 보장하면서도, 전체 시스템의 '일관성'이라는 목표를 달성할 것인가?"
이 두 가치 사이의 줄다리기가 바로 MSA 운영의 핵심일 것이다. 레포지토리 관리, 공통 코드, CI/CD 전략 모두 이 질문에 대한 답을 찾아가는 과정이다.

📚 참고 링크