Files
2nd/10_Wiki/Topics/Architecture/Legacy Code (레거시 코드).md
T

10 KiB

Legacy Code (레거시 코드)

📌 Brief Summary

마이클 페더스(Michael Feathers)는 레거시 코드를 단순히 다른 사람에게서 물려받은 코드가 아니라 "테스트가 없는 코드"로 정의합니다 [1, 2]. 잘 작성되고 객체 지향적인 코드라도 테스트가 없다면 시스템의 동작을 빠르고 검증 가능하게 변경할 수 없으므로 나쁜 코드(레거시)로 간주됩니다 [3]. 레거시 코드는 이해하기 어렵고 수정이 두려운 구조적 특징을 지니며, 이를 안전하게 개선하고 기능을 추가하려면 의존성을 끊고 테스트를 먼저 작성하는 체계적인 과정이 필수적입니다 [3-5].

📖 Core Content

  • 레거시 코드의 딜레마와 해결 알고리즘 레거시 코드를 안전하게 변경하려면 테스트가 필요하지만, 테스트를 작성하려면 기존 코드의 얽힌 의존성을 끊어내기 위해 코드를 먼저 수정해야 하는 모순에 직면합니다 [5, 6]. 이 딜레마를 돌파하는 표준 알고리즘은 1) 변경 지점 식별, 2) 의존성 제거, 3) 테스트 작성, 4) 기능 변경, 5) 리팩토링 순으로 진행됩니다 [6, 7].
  • 접점 (Seams)을 통한 의존성 분리 레거시 코드에 테스트를 적용하려면 소스 코드를 직접 편집하지 않고도 프로그램의 동작을 바꿀 수 있는 지점인 '접점(Seam)'을 찾아야 합니다 [5, 8]. 객체 지향 언어에서는 다형성을 활용하여 테스트 시 가짜 객체를 주입할 수 있는 '객체 접점(Object Seams)'이 가장 널리 사용되며, C/C++와 같은 환경에서는 전처리 접점(Preprocessing Seams)이나 링크 접점(Link Seams)을 활용하여 의존성을 우회합니다 [9-12].
  • 특성화 테스트 (Characterization Tests)와 승인 테스트 명세가 없거나 코드가 불투명한 레거시 환경에서는 코드가 '무엇을 해야 하는지'보다 '실제로 어떻게 동작하는지'를 있는 그대로 포착하는 것이 중요합니다. 이를 위해 현재 시스템의 동작 상태를 기록하여 향후 예기치 않은 변경(부수 효과)을 방지하는 특성화 테스트 또는 승인 테스트(Approval Testing)를 도입해야 합니다 [7, 13].
  • 시간이 부족할 때의 우회 전략 (Sprout & Wrap) 테스트 없는 거대한 레거시 클래스(예: 수천 줄의 코드)에 당장 테스트를 작성할 시간이 부족할 때, 위험을 최소화하며 새 기능을 추가하는 두 가지 주요 기법이 있습니다 [14].
    • 스프라우트 기법 (Sprout Technique): 새로운 로직을 완전히 분리된 다른 곳에서 작성하고 단위 테스트를 거친 뒤, 기존 레거시 코드에서는 그 함수를 호출(삽입)만 하도록 처리합니다 [15, 16].
    • 랩 기법 (Wrap Technique): 기존 메서드의 이름을 변경한 후, 기존 메서드와 동일한 서명을 가진 새 메서드를 만듭니다. 새 메서드 내에서 기존 메서드를 호출하면서 그 앞이나 뒤에 새로운(테스트된) 로직을 추가하여 레거시 로직을 감쌉니다 [16, 17].
  • 스크래치 리팩토링 (Scratch Refactoring) 파악조차 불가능한 레거시 코드를 이해하기 위한 기법입니다. 테스트 없이 자유롭게 코드를 분리하고 변수명을 바꾸는 등 실험적 리팩토링을 수행하여 코드의 구조를 파악한 후, 작업이 끝나면 반드시 변경 사항을 원래대로 롤백(Revert)해야 합니다 [18, 19].

