아날로그 리팩터링 시티: 새 기능을 하나 더 넣기 전에, 도시 계획하듯 코드베이스를 존(zoning)하라
레거시 시스템에 기능을 하나 더 덧붙이기 전에, 코드베이스를 하나의 ‘도시’로 생각해 보자. 존잉, 인프라 계획, 그리고 구성원 참여를 통해, 소프트웨어를 더 확장 가능하고, 테스트 가능하며, 유지보수하기 쉬운 방향으로 안전하게 점진 리팩터링하는 방법을 살펴본다.
아날로그 리팩터링 시티: 새 기능을 하나 더 넣기 전에, 도시 계획하듯 코드베이스를 존(zoning)하라
소프트웨어 엔지니어와 도시 계획가는 생각보다 닮아 있다.
둘 다 이미 존재하는, 살아 있는 복잡한 시스템을 물려받는다. 도시에는 수십 년 동안 무질서하게 성장한 구역이 있고, 코드베이스에는 10년 넘은 해킹과 급한 땜질 코드가 있다. 둘 모두 “이거 하나만 더 넣자”라는 압박을 받는다. 여기에는 쇼핑몰 하나, 저기에는 피처 플래그 하나. 이미 기반 시설은 버거워하고 있는데 말이다.
그냥 무작정 기능을 얹어 가는 대신, 레거시 코드베이스를 하나의 ‘도시’로 보고, 리팩터링을 도시 계획처럼 해 볼 수는 없을까?
이 글에서는 존잉(zoning), 인프라, 그리고 시민(팀 구성원) 참여라는 개념이, 다음 마천루(새 기능)를 올리기 전에 시스템을 더 이해하기 쉽고, 테스트 가능하며, 유지보수하기 쉽게 만드는 안전한, 점진적 변경을 어떻게 이끌 수 있는지 살펴본다.
1. 코드베이스는 퍼즐이 아니라 ‘도시’다
우리는 종종 코드를 정적인 퍼즐처럼 이야기한다. 고정된 조각을 잘 맞추면 정답 하나가 나오는 식으로 말이다. 하지만 어느 정도 성숙한 시스템은 실제로는 도시와 더 비슷하게 움직인다.
- 시간이 지나면서 유기적으로 자란다.
- 서로 다른 사람들이 서로 다른 기준으로 만든 “동네”들이 섞여 있다.
- 아무도 누가 만들었는지 기억 못 하는 지름길, 골목길, 막다른 길들이 있다.
- 당신이 개선하는 동안에도, 사람들은 거기서 계속 살고 일해야 한다.
도시 메타포로 생각하면 리팩터링을 이렇게 다시 볼 수 있다.
- 구역(디스트릭트, Districts): 기능 영역, 모듈, 서비스
- 도로와 고속도로: 함수 호출, 메시지 큐, API, 데이터 파이프라인
- 기반 시설(Utilities): 로깅, 모니터링, 설정 관리, 빌드 시스템, CI/CD 파이프라인
- 존잉 규칙: 아키텍처 가이드라인, 코딩 컨벤션, 의존성 규칙
새 “빌딩”(기능)을 하나 더 올리기 전에 스스로에게 물어보자.
“이건 이 도시의 어디에 지어져야 하지? 그리고 이 빌딩이 요구하는 인프라는 무엇이지?”
2. 짓기 전에 경계를 먼저 그려라: 존잉(Zoning)
도시 계획가는 건물을 하나씩 배치하는 것부터 시작하지 않는다. 먼저 존잉을 한다. 어디가 주거, 상업, 공업 구역인지, 각 구역에서 무엇이 허용되는지를 정한다.
코드베이스도 마찬가지다.
시스템 안의 ‘구역’을 정의하라
지저분한 레거시 모놀리식(monolithic) 시스템에도, 사실상 존재하는 ‘구역’은 있다. 그것부터 식별하고 이름을 붙여 보자.
- 프레젠테이션(표현) 구역: UI 템플릿, 컨트롤러, API 핸들러
- 도메인 구역: 비즈니스 규칙, 코어 엔티티, 값 객체(value object)
- 인프라 구역: 데이터베이스 접근, 외부 API, 메시징, 파일 시스템
아마 이런 것들을 발견할 수 있을 것이다.
- 컨트롤러가 아무 데서나 직접 DB에 접근하는 코드
- 뷰 로직 안에 숨어 있는 비즈니스 규칙
- 온갖 걸 다 우겨 넣은 유틸리티 클래스(다목적 고층 빌딩처럼 쓰이는 클래스)
거대한 한 번의 리라이트(Big Bang rewrite)를 노리기보다, 먼저 존잉 경계부터 잡자.
- “새 비즈니스 로직은 도메인 레이어에 넣는다.”
- “컨트롤러는 도메인 서비스에는 의존해도 되지만, DB에는 직접 접근하지 않는다.”
- “인프라 디테일은 도메인 객체 밖에 있어야 한다.”
이건 가벼운 규칙이다. 실제 도시의 존잉 코드처럼, 새 코드가 어디에 위치할 수 있고, 무엇에 의존할 수 있는지를 제약하는 정도다.
존잉의 목적은 ‘완벽’이 아니라 ‘격리’다
도시는 하루아침에 정돈되지 않는다. 존잉의 목적은 다음과 같다.
- 새로 생기는 복잡도를 명확한 경계 안에 가두기
- 로컬 변경이 전체 코드베이스로 번져 나가는 것을 막기
- 배포를 멈추지 않고도, 시스템의 레이아웃을 서서히 재형성하기
각 경계를 방음벽이라고 생각해 보자. 한 구역의 변경 때문에 다른 모든 구역을 다시 만들고, 다시 테스트하고, 다시 이해해야 하는 상황을 막는 것이 목표다.
3. 도시 계획은 다학제다 — 리팩터링도 그래야 한다
도시 계획은 예쁜 지도를 그리는 일이 아니다. 다양한 분야가 얽혀 있다.
- 건축(Architecture): 건물은 어떤 모양이어야 하는가?
- 토목공학(Civil engineering): 다리는 실제로 버티는가?
- 사회과학(Social science): 사람들은 이 도시를 실제로 어떻게 사용하는가?
- 정치·경제: 예산은? 이해관계자는 누구인가?
코드베이스 리팩터링도 구조가 같다.
- 아키텍처: 모듈 경계와 레이어링은 어떻게 가져갈 것인가?
- 엔지니어링: 기존 의존성과 런타임 제약을 감안할 때 무엇이 기술적으로 가능한가?
- 디자인 & UX: 이 변경이 사용자 플로우, 사용성, 버그 패턴에 어떤 영향을 주는가?
- 사회적 요소: 팀은 어떻게 일하는가? 누가 무엇을 소유하는가? 핸드오프가 어디서 꼬이는가?
개발, QA, 운영 팀이 실제로 일하는 방식과 동떨어진 “완벽한” 아키텍처를 설계하면, 그 존잉 플랜은 실패한다. 사람들은 그 위를 우회해서 다닐 것이다.
그래서 이렇게 해야 한다.
- 개발자를 모듈 경계, 코드 오너십(ownership) 결정에 참여시킨다.
- QA를 테스트 경계, 회귀(risk) 포인트 정의에 참여시킨다.
- Ops/SRE를 배포 전략, 관측 가능성(observability), 장애 도메인(failure domain) 설계에 참여시킨다.
이들을 위에서 명령만 받는 대상이 아니라, 코드 도시의 시민으로 대하라.
4. 도시를 갈아엎기 전에 ‘도로망’을 먼저 공부하라
계획가는 느낌만으로 고속도로를 밀어버리지 않는다. 교통량, 토지 이용, 보행자 흐름을 분석한다.
코드에서 그에 해당하는 것은 의존성 그래프(dependency graph), 즉 코드 도시의 ‘도로망’이다.
의존성을 지도처럼 그려 보라
최소한 다음을 이해해야 한다.
- 어떤 모듈이 어떤 모듈에 의존하는가?
- 어떤 서비스가 어떤 API 또는 메시지 토픽을 호출하는가?
- 어디에 빽빽한 순환 의존(circular dependency)이 있는가?
정적 분석 도구, 아키텍처 다이어그램, 콜 그래프(call graph) 시각화 툴이 도움이 된다. 하지만 대충 손으로 그린 지도라도, 감(감각)에만 의존하는 것보다는 훨씬 낫다.
다음 같은 것들을 찾아보자.
- 고속도로(Highways): 모두가 호출하는 코어 서비스나 공용 라이브러리
- 병목 지점(Choke points): 엄청나게 많은 모듈이 의존하는 클래스나 모듈
- 불법 지름길(Illegal shortcuts): 상위 레이어에 의존해서는 안 되는 하위 레이어 코드, 의도된 경계를 가로지르는 의존성
지도를 바탕으로 점진적 변화를 설계하라
지도를 한 번 보게 되면, 도시 엔지니어처럼 계획할 수 있다.
- 병목 지점의 압력을 줄이기 위해, 안정적인 인터페이스를 추출한다.
- 지저분한 동네를 감싸는 순환도로(링 로드, ring road) — 파사드(facade)나 Anti-corruption Layer — 를 만든다.
- 위험한 샛길을 막는다 — 특정 크로스 레이어 의존을 금지하고, 린트(lint)나 자동 검증을 추가한다.
이렇게 하면 도시 전체를 멈추지 않고도, 도로망을 조금씩 리팩터링할 수 있다. 새 도로를 추가하고, 낡은 도로를 폐기하며, 교통 흐름을 서서히 바꿔 나가는 식이다.
5. 스프롤링(Sprwaling) 도시에서의 최신 빌드 시스템을 경계하라
최신 빌드 시스템은 고급 건설 장비와 같다. 강력하지만, 기존 문제를 증폭시키기도 한다.
의존성 그래프가 이미 엉켜 있다면, 빠르고 똑똑한 빌드 툴을 추가해도 문제가 사라지지 않는다. 다만 전역 리빌드(global rebuild) 가 더 빨리, 더 아프게 돌아갈 뿐이다.
핵심 문제는 크레인이 느린 게 아니다.
“발코니 하나만 고쳤는데, 존잉 규칙 때문에 도시 전체 스카이라인을 다시 심사해야 하는 구조”
이게 문제다.
꼬인 의존성이 빌드를 망치는 방식
모든 것이 모든 것에 의존할 때, 다음이 벌어진다.
- 작은 코드 변경도 시스템의 거대한 부분에 영향을 주는 것처럼 보인다.
- 빌드 툴은 보수적으로, 엄청나게 넓은 범위를 다시 빌드한다.
- CI 시간이 폭증해서, 리팩터링과 실험의 비용이 치솟는다.
빌드 툴을 탓하기 전에, 도시 계획을 다시 보자.
- 모듈과 레이어 경계를 명확히 도입한다.
- 구역 간의 무분별한 호출을 최소화한다.
- 명확한 인터페이스와 의존성 역전(Dependency Inversion)을 향해 나아간다.
도시의 구조가 단순해지고 나서야, 최신 빌드 시스템이 진짜 힘을 발휘한다. 인크리멘털 빌드, 병렬화, 안정적인 캐싱 같은 것들 말이다.
6. 영웅적 불도저보다, 꾸준한 도로 보수를 선호하라
모두가 한 번쯤 꿈꾼다.
도시를 멈추게 하고, 싹 밀어버린 뒤, 완벽하게 정렬된 격자형 메트로폴리스를 새로 짓는 것.
소프트웨어 세계 표현으로는 “영웅적인 풀 리라이트(full rewrite)”다.
현실은 다르다.
- 풀 리라이트는 위험하고, 비용이 크며, 기능 동등성(feature parity)에 도달하기도 전에 실패하는 경우가 많다.
- 비즈니스는 당신이 새 지도를 그리는 동안 멈춰 있지 않는다.
- 레거시 시스템이 처리하던 수많은 엣지 케이스와 내재된 지식을 잃어버리게 된다.
대부분의 도시 계획가는 이미 존재하는 것들 주변에서 일한다.
- 새 도로를 추가하고,
- 특정 구역만 재존잉(rezone)하며,
- 상하수도·전기 같은 인프라를 블록 단위로 업그레이드한다.
코드베이스도 똑같이 할 수 있다.
점진 리팩터링 패턴들
- Strangler Fig 패턴: 기존 기능을 새 인터페이스 뒤에 감싼다. 일부 트래픽을 새 구현으로 라우팅하고, 점점 범위를 늘려 가다가 결국 옛 코드를 제거한다.
- Anti-corruption Layer: 레거시 모듈 주변에 경계를 설치하고, 옛 모델과 새 모델 간의 변환을 여기서 처리한다. 이렇게 하면 레거시 제약이 시스템 전반에 퍼지지 않는다.
- 보이스카우트 규칙(Boy Scout Rule): 파일을 건드릴 때마다 조금이라도 더 나은 상태로 남겨 둔다. 이름을 개선하고, 함수를 추출하고, 테스트를 추가하고, 로직을 올바른 “구역”으로 옮긴다.
생각을 이렇게 바꿔 보자.
“오늘 이 블록에서, 안전하게 재존잉할 수 있는 게 뭐지?”
라는 질문으로 시작하고,
“도시 전체를 어떻게 갈아엎지?” 는 잊어버리는 것이다.
이런 작은, 저위험 변화들이 쌓여서 결국 큰 구조적 개선으로 이어진다.
7. 시민을 참여시켜라: 비밀 마스터 플랜이 아니라, 모두가 아는 가이드라인
서랍 속 한 권의 바인더에만 들어 있는 도시 계획은 쓸모없다. 아무도 지키지 않는 아키텍처 다이어그램은 그저 벽 장식일 뿐이다.
리팩터링을 지속 가능하게 만들려면, “시민”을 참여시키고 습관으로 굳혀야 한다.
존잉 규칙을 명시적으로 만들라
- 핵심 경계를 위해 짧은 ADR(Architecture Decision Record) 를 쓴다. 예: “UI는 도메인에 의존할 수 있지만, 도메인은 순수(pure)해야 하고, 인프라는 교체 가능해야 한다.”
- 코드 리뷰를 위한 가벼운 체크리스트를 만든다. 예: “이 새 코드는 레이어링을 잘 지키는가?”, “인프라 디테일이 도메인 객체 밖에 머물고 있는가?”
- 가능하면 린터나 모듈 경계 검사 같은 자동화된 검증을 추가한다.
실제로 일하는 사람들과 상의하라
- 개발자에게 물어보자: “어디가 유난히 위험하게 느껴지나요? 작은 변경인데도 불안한 곳이 어디죠?” 이런 곳이 대개 나쁜 교차로이거나, 빠진 경계가 있는 구역이다.
- QA에게 물어보자: “어느 부분이 특히 깨지기 쉽고, 회귀 테스트를 잔뜩 해야 하는 구역인가요?” 이런 지점이 존잉 실패 지점이고, 로컬 변경이 전역 영향으로 번지는 곳이다.
- Ops/SRE에게 물어보자: “프로덕션에서 가장 자주 문제를 일으키는 서비스나 모듈은 뭐죠?” 이런 곳이 과부하된 유틸리티나, 위태로운 다리 같은 곳이다.
사람들이 느끼는 실제 경험과 리팩터링 계획이 맞아떨어질 때, 모두가 새 “존잉 코드”를 지키려는 의지가 훨씬 커진다.
8. 다음 기능을 넣기 전에, 모든 것을 종합해 보자
레거시 코드 “도시”에 새 기능을 하나 더 얹기 전에, 잠깐 멈추고 다음을 해 보자.
-
도시를 지도에 그려라
- 주요 구역을 식별한다(UI, 도메인, 인프라, 크로스 컷팅 관심사 등).
- 핵심 의존성과 콜 패스를 시각화한다.
-
존잉을 정의하거나 다듬어라
- 어떤 종류의 코드가 어디에 있어야 하는지 정한다.
- 새 코드에 적용할, 단순하지만 강제 가능한 규칙을 적어 둔다.
-
인프라 개선을 계획하라
- 가장 심각한 병목 지점과 꼬인 교차로부터 먼저 손본다.
- 지저분한 영역 주변에 파사드나 Anti-corruption Layer를 도입한다.
-
도시 친화적인 방식으로 기능을 배포하라
- 새 기능을 적절한 구역에 배치하고, 경계를 존중한다.
- Strangler, Boy Scout 같은 점진 패턴을 사용해, 손대는 곳을 조금씩 개선한다.
-
시민을 참여시켜라
- 리팩터링을 팀의 워크플로, 테스트, 운영 방식과 정렬시킨다.
- 피드백에 따라 계획을 조정하고, 도시가 진화함에 맞춰 다시 그린다.
결론: 오래 살 수 있는 도시를 만들어라
당신에게 필요한 것은 완벽하게 반짝이는 스마트 시티가 아니다. 수년간 소프트웨어를 만들고, 유지보수하고, 안전하게 발전시킬 수 있는 도시다.
코드베이스를 도시처럼, 리팩터링을 도시 계획처럼 생각하면 다음과 같은 효과가 있다.
- 구조적 문제를 일상 언어(구역, 도로, 인프라)로 가시화할 수 있다.
- 영웅적 리라이트가 아니라, 경계와 존잉에 우선순위를 둘 수 있다.
- 의존성 지도를 이용해, 좁은 범위에 집중된 점진 개선을 설계할 수 있다.
- 기술적 변경을, 실제 팀의 일하는 방식과 정렬시킬 수 있다.
다음에 누군가 이렇게 말하더라도,
“이 기능 하나만 그냥 추가하면 안 돼요?”
당신의 대답은 꼭 체념 섞인 “네…”일 필요가 없다.
이렇게 말할 수 있다.
“넣을 수는 있는데, 먼저 존잉 맵부터 같이 보죠.”
이 사고방식의 전환이, 깨지기 쉬운 레거시 스프롤(sprawl)을 탄탄하고 살기 좋은 코드 도시로 바꾸는 출발점이다.