[코드가 환경을 모르는 구조 5/7] Rewrite Host — 공간 축을 교체한다

MSA가 로컬 개발을 어떻게 바꿨는가

한 줄만 고쳐도 조직 전체의 인프라가 켜져야 하는 순간, 로컬 개발은 무너지기 시작합니다.

모놀리식 시절의 로컬 개발은 단순했습니다. 노트북에서 서버 하나를 띄우면 급여도 연차도 인증도 같은 JVM 안에 들어 있었고, 급여 모듈의 한 줄을 고쳐도 연차 모듈이 그 변경을 보는 데 아무 장치가 필요 없었습니다. 서버 하나를 띄우는 행위가 곧 전체 시스템을 띄우는 일과 같았고, 브레이크포인트 하나로 어떤 경로든 따라갈 수 있었습니다.

MSA로 전환하면 이 경험이 통째로 사라집니다. 급여 서비스 한 줄을 고쳐 테스트하려면 앞에는 Gateway, 옆에는 인증 서버, 뒤에는 연차·퇴직금 같은 이웃 도메인과 MySQL·Redis·Kafka까지 전부 떠 있어야 합니다. 변경한 건 한 줄인데, 그 한 줄을 돌리는 데 필요한 환경은 조직 전체의 인프라로 부풀어 오릅니다.

두 선택지 모두 현실적이지 않습니다. 전부 로컬에 띄우면 노트북 자원과 개발자의 인내심이 먼저 바닥나고, 매번 dev 클러스터에 배포해 확인하면 Git push부터 ArgoCD 동기화까지 몇 분짜리 루프가 모든 이터레이션을 가로막습니다. 이 비용을 계속 치르다 보면 "그냥 안 고치고 넘어가자"는 유혹이 커집니다.

MSA를 경험한 엔지니어라면 누구나 한 번쯤 이 좌절을 겪습니다. 이터레이션 속도가 두 벽 사이에서 짓눌린다는 감각. flex 팀은 이 고통을 익숙하게 알고 있었기에, 로컬 개발 경험을 어떻게 지킬지 코드의 첫 줄이 쓰이기 전부터 고민했습니다. 나중에 불편하면 뭐라도 붙이겠다는 태도로는 풀리지 않는, 처음부터 설계의 1급 시민으로 다뤄야 풀리는 문제였기 때문입니다.

그 결과물이 이 글의 주제인 Rewrite Host입니다. dev 환경의 전체 시스템은 그대로 두고, 지금 고치는 한 서비스만 내 노트북의 것으로 바꿔 끼운다. 이 접근은 로컬 개발의 어려움을 뒤늦게 우회해서는 결코 도달할 수 없는 모양의 해법입니다.


로컬에서 전부 띄우는 건 불가능하다

한 서비스를 고치는 개발자가 마주하는 현실은 이렇습니다. 수정한 코드를 돌려보려면 그 서비스 혼자서는 안 되고, Gateway, 인증 서버, DB, 메시지 큐, 그리고 그 서비스가 호출하는 이웃 도메인까지 전부 필요합니다.

flex만 해도 9개 이상의 도메인 서비스, Gateway, Kafka, MySQL, Elasticsearch, Redis가 맞물려 돌아갑니다. 전부 노트북에 띄우려는 시도는 자원 문제 이전에 심리적 부담입니다. docker-compose로 묶어둔다 해도 최신 빌드를 따라가는 일, 인증 토큰과 샘플 데이터와 이벤트 토픽 상태를 맞추는 일만으로 하루가 갑니다.

매번 dev 클러스터에 배포해 확인하는 것도 답이 아닙니다. Git push → CI → 이미지 빌드 → 매니페스트 업데이트 → ArgoCD 동기화. 작은 실험 하나에 이 루프를 도는 순간 이터레이션 속도는 주저앉습니다.

개발자가 원하는 구도는 하나입니다. "dev 클러스터에 이미 돌고 있는 시스템 전체를 그대로 쓰되, 내가 수정 중인 서비스 하나만 내 노트북의 것으로 바꿔 쓰고 싶다." Rewrite Host는 이 요구를 디버그 헤더 하나로 풉니다.