⚖️ Trade-offs & Caveats

  • 안전망 부재의 위험성: 테스트가 완비되지 않은 상태에서 레거시 코드의 구조를 대규모로 변경하는 것은 "그물 없이 공중 곡예를 하는 것"처럼 매우 큰 위험(회귀 버그 발생 등)을 초래합니다 [2, 20].
  • 코드의 미학적 저하 발생 가능성: 의존성을 깨고 테스트를 도입하는 수술적인(surgery) 과정에서, 단기적으로는 코드가 일시적으로 더 복잡하거나 못생겨질 수 있습니다. 하지만 이는 더 건강한 상태(테스트 가능한 상태)로 가기 위한 불가피한 트레이드오프입니다 [21].
  • 스프라우트(Sprout)와 랩(Wrap) 기법의 한계: 시간이 부족할 때 안전하게 기능을 추가하는 훌륭한 방법이지만, 이 기법들을 남용하면 결국 원본 클래스의 크기와 복잡도를 더 증가시키는 부작용(Pitfalls)이 발생할 수 있습니다 [14, 17].

🔗 Knowledge Connections

[레거시 대처 및 분석 기법]

  • Seams (접점)
    • 연결 이유: 레거시 코드 내에서 테스트 목적으로 동작을 변경하거나 의존성을 대체할 수 있는 구조적 틈새를 의미합니다 [8, 22].
    • 이 개념을 통해 더 깊게 이해할 수 있는 부분: 객체지향에서 테스트 더블(Mock, Fake)을 주입하여 레거시 코드를 안전한 격리 환경에 배치하는 원리.
  • Characterization Tests (특성화 테스트)
    • 연결 이유: 문서나 명세가 없는 레거시 시스템에서 코드를 변경하기 전, 현재의 실제 동작을 보호하기 위해 작성하는 안전망입니다 [13, 23-26].
    • 이 개념을 통해 더 깊게 이해할 수 있는 부분: 리팩토링 전 동작 보존을 확인하기 위한 실용적인 테스트 작성 및 회귀 방지 접근법.
  • Scratch Refactoring (스크래치 리팩토링)
    • 연결 이유: 복잡하게 얽힌 레거시 코드를 이해하기 위해 안전망 없이 구조를 이리저리 수정해 보고 파악한 뒤 되돌리는 지식 탐색 기법입니다 [18, 19].
    • 이 개념을 통해 더 깊게 이해할 수 있는 부분: 정적 분석만으로 파악하기 힘든 레거시 도메인 맥락을 동적이고 안전하게 습득하는 노하우.

[우회적 기능 추가 패턴]

  • Sprout Method (스프라우트 메서드)
    • 연결 이유: 거대한 레거시 코드 덩어리에 직접 새로운 제어문을 추가하는 대신, 신규 로직을 독립적으로 추출해 테스트한 뒤 호출점만 추가하는 기법입니다 [14-16].
    • 이 개념을 통해 더 깊게 이해할 수 있는 부분: 레거시 시스템의 기술 부채 증가를 억제하면서 새로운 요구사항을 안전하게 통합하는 방법.
  • Wrap Method (랩 메서드)
    • 연결 이유: 기존 메서드의 앞뒤에 새로운 기능을 추가해야 할 때, 기존 메서드를 이름 변경하여 숨기고 새 래퍼(Wrapper) 메서드로 기존 동작과 신규 동작을 연결하는 기법입니다 [16, 17].
    • 이 개념을 통해 더 깊게 이해할 수 있는 부분: 기존 코드의 내부를 훼손하지 않고 외부 인터페이스를 유지한 채 부가 기능을 안전하게 탑재하는 구조적 패턴.

