Step 14. 진흙공에서 탈출하기 — 브라운필드 이행

만화로 보는 요약 — 먼저 읽어보세요
브라운필드 이행은 동작을 먼저 테스트로 고정하고 봉합선을 만든 뒤, 스트랭글러 방식으로 기능을 조금씩 옮겨 달리는 시스템을 고친다.
면접 실전 질문: ① 레거시 코드는 왜 검증된 지식인가? ② 특성화 테스트는 무엇을 고정하는가? ③ 스트랭글러 무화과 패턴의 단계는 무엇인가?
이 장의 질문: 멈출 수 없는 시스템을, 어떻게 달리는 채로 고쳐 긋는가? 그리고 왜 “다 밀고 새로 짜자”는 제안은 거의 언제나 함정인가?
1. 현실의 출발점 — 그린필드는 사치다
앞 장의 실험은 사치였다 — 빈 종이, 빈 패키지, 마음대로 긋는 경계. 현실의 절대다수는 다르다: 이미 굴러가는 시스템, 매출이 흐르는 코드, 그리고 Step 05에서 배운 그대로의 진흙. 아키텍처 책의 마지막 실전 질문은 그래서 이것이다 — 이미 있는 것을 어떻게 바꾸는가.
가장 유혹적인 답부터 처형하자: 전면 재작성(big bang rewrite). Joel Spolsky가 2000년에 “절대 하지 말 것”이라 못박은 그것이다 — 그의 사례는 Netscape가 브라우저를 처음부터 다시 쓰기로 결정하고 3년을 시장에서 사라진 일이었고, 그 공백이 경쟁자에게 시장을 헌납했다. 재작성이 함정인 이유는 Step 05에서 이미 절반을 배웠다: 코드에만 축적된 도메인 지식 — 그 이상한 if문이 막고 있는 실제 사고 — 이 함께 버려진다. 나머지 절반은 산수다: 재작성 기간 동안 기능은 동결되는데 요구사항은 동결되지 않는다. 목표물이 움직이는데 총알은 3년 전에 발사됐다. Reconstruction이 논문에서도 최후 수단이었던 이유다.
그래서 남는 답은 하나다. 달리는 채로 고친다. Keep It Working(Step 05)의 완성형이 이 장의 내용이다.
2. 레거시의 정확한 정의 — 그리고 순환의 탈출구
Michael Feathers의 『Working Effectively with Legacy Code』(2004)는 이 분야의 원전이고, 그의 정의는 도발적이다:
레거시 코드란 테스트가 없는 코드다.
낡음도, 언어도, 작성자의 퇴사 여부도 아니다 — 테스트의 부재. 이 정의가 정확한 이유는 브라운필드의 근본 순환을 드러내기 때문이다: 코드를 안전하게 바꾸려면 테스트가 필요하다 → 테스트를 붙이려면 (의존성을 끊을) 구조가 필요하다 → 구조를 바꾸려면 코드를 바꿔야 한다 → 처음으로.
Feathers의 탈출구는 두 개념이다. 봉합선(seam) — “그 자리를 편집하지 않고도 동작을 바꿀 수 있는 지점”. 우리의 언어로 번역하면, 화살표를 가로챌 수 있는 곳 — 인터페이스, 주입 지점, 함수 포인터. 브라운필드 작업이란 결국 봉합선이 없는 곳에 최소 침습으로 봉합선을 만드는 수술이다. 그리고 특성화 테스트(characterization test) — 올바른 동작이 아니라 현재 동작을 사실 그대로 기록하는 테스트. “이 함수는 무엇을 해야 하는가”는 아무도 모르지만 “지금 무엇을 하는가”는 실행하면 안다. 그것을 골든 마스터로 박제한 뒤에야 리팩터링이 도박에서 공학이 된다. (봉합선조차 낼 수 없는 자리를 위한 보조 기법도 Feathers에게 있다 — 엉킨 코드를 편집하는 대신 새 기능을 새 메서드/클래스로 곁에 싹틔우는 스프라우트(sprout).)
3. 스트랭글러 무화과 — 감싸고, 옮기고, 숙주를 제거한다
전략 차원의 원전은 Fowler의 스트랭글러 무화과 패턴(2004 — 원제는 “Strangler Application”이었고, “무화과(Fig)“는 훗날 은유를 더 정확히 하려 덧붙인 개명이다)이다. 은유부터: 호주의 교살자 무화과는 숙주 나무의 가지 위에서 발아해, 뿌리를 땅까지 내리며 숙주를 감싸고 자라다가, 마침내 숙주가 죽어도 스스로 서는 구조물이 된다. 번역하면:
- 가로채기(interception). 낡은 시스템으로 들어가는 요청/이벤트의 길목에 파사드(프록시, 게이트웨이, 또는 코드 수준의 인터페이스)를 세운다. 처음에는 전부 그대로 통과시킨다 — 이 시점의 diff는 위험이 거의 없다.
- 기능 단위 이관(asset capture). 기능 하나를 새 구조로 구현하고, 길목에서 그 기능의 트래픽만 새 쪽으로 돌린다. 낡은 쪽 코드는 지우지 않고 방치한다 — 지우는 것은 나중이다.
- 반복, 그리고 숙주 제거. 이관된 기능이 늘수록 낡은 시스템은 속이 비어가고, 마지막 트래픽이 옮겨진 날 통째로 제거한다.
핵심 성질은 매 단계가 배포 가능하고 되돌릴 수 있다는 것이다. 빅뱅 재작성이 3년짜리 도박 한 판이라면, 스트랭글링은 수백 개의 작은 내기다 — 각각은 지더라도 되돌리면 그만이다. Step 02의 언어로: 되돌리기 비싼 결정 하나를, 되돌리기 싼 결정 수백 개로 분해한 것이다.
4. 실전 도구 상자
전략(무화과) 아래에서 쓰는 전술들 — 대부분 이 책 앞부분의 재회다.
- 추상화 브랜치(Branch by Abstraction). 오래 사는 Git 브랜치로 이행하면 머지 날 지옥이 온다. 대신 코드 안에 추상(인터페이스)을 세우고, 그 뒤에서 낡은 구현과 새 구현을 공존시키다가, 스위치를 넘기고(그 스위치의 표준 도구가 피처 토글이다), 낡은 쪽을 지운다. 알아봤겠지만 이것은 DIP의 이행 버전이다 — Step 03의 지렛대가 리팩터링 전술로 쓰이는 것.
- 안티커럽션 레이어(ACL). 새로 세운 도메인 모델이 낡은 시스템의 모델(스키마, 개념, 명명)에 오염되지 않도록 번역 계층을 세운다 — Step 07의 그 패턴, Step 05 “양탄자 밑으로”의 정제판. 이행기의 ACL은 임시 비용이 아니라 새 모델의 면역 체계다.
- 병행 실행(parallel run). 신구 두 구현을 같은 입력으로 동시에 돌리고 결과를 비교한다 — 신뢰가 쌓일 때까지 응답은 낡은 쪽 것을 쓴다. 금융이라면 이 장치가 낯익을 것이다: 대사(Step 11)의 비교-표시 메커니즘을 이행 검증에 재사용한 것이다 — 다만 목적의 차이는 구분하자. 대사의 불일치는 관리할 업무 사건이지만, 병행 실행의 신·구 불일치는 없애야 할 이행 버그다. 그 구분 위에서라면, 정산 로직 이행에 병행 실행 + 일일 대사만큼 강한 안전망은 없다.
- 데이터 이행. 가장 어려운 마지막 조각. 이중 기록(양쪽 DB에 쓰기 — 이중 쓰기 문제가 그대로 오므로 아웃박스/CDC로), 또는 CDC로 낡은 DB를 소스 삼아 새 DB를 따라가게 하다가 소유권을 넘긴다. Step 11의 도구 없이 이 단계는 불가능하다.
5. 앞 장 예제로 보는 이행 경로 — 레이어드를 헥사고날로 교살하기
앞 장의 layered/OrderService가 5년 묵은 실제 시스템이라 치자. 규칙 30개, 테스트 0개, 매일 정산이 돈다. 경로는 이렇다.
- 특성화 테스트로 현재 동작을 박제한다. “정산 완료 주문 취소 시 IllegalStateException” — 옳은지 묻지 말고 기록한다. 이것이 이후 모든 단계의 안전망이다.
- 봉합선을 만든다 = 추상화 브랜치.
OrderService가 직접new하던MySqlOrderRepository앞에 인터페이스를 세운다 — 단, Step 03을 기억하라, 인터페이스는 도메인 쪽에 둔다(여기서 소유권을 틀리면 이행이 끝나도 화살표는 그대로다). 기존 저장소는 그 인터페이스의 어댑터로 감싼다. 정직한 각주: 1·2단계는 실무에서 맞물린다 — 2절의 순환 그대로다. 실제 DB에 붙은 코드라면 특성화 테스트부터가 봉합선을 요구하므로, 얻을 수 있는 첫 지점(공개 API 경유, 테스트 DB, 최소 침습 봉합선 하나)에서 시작해 서로를 끌어올린다. - 규칙을 한 개씩 이사시킨다. 서비스의 if문 하나를
Order.cancel()로 옮기고, 특성화 테스트로 동작 불변을 확인하고, 커밋. 서른 번 반복한다 — 지루함이 미덕이다. 각 이사는 밀리초 도메인 테스트를 하나씩 남긴다. - 정산 채널을 포트로 뽑는다.
System.out.println자리(실제라면 얽혀 있는 원장 기록)를SettlementLedger포트로 — 병행 실행을 붙이기 가장 좋은 지점이다. - 적합도 함수를 켠다. 이관이 끝난 영역부터 ArchUnit 규칙(앞 장의
ArchitectureRulesTest)을 적용한다 — 새로 그은 경계가 다시 침식되는 것을(Step 12) 막는 자물쇠. 이행의 마지막 단계는 코드가 아니라 방어선 가동이다.
주목할 것: 이 경로 어디에도 “시스템 정지”가 없고, 각 단계는 하루 안에 배포 가능하다.
6. 전략 — 어디부터 자르고, 무엇을 포기하는가
어디부터? Newman(『Monolith to Microservices』, 2019)의 조언을 압축하면: 첫 대상은 작고, 결합이 낮고, 그러나 가치가 보이는 조각이다. 가장 아픈 곳(대개 가장 결합 높은 심장부)부터 수술하고 싶은 유혹은 누르라 — 첫 이행은 기술 검증이자 조직의 학습이므로, 실패해도 싸고 성공하면 다음 이행의 신뢰 자본이 되는 곳이어야 한다. 자르는 선의 기준은 이미 있다: 바운디드 컨텍스트(Step 07) — 모델과 언어가 갈리는 곳이 가장 저항이 적은 절단면이다.
무엇을 포기하는가? 세 가지를 명시적으로 포기해야 이행이 산다.
- 일시적 순수성. 이행 중의 시스템은 두 아키텍처가 공존하는 잡종이다 — 이것은 실패 상태가 아니라 정상 상태이고, 수개월~수년 지속된다. ACL과 파사드가 그 공존의 관리 도구다.
- 완벽한 목표 설계. 이행하며 배운 것이 목표를 수정한다(Cunningham의 학습-상환, Step 02). 첫 날의 설계도를 경전으로 만들지 마라.
- “언젠가 끝내지”라는 낙관. 가장 흔한 실패는 이행의 중단이다 — 스트랭글링이 절반에서 멈추면 두 시스템을 영구히 유지하는 최악의 상태가 된다. 그래서 이행은 기술 프로젝트가 아니라 예산과 종료 조건이 있는 사업 결정이어야 한다. 낡은 시스템의 제거일까지가 프로젝트다.
7. 흔한 오해 바로잡기
오해 1: “재작성이 결국 더 빠르다.” 그 계산에는 보통 세 항목이 빠져 있다: 코드에만 축적된 도메인 지식의 복원 비용, 재작성 기간의 기능 동결(경쟁자는 동결하지 않는다), 그리고 “새 시스템도 완성 무렵엔 레거시”라는 시간의 법칙(Lehman, Step 05). 재작성이 정당한 경우는 존재하지만 — 시스템이 작거나, 도메인 지식이 코드 밖에 온전히 있거나 — 그것은 예외이지 기본값이 아니다.
오해 2: “낡은 코드는 나쁜 코드다.” 낡은 코드는 검증된 코드이기도 하다 — 그 안의 기괴한 분기 하나하나가 실제로 터졌던 사고의 기록이다(동작하는 지식의 보고, Step 05). 경멸은 이행의 적이다: 낡은 코드를 읽지 않고 새로 쓰면, 그 사고들을 운영 환경에서 다시 배우게 된다.
오해 3: “스트랭글러는 마이크로서비스 추출 전용이다.” 프로세스 경계와 무관한 전략이다. 5절이 보였듯 한 모듈 안에서 레이어드 → 헥사고날로 옮기는 데도, 모놀리스를 모듈러 모놀리스로 재구조화하는 데도 같은 원리(가로채기 → 점진 이관 → 숙주 제거)가 작동한다.
오해 4: “이행은 기술 부서의 프로젝트다.” 이행 기간 내내 기능 요구는 계속 들어오고, 우선순위 경쟁에서 이행은 항상 진다 — 사업 가치와 묶이지 않는 한. 잘 되는 이행은 “리팩터링 스프린트”가 아니라 기능 개발의 경로 위에 이행을 얹는다: 이번 분기에 손대야 하는 바로 그 영역부터 교살한다.
8. 요약, 그리고 마지막 장으로
- 그린필드는 사치다. 전면 재작성은 도메인 지식 소실 + 기능 동결 + 움직이는 목표물이라는 세 겹 함정 — 답은 달리는 채로 고치기.
- 레거시 = 테스트 없는 코드(Feathers). 탈출구는 봉합선(화살표를 가로챌 지점을 최소 침습으로 만들기)과 특성화 테스트(옳은 동작이 아니라 현재 동작의 박제).
- 스트랭글러 무화과: 가로채고 → 기능 단위로 이관하고 → 숙주를 제거한다. 되돌리기 비싼 결정 하나를 되돌리기 싼 결정 수백 개로 분해하는 전략.
- 전술은 재회다: 추상화 브랜치(=DIP의 이행 버전), ACL(=새 모델의 면역 체계), 병행 실행(=대사의 재사용), 데이터 이행(=Step 11 도구 총동원). 마지막 단계는 적합도 함수 가동.
- 어디부터: 작고 결합 낮고 가치 보이는 곳, 바운디드 컨텍스트 절단면. 무엇을 포기: 일시적 순수성에 대한 결벽, 완벽한 목표 설계, 그리고 “언젠가”라는 낙관 — 종료 조건 없는 이행은 최악의 상태에서 멈춘다.
마지막 장 예고: 이제 도구는 전부 손에 있다. 남은 것은 하나 — 고르는 법. 팀 규모, 도메인 복잡도, 수명에 따라 무엇을 선택하고, 그 선택을 어떻게 기록으로 남기는가(ADR). Step 01에서 세운 정의 — 아키텍처란 결정과 그 근거 — 가 실천 도구로 돌아오며 책이 닫힌다.
원전
| 연도 | 문헌 | 이 장에서의 역할 |
|---|---|---|
| 2000 | Spolsky, Things You Should Never Do, Part I | 전면 재작성의 처형 (Netscape 사례) |
| 2004 | Fowler, Strangler Application (bliki; 후에 Strangler Fig Application으로 개명) | 스트랭글러 무화과 패턴의 원전 |
| 2004 | Feathers, Working Effectively with Legacy Code | 레거시=테스트 없는 코드, 봉합선, 특성화 테스트 |
| 2007경 | Hammant 외, Branch by Abstraction (이후 Humble의 CD 문헌으로 보급) | 추상화 뒤 구현 교체 |
| 2019 | Newman, Monolith to Microservices | 이행 패턴 집대성 — 가로채기, 병행 실행, 데이터 이행 |