헤더가 곧 라우팅 스위치다

flex의 Gateway는 Spring Cloud Gateway 기반의 글로벌 필터 위에 Rewrite Host 로직을 얹어 두었습니다. 동작은 단순합니다. 요청에 특정 헤더가 붙어 있으면, Gateway는 원래 목적지 대신 그 헤더가 지정하는 주소로 요청을 보냅니다.

헤더는 두 갈래입니다. "이 기능을 활성화하겠다"는 디버그 스위치 헤더와, "어느 서비스를 어느 주소로 바꿀지" 지정하는 서비스별 라우팅 매핑 헤더. 스위치 헤더가 없으면 매핑은 무시되고 정상 라우팅으로 돌아갑니다. 스위치가 켜져 있을 때만 Gateway가 매핑을 해석해 라우팅 테이블을 일시적으로 조정합니다.

매핑 헤더는 원하는 대상만큼 붙일 수 있습니다. Accounting API만 자기 노트북 인스턴스로 돌리고 싶다면, 해당 서비스 이름을 담은 매핑 헤더 한 줄에 로컬 주소를 값으로 얹으면 끝입니다. 다른 서비스는 모두 dev의 것을 씁니다. 인증 서버도 dev, Gateway도 dev, DB도 dev. 손대고 있는 한 지점만 로컬로 끌어옵니다.


Gateway 필터는 주소만 바꿔치기한다

헤더가 스위치라면, 그 스위치가 눌린 뒤 무엇이 바뀌는지를 보겠습니다. 내부 구현의 핵심은 생각보다 단순합니다. Gateway 필터는 원래 목적지 URI를 꺼내서 scheme/host/port 세 요소만 매핑에 따라 교체합니다. 경로, 쿼리, 헤더는 그대로 둡니다. 라우팅이 일어나기 전 URI 결정 지점에서 이 치환을 한 번 수행하면, 그 뒤의 로드 밸런싱, 재시도, 써킷 브레이커는 원래 라우팅이었던 것처럼 작동합니다. 요청의 바디도, 인증 헤더도 그대로 전달됩니다. 바뀌는 건 "어느 주소로 가는가" 하나뿐입니다.

이 설계가 단정해지는 이유는 "라우팅 주소"라는 축을 분리해 두었기 때문입니다. 타임머신이 "현재 시간"이라는 한 축만 바꾸고 나머지를 유지했듯이, Rewrite Host는 "대상 주소"라는 한 축만 바꿉니다. 두 도구는 다른 문제를 풀지만 같은 사고방식 위에 서 있습니다.


불일치를 응답 헤더로 돌려준다

헤더를 붙였는데 기능이 동작하지 않으면 개발자는 당황합니다. "내가 헤더를 잘못 썼나? Gateway가 무시했나? 이 환경에서는 꺼져 있나?" 질문이 쌓이면 디버깅 도구 자체가 디버깅을 필요로 하는 블랙박스가 됩니다.

flex의 Rewrite Host는 이 문제를 응답 헤더로 풉니다. 디버그 헤더는 있었지만 매핑이 어떤 라우트와도 맞지 않거나, 기대한 서비스 ID가 없거나, 헤더 포맷이 부정확할 때 — Gateway는 응답에 불일치 피드백 헤더를 얹어 "이러이러한 이유로 적용되지 않았다"는 신호를 돌려줍니다.

응답 헤더 한 번이면 개발자는 자기 헤더가 유효했는지 즉시 판단합니다. 브라우저 개발자 도구 네트워크 탭에서도, curl의 -v 옵션에서도 이 정보는 곧바로 보입니다. 조용히 실패하지 않는다는 약속이, 헤더 기반 토글 도구를 신뢰할 수 있게 만드는 얇은 층입니다.


프론트엔드에도 같은 원리

이 사고방식은 백엔드에만 머물지 않습니다. flex에서는 같은 개념이 마이크로 프론트엔드 구성에도 적용됩니다.