Deeper Research Questions

  • 객체지향 언어(Java, C++)와 절차적 언어(C)에서 시스템의 동작을 가로채기 위해 사용되는 접점(Preprocessing, Link, Object Seam)의 적용 방식과 그 한계는 어떻게 다른가?
  • '스프라우트 기법'과 '랩 기법'을 장기간 지속적으로 적용할 경우 발생하는 아키텍처적 부작용은 무엇이며, 이를 근본적으로 해결하기 위한 후속 리팩토링 전략은 무엇인가?
  • 문서화되지 않은 레거시 시스템에 특성화 테스트(Characterization Test)를 적용할 때, 의도된 비즈니스 로직과 단순한 버그를 어떻게 구분하여 테스트로 승인할 것인가?
  • 거대언어모델(LLM)을 기반으로 한 AI 코딩 도구가 의존성이 극도로 얽힌 레거시 환경에서 접점을 찾아내고 테스트 코드를 자동 생성하는 데 어디까지 기여할 수 있는가?
  • 마이클 페더스가 제안한 "의존성 제거 후 테스트 작성"의 일반적 알고리즘을 적용조차 하기 힘든 '거대 괴물 메서드(Monster Method)'를 분해하는 효과적인 단계적 접근법은 무엇인가?

Practical Application Contexts

  • Implementation: 긴급한 릴리즈 일정이 잡힌 상황에서 레거시 로직 사이에 새로운 요구사항을 구현해야 할 때, 기존 함수를 오염시키지 않고 스프라우트(Sprout) 메서드를 활용하여 격리된 테스트 코드를 동반한 구현을 진행합니다.
  • System Design: 초기 시스템 설계 시, 나중에 레거시 코드가 되더라도 쉽게 테스트를 붙일 수 있도록 인스턴스 주입이 용이한 객체 접점(Object Seams)을 적극 반영한 의존성 역전 구조를 설계합니다.
  • Operation / Maintenance: 유지보수 담당자가 인수인계받지 못한 오래된 모듈을 분석할 때, 스크래치 리팩토링 기법을 적용해 로컬 환경에서 코드를 해체하고 재조립하며 도메인 지식을 확보한 뒤, 코드를 원상 복구합니다.
  • Learning Path: 리팩토링 기법(예: 마틴 파울러의 기법)을 실제 레거시 프로젝트에 적용하기 전, 마이클 페더스의 "레거시 코드 다루기"를 먼저 학습하여 안전망(테스트)을 먼저 구축하는 기술을 습득해야 합니다.
  • My Project Relevance: 현재 유지보수 중인 테스트가 전혀 없는 수천 줄의 코드베이스에 리팩토링을 적용하기 전에, 데이터베이스 및 외부 서비스와의 의존성을 끊어내고 특성화 테스트를 구축하는 단계적 접근에 활용할 수 있습니다.

Adjacent Topics

  • Technical Debt (기술 부채)
    • 확장 방향: 레거시 코드가 쌓이게 된 근본 원인이자 결과인 기술 부채를 정량적으로 측정하고, 장기적으로 상환 및 관리하기 위한 조직적, 기술적 접근법으로 개념을 확장합니다.
  • Test-Driven Development (TDD)
    • 확장 방향: 레거시 코드를 수정할 때 TDD의 Red-Green-Refactor 사이클을 부분적으로 차용하는 방법을 넘어, 향후 작성하는 코드가 다시 레거시(테스트 없는 코드)로 전락하지 않게 하는 필수 개발 방론으로 탐구합니다.
  • Code Smells (코드 스멜)
    • 확장 방향: 레거시 코드 내부를 진단할 때, 복잡성과 유지보수 어려움을 암시하는 구조적 지표들(예: 데이터 뭉치, 거대 클래스)을 식별하고 이를 구체적인 리팩토링 카탈로그와 매핑하여 학습을 확장합니다.

Last updated: 2026-05-03