지난 몇 년간 서비스 지향 아키텍처와 마이크로서비스 아키텍처는 유행을 넘어 신앙에 가까워졌다. 시스템을 작은 서비스들로 잘게 쪼개면 결합이 풀리고, 팀마다 독립적으로 개발하며, 서로를 방해하지 않고 따로 배포할 수 있다고들 말한다. 마틴은 이 장에서 그 믿음에 브레이크를 건다. 서비스로 나누는 것 자체는 좋은 일일 수 있다. 그러나 서비스라는 사실만으로는 아키텍처의 이점 가운데 어느 것도 자동으로 따라오지 않는다. 서비스는 아키텍처가 아니다. 서비스는 그저 프로세스를 가르는 하나의 방식일 뿐이다.
이 장의 질문은 날카롭다 — 시스템을 서비스로 나누면 정말로 결합이 풀리는가, 정말로 독립적으로 개발하고 배포할 수 있는가. 마틴의 답은 “경우에 따라, 그리고 대개는 당신이 믿는 만큼은 아니다”이다.
서비스가 판다고 광고하는 세 가지
마이크로서비스의 세일즈 문구는 대개 세 가지 이점으로 요약된다. 결합 분리, 독립 개발, 독립 배포다. 서비스들이 오직 네트워크 호출로만 대화하므로 서로의 소스 코드를 공유하지 않고, 따라서 결합이 끊긴다. 팀이 서비스별로 나뉘므로 서로 간섭 없이 개발한다. 서비스가 각자 배포 단위이므로 하나를 고쳐도 나머지는 건드리지 않고 올릴 수 있다. 세 가지 다 매력적이다. 문제는 이 세 문장이 주장이지 보장이 아니라는 것이다.
마틴은 이 세 이점 각각이 “일부만 참”이라고 못박는다. 결합은 완전히 끊기지 않는다. 개발은 생각만큼 독립적이지 않다. 배포도 종종 함께 묶여 나간다. 이유는 하나로 모인다 — 서비스들은 여전히 데이터를 공유하고, 행위를 공유하기 때문이다. 프로세스를 갈랐다고 해서 그들이 다루는 업무가 갈라진 것은 아니다.
판단 기준: “서비스로 쪼갰으니 결합이 풀렸다”고 말하기 전에, 그 서비스들이 같은 데이터 레코드를 공유하는지, 같은 업무 개념 하나가 바뀔 때 여러 서비스가 함께 바뀌어야 하는지를 먼저 세어 보라. 함께 바뀌는 것들은 물리적으로 떨어져 있어도 결합돼 있다. 함정: 네트워크 경계를 아키텍처 경계로 착각하는 것이다. 호출이 함수 콜에서 REST로 바뀌었을 뿐, 아는 것이 줄지 않았다면 결합은 그대로 남아 오히려 더 잘 안 보이는 곳으로 숨는다.
결합 분리라는 오류
서비스가 결합을 끊는다는 믿음부터 해부하자. 표면적으로 서비스들은 각자의 프로세스, 각자의 주소 공간에서 돈다. 한쪽 변수를 다른 쪽이 직접 만질 수 없으니 결합이 끊긴 것처럼 보인다. 그러나 결합은 변수 수준에서만 생기는 게 아니다. 두 서비스가 같은 데이터의 스키마를 공유하는 순간, 그 데이터 구조에 필드 하나가 추가되거나 의미가 바뀌면 그것을 읽고 쓰는 모든 서비스가 함께 흔들린다. 프로세스는 갈라졌지만 데이터는 여전히 그들을 하나로 꿰고 있다.
행위의 결합도 마찬가지다. 하나의 업무 흐름이 여러 서비스를 순서대로 거쳐 완성된다면, 그 흐름을 바꾸는 변경은 참여하는 서비스들을 줄줄이 건드린다. 마틴의 표현으로, 이런 서비스들은 데이터나 행위를 공유하는 탓에 결합돼 있으며, 그 결합은 소스 코드 수준의 결합과 본질이 다르지 않다. 다만 네트워크라는 안개 뒤에 가려져 눈에 덜 띌 뿐이다.
판단 기준: 서비스 A의 데이터 형태가 바뀔 때 B·C가 재배포돼야 한다면 A·B·C는 한 덩어리다 — 배포 단위가 셋이어도 변경 단위는 하나다. 함정: 결합을 “코드를 공유하느냐”로만 재면 서비스는 늘 결합이 없어 보인다. 결합의 진짜 척도는 “무엇이 함께 바뀌느냐”이며, 이 잣대로 보면 서비스 경계가 결합을 끊었다는 증거는 생각보다 드물다.
독립 개발과 배포라는 오류
두 번째와 세 번째 이점 — 독립 개발과 독립 배포 — 도 같은 결로 무너진다. 작고 단순한 시스템에서는 팀들이 정말로 서비스별로 나뉘어 독립적으로 굴러간다. 그러나 시스템이 커지고 서비스들이 데이터와 행위를 얽어 가기 시작하면, 한 팀의 변경이 다른 팀의 서비스 인터페이스를 건드리게 되고, 조율 회의가 늘고, 릴리스는 서로의 눈치를 보며 함께 묶여 나간다. 규모가 커질수록 독립성은 옅어진다.
마틴은 이것이 서비스만의 문제가 아니라 결합 분리 일반의 문제임을 상기시킨다. 소스 수준으로 분리한 컴포넌트든, 배포 수준으로 분리한 jar든, 서비스 수준으로 분리한 프로세스든 — 분리의 물리적 형태가 무엇이든, 데이터와 행위를 공유하면 그 공유가 만든 결합은 형태를 뚫고 남는다. 서비스는 결합 분리의 한 가지 선택지일 뿐, 결합 분리 그 자체가 아니다.
판단 기준: 독립 개발·배포를 원한다면 서비스로 쪼갤 게 아니라 공유되는 데이터와 행위를 먼저 갈라야 한다. 경계를 나눌 축은 프로세스가 아니라 “함께 바뀌는 것”이다. 함정: 조직도를 아키텍처로 착각하는 것 — 팀을 서비스별로 나눴다고 코드가 그 경계대로 갈라지지는 않는다. 콘웨이의 법칙은 조직 구조를 코드에 새기지만, 그 새김이 좋은 경계라는 보장은 어디에도 없다.
택시 집계 시스템 — 깔끔해 보이던 서비스들
마틴은 추상론을 하나의 구체적인 장면으로 착지시킨다. 도시의 택시 배차를 총괄하는 집계(aggregator) 시스템을 상상하자. 사용자는 여러 택시 회사를 가리지 않고 한 곳에서 택시를 부른다. 시스템은 위치·목적지·픽업 시간·차종·서비스 등급 같은 조건을 따져 가장 알맞은 택시를 골라 배차한다.
이 시스템을 마이크로서비스로 설계하면 그림이 아주 깔끔하게 나온다. 사용자 인터페이스를 받는 TaxiUI 서비스, 택시 회사들의 위치와 상태를 관리하는 TaxiSupplier 서비스, 요금을 계산하는 TaxiBilling 서비스, 손님과 기사를 실제로 이어 주는 TaxiDispatch 서비스. 각 서비스는 자기 관심사만 책임지고, 서로 REST로 대화한다. 다이어그램 위에서 이보다 단정할 수 없다. 결합이 끊긴 것처럼, 팀이 나뉜 것처럼, 따로 배포되는 것처럼 보인다.
그런데 이 단정함은 요구가 하나도 바뀌지 않는 동안에만 유지된다.
새 기능 하나가 경계를 가로지른다
이제 마케팅 부서가 새 기능을 요구한다. 고양이를 좋아하는 손님을 위한 “키티(Kitties)” 프로모션 — 특정 회사의 택시에는 귀여운 고양이 굿즈를 비치하고, 이 프로모션에 참여한 손님에게는 그 택시를 우선 배차하며, 요금을 할인하고, 별도의 배차 우선순위를 준다. 사소해 보이는 마케팅 기능 하나다.
이 기능이 어디에 손을 대는지 따라가 보자. 손님이 키티 프로모션을 선택할 수 있어야 하니 TaxiUI가 바뀐다. 어떤 택시가 키티에 참여했는지 알아야 하니 TaxiSupplier가 바뀐다. 할인을 적용해야 하니 TaxiBilling이 바뀐다. 키티 손님에게 키티 택시를 먼저 붙여야 하니 배차 로직인 TaxiDispatch가 바뀐다. 네 서비스가 전부 바뀐다. 깔끔하게 갈라져 있던 서비스들이, 프로모션이라는 단 하나의 횡단 관심사(cross-cutting concern) 앞에서 일제히 함께 열려야 하는 하나의 덩어리로 정체를 드러낸다.
여기서 마틴의 결론이 선명해진다. 서비스 경계는 이 프로모션 기능이 가로지르는 진짜 변경의 축과 일치하지 않았다. 서비스들은 “택시를 공급한다”, “요금을 매긴다” 같은 명사적 관심사로 갈라졌지만, 실제 변경은 “키티 프로모션”처럼 그 명사들을 가로질러 흐른다. 경계가 변경의 축 위에 놓이지 않으면, 새 기능은 매번 여러 서비스를 동시에 찢으며 지나간다. 서비스 다이어그램은 결합을 감췄을 뿐 없애지 못했다.
판단 기준: 서비스 분할이 좋은지 판단하려면 “앞으로 올 변경들이 이 경계를 가로지르는가, 아니면 경계 안에 갇히는가”를 그려 보라. 횡단 관심사가 여러 서비스를 매번 함께 여는 구조라면 그 경계는 잘못 그어졌다. 함정: 명사(공급자·요금·배차)를 기준으로 서비스를 나누면 다이어그램은 예뻐지지만, 정작 변경은 동사(프로모션·정산 정책·등급 개편)를 따라 오므로 경계마다 새 기능이 관통한다.
진짜 경계는 서비스 안에 있다
그렇다면 키티 문제를 서비스로는 풀 수 없는가. 마틴의 처방은 서비스를 없애라는 것이 아니라, 서비스 내부에 진짜 아키텍처 경계를 세우라는 것이다. 각 서비스를 통짜 코드 덩어리로 두지 말고, 그 안을 컴포넌트로 나누되 22장의 의존성 규칙을 그대로 적용한다. 즉 서비스 안에 업무 규칙을 담은 안쪽 컴포넌트를 두고, 새로 추가되는 정책(키티 같은 것)은 그 컴포넌트에 플러그인처럼 꽂히는 파생 컴포넌트로 격리한다.
이렇게 하면 키티 프로모션은 각 서비스의 핵심 로직을 뜯어고치는 변경이 아니라, 각 서비스에 새 파생 컴포넌트를 더하는 개방-폐쇄적 확장이 된다. 기존 컴포넌트는 키티의 존재를 모른 채 그대로 있고, 새 정책은 의존성 규칙을 지키며 바깥에서 안쪽을 향해 꽂힌다. 서비스는 그대로 넷이지만, 변경의 파급은 각 서비스의 핵심을 건드리지 않고 새 컴포넌트 안에 갇힌다.
핵심은 이것이다 — 아키텍처 경계를 만드는 것은 서비스가 아니라, 서비스 안에서 지켜지는 컴포넌트 의존성 규칙이다. 서비스로 나눴는지 여부는 배포와 운영의 문제이지 아키텍처의 문제가 아니다. 좋은 경계는 프로세스를 가르는 선이 아니라, 의존성이 오직 안쪽 고수준 정책을 향하도록 강제하는 선에서 나온다.
판단 기준: 서비스든 모놀리스든, 새 정책을 “기존 코드를 고쳐서” 넣어야 하면 경계가 없는 것이고, “새 컴포넌트를 꽂아서” 넣을 수 있으면 경계가 있는 것이다 — 경계의 유무는 배포 형태가 아니라 변경의 반응에서 드러난다. 함정: 마이크로서비스로 이전하면 아키텍처가 개선된다는 기대. 내부에 의존성 규칙이 없는 서비스는 그냥 네트워크 너머로 흩어진 진흙 덩어리이며, 오히려 디버깅과 추적만 더 어려워진다.
서비스는 세부사항이다
마틴이 이 장에서 도달하는 지점은 이 책 전체의 어법과 정확히 포개진다. 데이터베이스가 세부사항이고 웹이 세부사항이듯, 서비스로 나눌 것인가 아닌가도 세부사항이다. 시스템을 하나의 프로세스로 둘지, 여러 서비스로 쪼갤지는 배포·운영·확장성의 요구에 따라 나중에 결정하고 또 번복할 수 있는 문제여야 한다. 아키텍처의 심장인 업무 규칙과 그들 사이의 의존성 방향은 그 결정과 무관하게 지켜져야 한다.
그러니 순서를 뒤집지 말아야 한다. 서비스로 먼저 쪼갠 뒤 그 안에 아키텍처를 끼워 넣으려 하면 늦다. 먼저 컴포넌트와 의존성 규칙으로 아키텍처 경계를 세우고, 그 경계 가운데 어떤 것을 서비스라는 물리적 분리로 승격시킬지는 운영상의 필요가 요구할 때 고르는 것이다. 이렇게 하면 서비스로 나누든 합치든 아키텍처는 그대로 살아남는다.
- 마이크로서비스로 쪼갠다는 사실 하나로는 결합 분리·독립 개발·독립 배포 중 무엇도 자동으로 얻지 못한다. 세 이점은 보장이 아니라 주장이다.
- 프로세스를 갈라도 데이터와 행위를 공유하면 결합은 네트워크 뒤에 숨은 채 그대로 남는다 — 결합의 척도는 코드 공유가 아니라 “무엇이 함께 바뀌느냐”다.
- 택시 집계에서 키티 프로모션이라는 횡단 관심사 하나가 네 서비스를 동시에 찢으며, 서비스 경계가 진짜 변경의 축과 어긋나 있었음을 폭로한다.
- 진짜 아키텍처 경계는 서비스 사이가 아니라 서비스 내부의 컴포넌트 의존성 규칙에서 온다. 새 정책을 플러그인처럼 꽂을 수 있으면 경계가 있는 것이다.
- 서비스로 나눌지 여부는 배포·운영의 세부사항이다. 아키텍처를 먼저 세우고, 서비스 분리는 필요가 요구할 때 나중에 고른다.
서비스는 시스템을 물리적으로 흩어 놓는 강력한 도구다. 그러나 흩어 놓는 것과 갈라 놓는 것은 다르다. 물리적으로 아무리 멀리 떨어뜨려도, 함께 바뀌는 것들은 여전히 한 몸이다. 다음 장은 시스템의 가장 바깥에 있으면서도 자주 잊히는 또 하나의 경계 — 테스트 — 로 이 논의를 이어 간다.
다음장으로 28장