사용자 브라우저가 Gateway에 요청을 보낼 때, 특정 마이크로 프론트엔드 앱 번들만 로컬 개발 서버에서 받아 오고 싶을 때가 있습니다. 근태 UI만 수정 중이라면, 전체 웹앱은 dev 번들을 쓰되 근태 앱 번들만 로컬에서 가져오고 싶습니다. 이를 위한 채널이 마이크로 프론트엔드용 라우팅 매핑 헤더입니다.

개발자가 브라우저 확장이나 proxy 스크립트로 이 헤더를 요청에 주입하면, Gateway 또는 frontend 라우팅 레이어가 해당 앱 번들의 주소만 로컬로 치환합니다. 다른 번들은 dev, 인증도 세션도 dev. 프론트엔드 개발자도 로컬에 전체 시스템을 재현할 필요가 없습니다.

백엔드 Rewrite Host와 프론트엔드 Rewrite Host는 구현이 다르지만(한쪽은 Spring Cloud Gateway 필터, 다른 쪽은 CDN/서빙 레이어의 번들 라우팅), 정신적 모형은 같습니다. "이 요청이 도달하는 주소"라는 축만 갈아 끼우고, 나머지는 그대로 둔다. 편집 중인 부분이 어디든 전체를 재현하지 않고 부분만 교체해 검증할 수 있다는 약속이 레이어를 가로질러 지켜집니다.


시간 축과 공간 축, 같은 사고방식

레이어를 가로지른 이 약속은, 시리즈를 가로질러도 똑같이 유지됩니다. 4화와 5화를 나란히 놓고 보면 둘이 같은 설계 양식을 따른다는 사실이 드러납니다.

타임머신에서는 Clock이 Port였고, 기본 Adapter와 헤더 기반 Adapter가 있었습니다. Rewrite Host에서는 라우팅이 Port이고, 기본 Adapter(원래 라우팅)와 헤더 기반 Adapter(로컬 주소로 치환하는 라우팅)가 있습니다. 시간이든 공간이든 축을 한 번에 하나씩만 바꾸고 나머지는 그대로 둔다는 절제가 동일합니다.

이 대칭은 우연이 아닙니다. 둘은 같은 팀의 같은 사고방식으로 태어났고, 같은 코드베이스의 같은 디렉토리에 이웃합니다. 디버그 헤더 네임스페이스를 공유하고, 응답 헤더 피드백 채널의 원칙을 공유하고, "환경이 이 기능을 켠다"는 활성화 제어를 공유합니다. 위에서 내려다보면 같은 도구의 두 축입니다.

전제 조건도 같습니다. 시간 축을 교체하려면 코드가 Clock을 주입받아야 하고, 공간 축을 교체하려면 Gateway가 라우팅을 한 곳에서 결정해야 합니다. 두 전제 모두 "경계가 어디에 있는지"를 구조적으로 드러내는 규율이고, 그 규율이 없으면 두 도구 모두 작동하지 않습니다. 본편 5화가 말한 "경계를 깎아두면 그 경계를 따라 교체할 수 있다"는 문장이 여기서 가장 구체적으로 증명됩니다.


더 나아가: Service Discovery로 내부 통신까지 관통시키기

지금까지 설명한 Rewrite Host는 외부에서 Gateway로 들어오는 첫 번째 요청에만 작용합니다. Gateway가 헤더를 보고 Accounting API를 로컬로 돌렸다 해도, 그 이후의 여정은 Gateway의 관할 밖입니다. Accounting API가 내부에서 Payroll API를 호출하는 순간, 그 호출은 서비스 간 내부 경로를 따라 dev 클러스터의 Payroll로 흘러갑니다. 디버그 헤더가 거기까지 전달되지 않으니 Rewrite Host가 개입할 여지도 없습니다. 연쇄 호출의 입구만 바뀌고 안쪽은 그대로인 상태입니다.

이 한계는 도구의 설계 범위가 아직 Gateway 한 층에 머물러 있기 때문입니다. "축을 교체한다"는 사고방식이 진입점에서는 작동하지만, 서비스 망 안으로 깊어질수록 약속이 끊어집니다. 요청 체인 전체에서 "이 서비스는 로컬 인스턴스를 쓴다"는 약속을 유지하려면, 디버그 컨텍스트가 서비스 경계를 넘을 때마다 함께 흘러야 합니다.

