규율 있는 마이크로서비스 구축: 'Micro'의 함정을 넘어서 - 목차

[ microservice  ]

규율 있는 마이크로서비스 구축: 'Micro'의 함정을 넘어서

서론: 마이크로서비스라는 오해의 단어

지난 10여 년간 “마이크로서비스(Microservices)”는 소프트웨어 아키텍처의 정답처럼 여겨져 왔습니다. 하지만 그 용어 자체가 가져온 거대한 오해가 있습니다. 바로 “무조건 작게 나누는 것이 좋다”는 착각입니다.

마이크로서비스가 추구하는 핵심 가치는 단일 서버의 성능을 높이는 스케일 업(Scale Up)으로 감당하기 어려운 거대한 트래픽을, 수평적으로 확장하는 스케일 아웃(Scale Out)을 통해 처리하는 것입니다. 하지만 대부분의 비즈니스 초기 단계에서는 스케일 업만으로도 충분히 대응 가능하며, 섣부른 스케일 아웃 도입은 복잡도라는 이자만 키울 뿐입니다.

많은 팀들이 서비스의 경계(Bounded Context)를 명확히 정의하기도 전에 물리적인 서버부터 분리하기 시작했습니다. 그 결과는 참담했습니다. “분산된 거대 진흙 덩어리(Distributed Big Ball of Mud)”가 탄생했고, 함수 호출(Function Call)로 1밀리초면 끝날 일이 네트워크를 타고 100밀리초가 걸리는 RPC가 되었습니다. 트랜잭션 관리는 악몽이 되었고, 디버깅을 위해 10개의 터미널 창을 띄워야 했습니다.

우리는 마이크로서비스가 해결하려던 진짜 문제가 무엇인지, 그리고 모놀리스(Monolith)가 왜 여전히 강력한지 다시 살펴봐야 합니다.

모놀리스(Monolith)가 추구하는 가치

“모놀리스”는 낡고 나쁜 것이 아닙니다. 잘 설계된 모듈러 모놀리스(Modular Monolith)는 초기 스타트업이나 중소 규모 팀에게 축복과도 같습니다.

  1. 단순성(Simplicity): 배포 파이프라인이 하나입니다. 로컬 테스트가 쉽습니다.
  2. 응집력(Cohesion): 관련된 코드가 한곳에 모여 있어 리팩토링이 자유롭습니다. 코드를 이동시키는 비용이 ‘0’에 가깝습니다.
  3. 데이터 일관성(Consistency): ACID 트랜잭션을 사용하여 데이터 정합성을 쉽게 보장할 수 있습니다.
  4. 제로 레이턴시(Zero Latency): 컴포넌트 간 통신에 네트워크 비용이 들지 않습니다.

마이크로서비스 도입의 가장 큰 실패는 이 모놀리스의 장점을 “확장성(Scalability)”이라는 막연한 미래를 위해 너무 일찍 포기했을 때 발생합니다.

새로운 서비스를 구축할 때의 접근법: 규율(Discipline)

새로운 시스템을 구축할 때 우리는 “물리적 분리”가 아닌 “논리적 격리”를 먼저 고민해야 합니다. 이것이 바로 규율 있는 마이크로서비스 구축의 핵심입니다.

1. 인터페이스 우선 (Interface First)

서비스를 물리적으로 나누기 전에, 인터페이스로 먼저 나눕니다. 코드 내부에서도 서로 다른 도메인(예를 들면 Auth, Payment, Store 등)은 엄격하게 정의된 인터페이스(Protobuf, gRPC)를 통해서만 소통해야 합니다. 구현체가 옆 파일에 있더라도, 마치 원격에 있는 것처럼 인터페이스를 통해 호출하십시오. 이렇게 하면 나중에 물리적으로 떼어낼 때 코드 수정이 거의 필요 없습니다.

2. 전략적 동거 (Strategic Co-location)

모든 컴포넌트를 분리할 필요는 없습니다. 특히 인증(Auth)과 같이 모든 요청에 관여하는 서비스는 게이트웨이(Gateway)와 함께 있는 것이 성능상 훨씬 유리합니다. “마이크로서비스니까 무조건 분리해야 해”라는 도그마 대신, “네트워크 홉(Hop)을 줄이는 것이 이득인가?”라는 실용적인 질문을 던져야 합니다.

성능 최적화는 단계적인 캐시(Multi-level Caching) 전략에서 극대화됩니다.

  1. 1차 캐시 (L1): 서비스 내부 메모리에 존재하는 가장 빠른 캐시. (마이크로초 단위)
  2. 2차 캐시 (L2): Redis 등을 이용한 분산 캐시. (밀리초 단위)
  3. 데이터 원본 (Source): 데이터베이스에 저장된 원본 데이터. (가장 느림, I/O 발생)

가장 빈번하게 호출되는 인증(Auth)과 같은 컴포넌트가 게이트웨이 내부에 존재하면(Co-location), 네트워크를 타지 않고 L1 캐시를 즉시 참조할 수 있어 성능이 비약적으로 향상됩니다. 이것이 우리가 불필요한 네트워크 분리를 경계해야 하는 이유입니다.

💡 [참고] Latency Numbers Every Programmer Should Know (by Jeff Dean)

작업 (Operation) 소요 시간 (Latency) 비유
L1 캐시 참조 0.5 ns 심장 박동 1번
메인 메모리 참조 100 ns 눈 깜빡임
네트워크(동일 데이터센터) 왕복 500,000 ns (0.5 ms) 마라톤 완주
네트워크(국가 간) 왕복 150,000,000 ns (150 ms) 지구 반 바퀴 여행

메모리 참조(함수 호출)와 네트워크 호출은 약 5,000배 이상의 성능 차이가 납니다.

