규율 있는 마이크로서비스 구축: 'Micro'의 함정을 넘어서 - 목차
지난 10여 년간 “마이크로서비스(Microservices)”는 소프트웨어 아키텍처의 정답처럼 여겨져 왔습니다. 하지만 그 용어 자체가 가져온 거대한 오해가 있습니다. 바로 “무조건 작게 나누는 것이 좋다”는 착각입니다.
마이크로서비스가 추구하는 핵심 가치는 단일 서버의 성능을 높이는 스케일 업(Scale Up)으로 감당하기 어려운 거대한 트래픽을, 수평적으로 확장하는 스케일 아웃(Scale Out)을 통해 처리하는 것입니다. 하지만 대부분의 비즈니스 초기 단계에서는 스케일 업만으로도 충분히 대응 가능하며, 섣부른 스케일 아웃 도입은 복잡도라는 이자만 키울 뿐입니다.
많은 팀들이 서비스의 경계(Bounded Context)를 명확히 정의하기도 전에 물리적인 서버부터 분리하기 시작했습니다. 그 결과는 참담했습니다. “분산된 거대 진흙 덩어리(Distributed Big Ball of Mud)”가 탄생했고, 함수 호출(Function Call)로 1밀리초면 끝날 일이 네트워크를 타고 100밀리초가 걸리는 RPC가 되었습니다. 트랜잭션 관리는 악몽이 되었고, 디버깅을 위해 10개의 터미널 창을 띄워야 했습니다.
우리는 마이크로서비스가 해결하려던 진짜 문제가 무엇인지, 그리고 모놀리스(Monolith)가 왜 여전히 강력한지 다시 살펴봐야 합니다.
“모놀리스”는 낡고 나쁜 것이 아닙니다. 잘 설계된 모듈러 모놀리스(Modular Monolith)는 초기 스타트업이나 중소 규모 팀에게 축복과도 같습니다.
마이크로서비스 도입의 가장 큰 실패는 이 모놀리스의 장점을 “확장성(Scalability)”이라는 막연한 미래를 위해 너무 일찍 포기했을 때 발생합니다.
새로운 시스템을 구축할 때 우리는 “물리적 분리”가 아닌 “논리적 격리”를 먼저 고민해야 합니다. 이것이 바로 규율 있는 마이크로서비스 구축의 핵심입니다.
서비스를 물리적으로 나누기 전에, 인터페이스로 먼저 나눕니다. 코드 내부에서도 서로 다른 도메인(예를 들면 Auth, Payment, Store 등)은 엄격하게 정의된 인터페이스(Protobuf, gRPC)를 통해서만 소통해야 합니다. 구현체가 옆 파일에 있더라도, 마치 원격에 있는 것처럼 인터페이스를 통해 호출하십시오. 이렇게 하면 나중에 물리적으로 떼어낼 때 코드 수정이 거의 필요 없습니다.
모든 컴포넌트를 분리할 필요는 없습니다. 특히 인증(Auth)과 같이 모든 요청에 관여하는 서비스는 게이트웨이(Gateway)와 함께 있는 것이 성능상 훨씬 유리합니다. “마이크로서비스니까 무조건 분리해야 해”라는 도그마 대신, “네트워크 홉(Hop)을 줄이는 것이 이득인가?”라는 실용적인 질문을 던져야 합니다.
성능 최적화는 단계적인 캐시(Multi-level Caching) 전략에서 극대화됩니다.
가장 빈번하게 호출되는 인증(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배 이상의 성능 차이가 납니다.
처음에는 ./app start와 같은 단일 명령어로 모든 것이 실행되게 하십시오.
팀이 커지고 배포 주기가 서로 달라지거나, 특정 서비스(예: AI 처리 로직)만 막대한 GPU 자원을 필요로 할 때, 그때 비로소 해당 모듈만 떼어내어(Micro mode) 별도 프로세스로 실행하십시오.
준비된 인터페이스 덕분에 이는 설정값 하나(mode="remote")를 바꾸는 일에 불과할 것입니다.
또한, 개발 환경과 운영 환경, 모놀리스 모드와 마이크로 모드에 관계없이 동일하게 빌드된 단일 실행 파일(Single Binary)을 사용하는 것이 바람직합니다. 이는 배포 복잡도를 낮추고 ‘내 컴퓨터에서는 되는데…‘라는 문제를 원천적으로 차단합니다. 따라서 별도의 실행 파일로 분리하는 것을 최대한 늦추고, 일단은 단일 실행 파일로 시작하십시오.
마이크로서비스의 장점으로 흔히 ‘폴리글랏(Polyglot) 프로그래밍’을 꼽지만, 현실에서는 대부분의 경우 전체 시스템이 하나의 주력 언어와 계층별 단일 솔루션을 유지하며, 하나의 스택 위에서 동작하는 비즈니스 로직을 구현하는 것이 훨씬 바람직합니다.
언어가 통일되어 있어야 모놀리스 내부에서 모듈 간 코드를 이동시키거나 리팩토링하기 쉽습니다. 만약 서비스마다 언어가 제각각이라면, 로직을 다른 서비스로 옮길 때마다 코드를 처음부터 다시 짜야 합니다. 결국 이 ‘통일성’이 유지되어야만 모듈러 모놀리스에서 마이크로서비스로 점진적으로 확장해 나가는 전략이 유효해집니다.
주력 언어를 선택할 때는 다음 기준을 고려해야 합니다.
애플리케이션의 비즈니스 로직은 대부분 상태가 없는(Stateless) 반면, 데이터베이스는 상태(State)를 영구적으로 저장해야 하므로 설계 초기부터 훨씬 신중한 검토가 필요합니다.
마이크로서비스는 목적지가 아니라 여정(Journey)입니다. 처음부터 파편화된 서비스로 시작하는 것은 마치 건물을 짓기도 전에 방과 거실을 쪼개어 다른 동네에 두는 것과 같습니다.
우리는 “규율 있는 모놀리스”로 시작해야 합니다. 도메인의 경계를 명확히 하고, 인터페이스를 준수하며, 데이터의 소유권을 존중하는 것. 이 규율이 지켜진다면, 모놀리스든 마이크로서비스든 그것은 단지 배포의 형태(Deployment Artifact)일 뿐입니다.
소프트웨어 아키텍처의 목표는 ‘마이크로서비스를 하는 것’이 아니라, ‘비즈니스의 변화 속도를 기술이 막지 않게 하는 것’임을 기억해야 합니다.
더 깊이 있는 이해를 위해 다음 자료들을 일독하시길 권합니다.