다음 단계로 구상하는 방향은 두 가지입니다. 첫 번째는 디버그 헤더를 trace context처럼 전파하는 방식입니다. OpenTelemetry의 W3C TraceContext나 Baggage처럼, 디버그 헤더를 서비스 간 HTTP 호출에 자동으로 포워딩하도록 각 서비스의 클라이언트 레이어에 약속을 심습니다. Gateway가 내려보낸 디버그 컨텍스트를 Accounting API가 받아 들고, Payroll API를 호출할 때 그대로 얹어 보냅니다. Payroll API 앞의 Gateway(또는 sidecar)가 이 컨텍스트를 보고 라우팅을 조정합니다. 헤더가 사라지지 않는 한, 체인이 몇 단계를 지나도 로컬 인스턴스를 향하는 약속이 이어집니다.

두 번째는 Service Discovery 레이어에 통합하는 방식입니다. 서비스 간 호출이 Kubernetes Service, Eureka, Consul 같은 Service Discovery를 경유한다면, 로컬 서버를 해당 Service Discovery에 일시 등록하거나, 조회 시점에 디버그 컨텍스트를 참조해 로컬 주소를 돌려주는 방법이 있습니다. 각 서비스의 클라이언트 코드에 별도 약속을 심지 않아도 된다는 장점이 있지만, Service Discovery 자체에 디버그 인식 로직이 들어가야 한다는 부담이 따라옵니다. 두 방향 모두 "라우팅 결정 지점을 드러내고, 그 지점에 Adapter를 꽂는다"는 같은 원칙 위에 서 있습니다.

이 방향은 현재 도구가 걸어온 길의 자연스러운 연장선입니다. Gateway 한 층에서 작동하던 "축 교체"를 서비스 망 전체로 확장하면, 개발자는 진입점뿐 아니라 체인 중간 어느 서비스든 로컬 인스턴스로 교체해 관통 실험을 할 수 있습니다. 본편 5화가 말한 "경계를 깎아두면 그 경계를 따라 교체할 수 있다"는 약속이 수평으로 넓어지는 셈입니다.


다시, 부분만 바꿔서 검증한다

Rewrite Host는 라우팅이라는 하나의 축을 교체 가능한 Port로 만들고, 디버그 헤더를 그 Port에 꽂히는 Adapter로 다룹니다. 덕분에 개발자는 전체 시스템을 재현하지 않고도 편집 중인 서비스 하나만 바꿔 끼워 검증합니다. 응답 헤더의 피드백 채널이 사용성을 지탱하고, 환경별 활성화 제어가 프로덕션 오염을 막습니다.

부분만 바꿔서 검증한다는 약속이 로컬 디버깅에서 이 정도로 작동한다면, CI에서는 어떻게 같은 약속을 지킬 수 있을까요. Testcontainers는 그 답의 표준적 구성처럼 보이지만, 막상 돌려 보면 속도가 무너지기 시작합니다. 다음 편에서는 그 이유를 파고듭니다.

다음 화에서는,
컨테이너는 왜 폭발하는가. Testcontainers가 속도의 적이 되는 순간, 그리고 BuildService라는 해법”에 대하여 이야기 합니다.

🚀플렉스팀 채용페이지 바로가기☕flex Private Talk 신청하기
글이 마음에 드셨나요?
공유하기
페이스북링크드인트위터
flex가 궁금하다면? 지금 무료로 체험해 보세요
flex가 궁금하다면? 지금 무료체험하기
  • 뉴스룸
    2026. 5. 7
    "팀장님, 조직관리는 AI에 맡기세요" 플렉스가 말하는 AX시대 리더십
    채효진 플렉스 엔터프라이즈 컨설턴트 "관계 기반 AI로 주도적 리더십 구축"
  • 아티클
    2020. 5. 25
    근태관리, 유연근무제, 그리고 코로나 시대
    코로나, 뉴노멀, 유연근무제, 그리고 근태관리