3. 모놀리스로 시작하여 필요할 때 분화(Fork)하라

처음에는 ./app start와 같은 단일 명령어로 모든 것이 실행되게 하십시오. 팀이 커지고 배포 주기가 서로 달라지거나, 특정 서비스(예: AI 처리 로직)만 막대한 GPU 자원을 필요로 할 때, 그때 비로소 해당 모듈만 떼어내어(Micro mode) 별도 프로세스로 실행하십시오. 준비된 인터페이스 덕분에 이는 설정값 하나(mode="remote")를 바꾸는 일에 불과할 것입니다.

또한, 개발 환경과 운영 환경, 모놀리스 모드와 마이크로 모드에 관계없이 동일하게 빌드된 단일 실행 파일(Single Binary)을 사용하는 것이 바람직합니다. 이는 배포 복잡도를 낮추고 ‘내 컴퓨터에서는 되는데…‘라는 문제를 원천적으로 차단합니다. 따라서 별도의 실행 파일로 분리하는 것을 최대한 늦추고, 일단은 단일 실행 파일로 시작하십시오.

4. 기술 스택의 통일성 (Uniformity)

마이크로서비스의 장점으로 흔히 ‘폴리글랏(Polyglot) 프로그래밍’을 꼽지만, 현실에서는 대부분의 경우 전체 시스템이 하나의 주력 언어와 계층별 단일 솔루션을 유지하며, 하나의 스택 위에서 동작하는 비즈니스 로직을 구현하는 것이 훨씬 바람직합니다.

언어가 통일되어 있어야 모놀리스 내부에서 모듈 간 코드를 이동시키거나 리팩토링하기 쉽습니다. 만약 서비스마다 언어가 제각각이라면, 로직을 다른 서비스로 옮길 때마다 코드를 처음부터 다시 짜야 합니다. 결국 이 ‘통일성’이 유지되어야만 모듈러 모놀리스에서 마이크로서비스로 점진적으로 확장해 나가는 전략이 유효해집니다.

주력 언어를 선택할 때는 다음 기준을 고려해야 합니다.

  1. 생산성: 빌드와 실행 속도가 빠르고, 러닝 커브가 낮아 새로운 엔지니어도 쉽게 익힐 수 있어 빠른 반복 개발(Iteration)이 가능해야 합니다.
  2. 조직 적합성과 지속 가능성: 개발팀 전체가 동의하고, 회사 내외부에서도 널리 사용되어 인력 풀이 풍부해야 합니다.

5. 데이터베이스 전략: 상태(State)의 무게

애플리케이션의 비즈니스 로직은 대부분 상태가 없는(Stateless) 반면, 데이터베이스는 상태(State)를 영구적으로 저장해야 하므로 설계 초기부터 훨씬 신중한 검토가 필요합니다.

  1. 단일 솔루션 사용: 애플리케이션 언어와 마찬가지로, 데이터베이스 역시 단일 솔루션을 프로젝트 전체적으로 사용하는 것이 운영 효율성 측면에서 유리합니다.
  2. 논리적 격리 (1:1 매핑): 물리적으로는 하나의 DB 인스턴스를 공유하더라도, 논리적으로는 각 서비스(도메인)마다 별도의 스키마(Schema)나 데이터베이스를 할당하여 1 서비스 : 1 데이터베이스 원칙을 지켜야 합니다. 그래야 나중에 서비스를 물리적으로 분리할 때 데이터베이스도 함께 떼어낼 수 있습니다.
  3. 데이터 접근 제한: 다른 서비스의 데이터베이스 테이블에 직접 접근(특히 쓰기 작업)하는 것은 금지해야 합니다. 반드시 해당 서비스의 API를 통해 접근해야 하며, 성능상 불가피한 경우에 한해 ‘읽기 전용(Read-Only)’ 접근만 제한적으로 허용해야 합니다.

결론

마이크로서비스는 목적지가 아니라 여정(Journey)입니다. 처음부터 파편화된 서비스로 시작하는 것은 마치 건물을 짓기도 전에 방과 거실을 쪼개어 다른 동네에 두는 것과 같습니다.

우리는 “규율 있는 모놀리스”로 시작해야 합니다. 도메인의 경계를 명확히 하고, 인터페이스를 준수하며, 데이터의 소유권을 존중하는 것. 이 규율이 지켜진다면, 모놀리스든 마이크로서비스든 그것은 단지 배포의 형태(Deployment Artifact)일 뿐입니다.

소프트웨어 아키텍처의 목표는 ‘마이크로서비스를 하는 것’이 아니라, ‘비즈니스의 변화 속도를 기술이 막지 않게 하는 것’임을 기억해야 합니다.

참고 문헌 (References)

더 깊이 있는 이해를 위해 다음 자료들을 일독하시길 권합니다.

  1. MonolithFirst (Martin Fowler): 마이크로서비스로 시작하지 말고 모놀리스로 시작하여 필요할 때 분리하라는 전략.
  2. Modular Monoliths (Simon Brown): 모놀리스 내부에서 ‘모듈성’을 지키는 아키텍처 설계 방법론 (YouTube).
  3. Modular Monolith Architecture (Milan Jovanović): 모듈러 모놀리스의 개념과 장점, 그리고 마이크로서비스로의 전환 전략을 다룬 아티클.
  4. Deconstructing the Monolith (Shopify Engineering): 거대 모놀리스를 ‘모듈러 모놀리스’로 리팩토링한 Shopify의 실제 사례.
  5. Modular Monolith with DDD (Kamil Grzybek): 도메인 주도 설계(DDD)를 적용하여 모듈러 모놀리스를 구현하는 구체적인 패턴과 코드 예시.