레슨 11: 소프트웨어 유지보수와 진화(Software Maintenance and Evolution)
레슨 11: 소프트웨어 유지보수와 진화(Software Maintenance and Evolution)¶
소프트웨어는 물리적 기계처럼 마모되지 않습니다. 서버에 저장된 데이터베이스 바이너리는 마찰, 부식, 피로를 경험하지 않습니다. 그러나 소프트웨어는 여전히 "노후화"됩니다. 비트가 열화되는 것이 아니라, 주변 세상이 변하기 때문입니다. 운영 체제가 진화하고, 보안 취약점이 발견되고, 비즈니스 규칙이 변화하고, 사용자 기대치가 높아집니다. 초기 릴리스 이후 소프트웨어를 유지하고 진화시키는 방법을 이해하는 것은 소프트웨어 공학에서 경제적으로 가장 중요한 기술 중 하나입니다.
난이도: ⭐⭐⭐
선수 학습: - 소프트웨어 개발 수명 주기 개념 (레슨 02) - 기본 소프트웨어 설계 원칙 - 버전 관리 (Git)에 대한 친숙함
학습 목표: - 네 가지 유형의 소프트웨어 유지보수를 구분하고 각각의 적용 시기 파악 - 레만의 법칙(Lehman's Laws)과 장기 시스템에 대한 시사점 설명 - 레거시 시스템 현대화 전략 평가 - 동작을 변경하지 않고 코드를 개선하는 리팩토링 기법 적용 - 역공학, 소프트웨어 노후화, 마이그레이션 전략 이해 - API 및 서비스에 적합한 폐기(Deprecation) 정책 선택 - 핵심 지표를 사용한 유지보수 건전성 측정
1. 소프트웨어 유지보수의 규모¶
소프트웨어 유지보수(Software Maintenance)는 결함을 수정하거나, 성능을 개선하거나, 변경된 환경에 적응하기 위해 납품 후 소프트웨어 제품을 수정하는 것입니다 (ISO 14764). 이는 부차적인 관심사가 아니라 소프트웨어 수명 주기를 지배합니다.
소프트웨어 수명 주기 전반에 걸친 일반적인 비용 분포:
| 단계 | 전체 수명 비용의 대략적 비율 |
|---|---|
| 요구사항 및 설계 | 5–10% |
| 코딩 및 단위 테스트 | 10–15% |
| 통합 및 시스템 테스트 | 10–15% |
| 유지보수 | 60–80% |
이러한 불균형을 유발하는 여러 요소: - 성공한 시스템은 사용자, 기능, 복잡성이 증가함 - 인력 이직으로 인해 유지보수 담당자가 원래 코드를 작성하지 않은 경우가 많음 - 비즈니스가 진화함에 따라 요구사항도 계속 진화 - 기술 플랫폼이 애플리케이션 아래에서 변화
2. 소프트웨어 유지보수 유형 (ISO 14764)¶
ISO 14764은 네 가지 유지보수 카테고리를 정의합니다. 어떤 유형의 작업을 하고 있는지 이해하면 자원을 올바르게 배분하고 적절한 기대치를 설정하는 데 도움이 됩니다.
2.1 수정 유지보수(Corrective Maintenance)¶
목표: 잘못된 동작이나 시스템 장애를 유발하는 결함을 수정합니다.
수정 유지보수는 사용자가 보고하거나 모니터링으로 감지된 버그를 처리합니다. 사소한 한 줄 오타 수정부터 분산 시스템의 경쟁 조건에 대한 수주일에 걸친 조사까지 다양합니다.
# 버그 보고: 주문이 없는 사용자에게 0으로 나누기 발생
# 수정 전 (버그 있음)
def average_order_value(user_id: int) -> float:
orders = get_orders(user_id)
total = sum(o.value for o in orders)
return total / len(orders) # 주문이 없으면 ZeroDivisionError
# 수정 후 (수정 유지보수)
def average_order_value(user_id: int) -> float:
orders = get_orders(user_id)
if not orders:
return 0.0
total = sum(o.value for o in orders)
return total / len(orders)
2.2 적응 유지보수(Adaptive Maintenance)¶
목표: 변경된 환경에서 작동하도록 소프트웨어를 수정합니다.
환경에는 운영 체제, 하드웨어, 데이터베이스 엔진, 서드파티 API, 법적 규정, 회사 정책이 포함됩니다. 이 중 어느 것도 영원히 안정적이지 않습니다.
예시: - TLS 1.0이 폐기된 후 TLS 인증서 처리 업데이트 - Python 2에서 Python 3으로 마이그레이션 - 초기 배포 후 도입된 GDPR 요건에 적응 - 폐기된 결제 게이트웨이에서 새 공급업체로 전환
2.3 완전화 유지보수(Perfective Maintenance)¶
목표: 성능, 유지보수성 개선 또는 사용자가 요청한 새로운 기능 추가.
이것은 종종 가장 자원 집약적인 카테고리입니다. 시스템이 그 가치를 증명함에 따라 이해관계자들은 기능 향상을 요청합니다. 완전화 유지보수와 신규 개발 사이의 경계는 모호하지만, 핵심적인 차이점은 시스템이 이미 존재하고 운영 중이라는 것입니다.
2.4 예방 유지보수(Preventive Maintenance)¶
목표: 문제가 발생하기 전에 신뢰성, 유지보수성, 보안을 개선하여 미래의 실패를 줄입니다.
예시: - 순환 복잡도를 줄이기 위해 뒤엉킨 모듈 리팩토링 - 테스트되지 않은 중요 경로에 자동화 테스트 추가 - 만료되기 전에 암호화 키 교체 - 알려진 취약점이 적극적으로 악용되기 전에 의존성 업그레이드
| 유형 | 트리거 | 예시 |
|---|---|---|
| 수정 | 버그 보고 또는 장애 | 운영 환경의 null 포인터 예외 수정 |
| 적응 | 환경 변화 | Python 3.12로 마이그레이션 |
| 완전화 | 사용자 또는 비즈니스 요청 | 보고서에 CSV 내보내기 추가 |
| 예방 | 선제적 분석 | 2,000줄 모놀리식 함수 리팩토링 |
실제로 대부분의 유지보수 백로그에는 네 가지 유형이 혼재합니다. 순수하게 수정 작업만 하는 조직은 반응적이고 취약합니다. 예방 유지보수에 투자하는 것이 성숙한 엔지니어링 조직의 특징입니다.
3. 레만의 소프트웨어 진화 법칙(Lehman's Laws)¶
Meir "Manny" Lehman과 László Bélády는 1970–1990년대 IBM에서 대형 소프트웨어 시스템을 연구하여 수십 년의 소프트웨어 역사에 걸쳐 놀랍도록 잘 유지되는 경험적 법칙을 도출했습니다.
| 법칙 | 이름 | 내용 |
|---|---|---|
| I | 지속적 변화(Continuing Change) | E-type 시스템은 지속적으로 적응하지 않으면 점점 덜 만족스러워짐 |
| II | 증가하는 복잡성(Increasing Complexity) | 시스템이 진화함에 따라 복잡성을 줄이기 위한 작업을 하지 않으면 복잡성이 증가함 |
| III | 자기 규제(Self Regulation) | 전체 시스템 속성 (크기, 활동 속도)은 자기 조절적이며 통계적으로 불변 |
| IV | 조직적 안정성 보존(Conservation of Organizational Stability) | 진화하는 시스템의 평균 효과적 전체 활동 속도는 불변 |
| V | 친숙성 보존(Conservation of Familiarity) | 연속적인 릴리스의 내용은 통계적으로 불변 |
| VI | 지속적 성장(Continuing Growth) | 수명 주기 동안 사용자 만족도를 유지하기 위해 기능적 내용이 증가해야 함 |
| VII | 품질 저하(Declining Quality) | 운영 환경 변화에 대해 엄격하게 유지하고 적응하지 않으면 품질이 저하되는 것으로 나타남 |
| VIII | 피드백 시스템(Feedback System) | E-type 진화 프로세스는 다단계, 다중 루프, 다중 에이전트 피드백 시스템 |
실질적인 시사점:
- 법칙 I: 조직이 성공적인 시스템에 대한 투자를 중단하면 시스템이 침식됩니다. "동결하고 잊기"는 안정적인 전략이 아닙니다.
- 법칙 II: 복잡성은 엔트로피의 자연스러운 방향입니다. 예방적 리팩토링, 아키텍처 검토, 기술 부채 감소가 필수적인 반대 압력입니다.
- 법칙 VII: 품질 저하가 예외가 아니라 기본 궤적입니다. 품질은 적극적이고 지속적인 투자를 필요로 합니다.
4. 레거시 시스템(Legacy Systems)¶
레거시 시스템(Legacy System)은 비즈니스에 중요하지만 나이, 기술, 설계, 문서 격차로 인해 변경하기 어려운 시스템입니다.
4.1 레거시 시스템의 특성¶
- 구식 기술로 구축 (COBOL, Visual Basic 6, ASP classic)
- 자동화 테스트 커버리지가 거의 없거나 전혀 없음
- 빈약하거나 누락된 문서
- 변경을 위험하게 만드는 구성요소 간의 높은 결합도
- 장기 근속 직원만 알고 있는 시스템 (또는 이미 퇴직한 직원)
- 높은 비즈니스 가치 — 시스템이 실제로 작동하고 신뢰됨
마지막 포인트가 중요합니다. 레거시 시스템은 종종 조롱받지만, 수십 년에 걸쳐 인코딩된 비즈니스 로직을 나타냅니다. 은행을 운영하는 메인프레임 급여 시스템이 40년이 되었을 수 있지만, 매일 수십억 달러를 올바르게 처리합니다.
4.2 현대화 전략¶
| 전략 | 설명 | 리스크 | 비용 |
|---|---|---|---|
| 그냥 두기 | 현상 유지 수용 | 누적됨 | 지금은 낮음, 나중에 높음 |
| 래핑(Wrap) | 레거시 코어 주위에 모던 API 레이어 추가 | 낮음 | 중간 |
| 확장 | 코어를 건드리지 않고 새 기능 추가 | 중간 | 중간 |
| 재작성 | 처음부터 대체품 구축 | 높음 | 매우 높음 |
| 교체 | COTS 패키지를 구매하여 교체 | 중간 | 높음 |
| 마이그레이션 | 점진적으로 모던 플랫폼으로 이동 | 낮음–중간 | 중간–높음 |
교살자 무화과 패턴(Strangler Fig Pattern) (Martin Fowler가 대중화)은 선호되는 점진적 접근법입니다. 나무를 감싸며 자라서 결국 나무를 대체하는 덩굴에서 이름을 따왔습니다:
1단계: 가로채기 2단계: 리다이렉션 3단계: 교살
(Intercept) (Redirect) (Strangle)
요청 요청 요청
│ │ │
▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────────┐
│레거시│ │라우터│ │새 시스템 │
│시스템│ └──┬───┘ └──────────┘
└──────┘ 레거시│ 새 시스템 (레거시 은퇴)
┌────┘ └──────┐
┌──────┐ ┌──────────┐
│레거시│ │새 시스템 │
│(일부)│ │(성장 중) │
└──────┘ └──────────┘
교살자 무화과 마이그레이션을 통해 가능한 것: - 빅뱅 전환 없음 (모든 마이그레이션에서 단일 최고 위험 이벤트) - 검증을 위해 구시스템과 새 시스템을 병렬로 실행 - 가치의 점진적 납품 - 문제 발생 시 일시 중지하거나 되돌릴 수 있는 능력
5. 리팩토링 vs. 재작성¶
리팩토링(Refactoring)은 관찰 가능한 외부 동작을 변경하지 않고 코드의 내부 구조를 변경하여 설계를 개선하는 것입니다. 이는 규율 있고, 점진적이며, 테스트로 뒷받침될 때 안전합니다.
재작성(Rewriting)은 기존 코드를 버리고 처음부터 구축하는 것입니다. 가끔은 필요하지만 팀이 예상하는 것보다 보통 더 위험하고 비쌉니다.
5.1 재작성에 반대하는 주장¶
Joel Spolsky의 유명한 글 "절대로 하지 말아야 할 것들, 1부" (2000)는 처음부터 재작성하는 것이 거의 항상 실수라고 주장합니다. 이유:
- "못생긴" 기존 코드에는 어떤 명세서에도 없는 수천 개의 버그 수정이 포함되어 있음
- 재작성은 예상보다 3–5배 더 걸림
- 재작성이 출시될 때쯤 원래 시스템은 계속 진화했으므로 재작성이 이미 뒤처짐
- 새로운 팀은 다른 (반드시 더 나은 것은 아닌) 설계 실수를 함
5.2 재작성이 정당화될 수 있는 경우¶
- 원래 기술 플랫폼이 마이그레이션 경로 없이 지원 종료
- 코드베이스에 테스트 커버리지가 없고 안전하게 리팩토링하기에는 너무 얽혀 있음
- 도메인 모델이 근본적으로 잘못되어 필요한 기능을 방해
- 비즈니스 요구사항이 너무 완전히 변경되어 현재 설계가 자산이 아닌 장애물
5.3 일반적인 리팩토링 패턴¶
# 메서드 추출(Extract Method): 긴 메서드를 명명된 하위 함수로 분해
# 수정 전
def process_order(order):
# 유효성 검사
if order.quantity <= 0:
raise ValueError("Quantity must be positive")
if order.product_id not in get_valid_products():
raise ValueError("Invalid product")
# 가격 계산
base_price = get_price(order.product_id)
discount = 0.1 if order.quantity > 100 else 0
total = base_price * order.quantity * (1 - discount)
# 저장
db.save(Order(order.product_id, order.quantity, total))
return total
# 수정 후 (리팩토링됨)
def process_order(order):
validate_order(order)
total = calculate_total(order)
save_order(order, total)
return total
def validate_order(order):
if order.quantity <= 0:
raise ValueError("Quantity must be positive")
if order.product_id not in get_valid_products():
raise ValueError("Invalid product")
def calculate_total(order) -> float:
base_price = get_price(order.product_id)
discount = 0.1 if order.quantity > 100 else 0
return base_price * order.quantity * (1 - discount)
def save_order(order, total: float):
db.save(Order(order.product_id, order.quantity, total))
리팩토링 규칙: 동작이 보존됨을 확인하는 테스트 스위트 없이는 절대 리팩토링하지 마세요.
6. 역공학과 프로그램 이해(Reverse Engineering and Program Comprehension)¶
작성하지 않은 코드 (또는 오래 전에 작성했다가 잊어버린 코드)를 유지보수할 때, 프로그램 이해(Program Comprehension) — 코드가 무엇을 하는지, 왜 그런 방식인지, 변경이 어떻게 전파될지를 이해하는 것 — 을 수행해야 합니다.
6.1 역공학 기법¶
- 정적 분석(Static Analysis): 실행 없이 코드, 호출 그래프, 의존성 맵 읽기
- 동적 분석(Dynamic Analysis): 시스템을 실행하고 동작 관찰 (프로파일러, 디버거, 로그 분석)
- 문서 복구(Documentation Recovery): 설계 근거를 위한 커밋 히스토리, 이슈 트래커, 오래된 이메일 마이닝
- 개념 위치 파악(Concept Location): 어떤 코드 모듈이 어떤 비즈니스 개념을 구현하는지 식별
6.2 프로그램 이해를 위한 도구¶
| 카테고리 | 도구 |
|---|---|
| 정적 분석 | SonarQube, ESLint, PyLint, Understand |
| 호출 그래프 생성 | Doxygen, pycallgraph, CodeScene |
| 의존성 시각화 | Sourcegraph, IntelliJ dependency view |
| 런타임 프로파일링 | py-spy, perf, async-profiler |
| Git 히스토리 마이닝 | git log --follow, git blame, GitLens |
7. 소프트웨어 노후화와 회춘(Software Aging and Rejuvenation)¶
7.1 소프트웨어 노후화(Software Aging)¶
David Parnas는 1994년에 소프트웨어 노후화 개념을 소개했습니다. 소프트웨어가 노후화되는 이유:
- 표류(Drift): 환경과 요구사항이 변함에 따라 시스템의 설계 가정이 점점 덜 유효해짐
- 누적(Accumulation): 각 패치와 빠른 수정은 복잡성, 미사용 코드, 불일치를 남김
- 무지(Ignorance): 왜 그런 방식인지에 대한 지식이 팀에서 점차 사라짐
소프트웨어 노후화 증상: - 새로운 기능 구현에 걸리는 시간 증가 - 오래된 모듈에서 높은 결함률 - 코드베이스의 특정 부분을 건드리는 것에 대한 두려움 - 특정 하위 시스템을 이해하는 사람이 한두 명뿐 - 빌드 및 테스트 사이클이 점점 길어짐
7.2 소프트웨어 회춘(Software Rejuvenation)¶
회춘(Rejuvenation)은 소프트웨어 노후화를 역전시키기 위한 계획적이고 지속적인 노력입니다:
- 정기적인 리팩토링 스프린트 또는 "기술 부채 스프린트"
- 아키텍처 검토 및 현대화 로드맵
- 페어 프로그래밍과 문서화를 통한 지식 이전
- 계획된 일정으로 오래된 프레임워크 및 라이브러리 교체
- 피드백 지연을 줄이기 위한 빌드 및 테스트 인프라 개선
유용한 경험칙: 지속적인 회춘에 엔지니어링 역량의 10–20%를 할당하세요. 이를 건너뛰는 팀은 결국 "대규모 재작성" 위기에 직면합니다.
8. 기술 부채(Technical Debt)¶
Ward Cunningham은 1992년에 기술 부채(Technical Debt) 라는 용어를 만들어, 더 오래 걸리는 더 나은 접근법 대신 지금 쉬운 해결책을 선택함으로써 발생하는 재작업의 암묵적 비용을 설명했습니다. 금융 부채처럼 기술 부채는 시간이 지남에 따라 이자가 쌓입니다 — 부채가 많은 영역에서의 모든 미래 변경은 그렇지 않을 때보다 더 많은 비용이 듭니다.
8.1 기술 부채의 유형¶
Martin Fowler의 기술 부채 사분면(Technical Debt Quadrant)은 의도와 신중함으로 부채를 분류합니다:
무모한 신중한
│ │
의도적 "설계할 시간이 "지금 출시해야 하고
없다" 나중에 처리해야 한다"
│ │
─────────────────────┼─────────────────┼─────────────
│ │
비의도적 "레이어링이란 "이제 어떻게 했어야
무엇인가?" 했는지 알겠다"
| 사분면 | 예시 | 적절한 대응 |
|---|---|---|
| 의도적 + 무모한 | "빠르게 가기" 위해 설계 건너뛰기 | 중지 — 이것은 거의 효과가 없음 |
| 의도적 + 신중한 | 출시 마감일을 맞추기 위해 설정값 하드코딩 | 추적된 상환 계획으로 수용 |
| 비의도적 + 무모한 | 경험 부족으로 인한 설계 안티패턴 | 팀 기술에 투자, 유지보수 중 수정 |
| 비의도적 + 신중한 | 구현 후 더 나은 설계 인식 | 정상적인 학습; 경제적으로 정당화될 때 리팩토링 |
8.2 기술 부채 레지스터(Technical Debt Register)¶
리스크 레지스터와 유사하게, 기술 부채 레지스터는 부채를 가시적이고 실행 가능하게 만듭니다:
| ID | 위치 | 설명 | 영향 | 작업량 | 우선순위 |
|------|-------------------|--------------------------------------|------------|---------|----------|
| TD01 | auth/session.py | Pickle 직렬화; 업그레이드 차단 | 높음 | 3일 | P1 |
| TD02 | reports/generate | 2,400줄 갓 함수 | 높음 | 5일 | P1 |
| TD03 | models/product.py | 타입 어노테이션 없음 | 중간 | 2일 | P2 |
| TD04 | tests/integration | 불안정한 sleep() 기반 비동기 테스트 | 중간 | 3일 | P2 |
| TD05 | frontend/styles | 800줄 CSS 파일, BEM 없음 | 낮음 | 4일 | P3 |
8.3 기술 부채 관리¶
효과적인 부채 관리는 상환과 기능 납품의 균형을 맞춥니다:
- 보이스카우트 규칙(The Boy Scout Rule): "코드를 찾았을 때보다 항상 조금 더 깨끗하게 남기세요." 작은 지속적인 개선이 시간이 지남에 따라 복리로 쌓입니다.
- 전담 부채 스프린트: 분기당 하나의 스프린트를 전적으로 부채 감소에 할당하세요. 이를 절대로 하지 않는 팀은 결국 위기 지점에 도달합니다.
- 완료 정의(Definition of Done): 팀의 완료 정의에 부채 생성 기준을 포함하세요 (예: "검토 없이 50줄 이상의 새로운 함수 없음").
- 부채 예산: 일부 팀은 최대 부채 대 기능 비율을 설정합니다 — 4번의 기능 작업 스프린트마다 최소 1번의 부채 감소 스프린트.
# 코드 리뷰 체크리스트 항목: 새로운 기술 부채 표시
# 모범 사례: PR 설명에서 추적
## PR #847: 대량 내보내기 기능 추가
### 생성된 기술 부채
- CSV 직렬화가 현재 스키마에 하드코딩되어 있습니다.
기술 부채 레지스터에 TD-023으로 추적됨.
Q3에 직렬화 인터페이스로 추상화 계획.
### 감소된 기술 부채
- `ReportBuilder` 클래스 리팩토링 (800줄 → 4개 클래스 × 약 200줄)
TD-019 종료.
8.4 부채로 인한 속도 저하¶
적극적인 부채 관리 없이 팀은 특징적인 패턴을 경험합니다:
팀 속도
│
100% │████████████
90% │████████████████
80% │████████████████████
70% │████████████████████████▌
60% │████████████████████████████
50% │████████████████████████████████
└────────────────────────────────────▶ 시간 (분기)
Q1 Q2 Q3 Q4 Q5 Q6 Q7 Q8
부채 감소 없이 높은 코드 변경률을 가진 코드베이스에서 속도는 분기당 약 10% 감소합니다.
이 패턴은 1년차에 스프린트당 40 스토리 포인트를 제공하던 팀이 3년차에 18 스토리 포인트를 제공하는 이유를 설명합니다 — 팀이 덜 유능해서가 아니라, 이제 각 변경이 누적된 기술 부채의 복잡한 웹을 헤쳐나가야 하기 때문입니다.
10. 마이그레이션 전략(Migration Strategies)¶
한 플랫폼, 데이터베이스, 또는 아키텍처에서 다른 것으로 이동할 때, 세 가지 주요 전략이 존재합니다:
8.1 빅뱅 마이그레이션(Big Bang Migration)¶
단일 전환 이벤트에서 이전 시스템을 새 시스템으로 교체합니다.
구 시스템 ──────────────────────────────── 전환 ──────── 은퇴
새 시스템 ────────────────
- 장점: 관리하기 간단; 장기 병렬 운영 없음
- 리스크: 새 시스템이 첫날 결함이 있으면 전체 사용자 집단이 즉시 영향받음
- 적합한 경우: 소규모 시스템, 낮은 트래픽, 우수한 테스트 커버리지를 갖춘 잘 이해된 도메인
8.2 단계적 마이그레이션(Phased Migration)¶
기능을 모듈별로 마이그레이션하여 구시스템과 새 시스템을 병렬로 실행합니다.
모듈 A: 구 ──── 새
모듈 B: 구 ──── 새
모듈 C: 구 ──── 새
- 장점: 단계별 낮은 리스크; 복잡한 부분을 마이그레이션하기 전에 학습 가능
- 리스크: 이중 시스템 데이터 동기화 복잡성; 전체 일정 연장
- 적합한 경우: 대부분의 중대형 시스템 (기본 권장 사항)
8.3 병렬 운영(Parallel Operation)¶
구시스템과 새 시스템을 동시에 실행하면서 결과를 비교합니다.
모든 요청 ──┬──▶ 구 시스템 ──▶ 권위 있는 응답
└──▶ 새 시스템 ──▶ 비교를 위해 로깅
- 장점: 트래픽 전환 전에 정확성 검증
- 리스크: 인프라 비용 두 배; 복잡한 비교 로직
- 적합한 경우: 고위험 시스템 (재무 계산, 의료 기록)
11. 폐기 정책(Deprecation Policies)¶
폐기(Deprecation)는 API, 기능 또는 서비스를 단계적으로 폐지하는 프로세스입니다. 잘 관리된 폐기는 파괴적 변경이 소비자를 놀라게 하는 것을 방지합니다.
9.1 폐기 모범 사례¶
import warnings
def legacy_calculate_discount(order_total: float, user_tier: str) -> float:
"""
주문 총액과 사용자 등급에 따른 할인 계산.
.. deprecated:: 2.4.0
대신 :func:`calculate_discount_v2`를 사용하세요. 이 함수는
버전 4.0.0에서 제거될 예정입니다 (2026-Q1 예정).
"""
warnings.warn(
"legacy_calculate_discount is deprecated and will be removed in v4.0.0. "
"Use calculate_discount_v2() instead.",
DeprecationWarning,
stacklevel=2,
)
return order_total * 0.1 if user_tier == "premium" else 0.0
폐기 타임라인 가이드라인:
| 시스템 유형 | 최소 폐기 기간 |
|---|---|
| 내부 라이브러리 (단일 팀) | 1 스프린트에서 1 분기 |
| 내부 API (여러 팀) | 2–4 분기 |
| 공개 API (외부 개발자) | 최소 1–2년 |
| 플랫폼/언어 기능 | 2–5년 |
폐기를 다음을 통해 전달하세요: 변경 로그, 컴파일러/런타임 경고, 개발자 뉴스레터, 마이그레이션 가이드.
12. 소프트웨어 유지보수의 안티패턴(Anti-Patterns)¶
일반적인 실패 패턴을 인식하면 팀이 선제적으로 피할 수 있습니다.
12.1 용암층 안티패턴(The Lava Layer Anti-Pattern)¶
각 세대의 개발자가 오래된 코드 위에 새로운 레이어를 추가하지만 구식 레이어를 제거하지 않아 다양한 기술과 패러다임의 "용암층" 스택을 만들어냅니다:
┌─────────────────────────────────────┐
│ Vue.js 3 components (2025) │ ← 현재 팀
├─────────────────────────────────────┤
│ Angular 1.x controllers (2019) │ ← 이전 팀
├─────────────────────────────────────┤
│ jQuery widgets (2015) │ ← 그 이전 팀
├─────────────────────────────────────┤
│ Legacy server-rendered HTML (2011) │ ← 원래 팀
└─────────────────────────────────────┘
증상: 여러 JavaScript 프레임워크가 공존; 새 개발자가 어떤 패턴을 따라야 할지 이해 불가; 여러 렌더링 시스템이 동시에 실행되어 성능 저하.
해결책: 각 오래된 레이어에 대해 커밋된 종료 날짜로 폐기 계획을 수립하세요. 이전 스타일로 새 기능을 추가하지 마세요.
12.2 "고장 나지 않았으면 건드리지 마라" 함정¶
시스템이 현재 작동하기 때문에 필요한 업데이트에 대한 저항. 이는 다음에 가장 위험합니다: - 보안 패치 ("아직 침해당한 적 없다") - 의존성 업그레이드 ("이전 버전이 아직 실행된다") - 아키텍처 현대화 ("왜 작동하는 것을 고치는가?")
리스크는 중요한 취약점이 악용되거나 주요 버전 점프로 인해 점진적으로 수행했을 때보다 업그레이드가 10배 더 어려워질 때까지 보이지 않게 누적됩니다.
12.3 영웅적 유지보수(Heroic Maintenance)¶
"시스템을 아는" 단일 엔지니어가 모든 운영 인시던트를 처리합니다. 이는 다음을 만듭니다: - 단일 실패 지점 (그들이 퇴직하거나 아프거나 휴가 중이면 어떻게 되는가?) - 시스템을 개선할 인센티브 없음 (영웅의 지위는 시스템이 복잡하게 유지되는 것에 달려 있음) - 영웅의 번아웃, 동료의 원망
해결책: 런북, 온콜 로테이션, 문서화를 생산하는 비난 없는 사후 검토, 의도적인 지식 공유 세션.
12.4 끓는 개구리(The Boiled Frog)¶
기술 부채가 너무 점진적으로 추가되어 팀이 경보를 울릴 갑작스러운 변화를 인식하지 못합니다. 각 스프린트에 조금 더 많은 부채가 추가되고, 각 인시던트는 빠른 수정으로 해결되다가, 갑자기 팀이 매 스프린트의 80%를 유지보수에 쓰면서 어떻게 그렇게 됐는지 설명할 수 없습니다.
예방: 부채 지표를 지속적으로 추적하세요 (섹션 8.2 참조). 조치를 촉발하는 임계값을 설정하세요: "기술 부채 비율이 10%를 초과하면 7% 미만으로 떨어질 때까지 기능 작업을 일시 중지합니다."
13. 유지보수 지표(Maintenance Metrics)¶
| 지표 | 정의 | 건전한 목표 |
|---|---|---|
| MTTR (평균 수리 시간) | 장애 감지부터 해결까지의 평균 시간 | 가능한 낮게; P1은 시간 단위 |
| 변경 실패율(Change Failure Rate) | 운영 인시던트를 유발하는 배포의 % | < 5% (엘리트 팀: < 1%) |
| 결함 밀도(Defect Density) | KLOC당 또는 기능당 버그 | 시간이 지남에 따라 감소 추세 |
| 기술 부채 비율(Technical Debt Ratio) | 예상 부채 해소 비용 / 총 개발 비용 | < 5% |
| 코드 변동(Code Churn) | 일정 기간 내 변경된 코드의 % | 안정적인 모듈에서의 높은 변동 = 우려 |
| 평균 실패 간격(MTBF) | 운영 장애 사이의 평균 시간 | 증가 추세 |
Accelerate 연구의 DORA 지표 (배포 빈도, 리드 타임, MTTR, 변경 실패율)는 소프트웨어 납품에서 조직 성과의 가장 검증된 예측 변수입니다.
14. 사례 연구: 모놀리스 현대화¶
시나리오: PHP 5.3으로 구축된 15년 된 전자상거래 플랫폼. 모놀리스는 제품 카탈로그, 주문, 결제, 배송을 처리합니다. 테스트, API 레이어가 없고 MySQL 5.5 데이터베이스에 의존합니다. 팀은 제품 개발을 멈추지 않고 현대화하려 합니다.
선택된 전략: 교살자 무화과 + 단계적 마이그레이션
1단계: 래핑 및 안정화 (6개월) - UI (Selenium)를 사용하는 통합 테스트를 추가하여 기존 동작 특성화 - 모놀리스 앞에 라우팅 프록시 (nginx) 도입 - 독립 서비스로 사용자 인증 추출 (가장 낮은 리스크 구성요소)
2단계: 고가치 서비스 추출 (12개월) - 새로운 제품 카탈로그 서비스 (Python/FastAPI) — 카탈로그 트래픽의 10%, 그다음 50%, 그다음 100% 라우팅 - 새로운 주문 서비스 — 전환 전 4주 동안 병렬 모드 (섀도 트래픽)로 실행 - 데이터베이스: CDC (변경 데이터 캡처)가 MySQL 변경사항을 새 PostgreSQL 데이터베이스로 스트리밍
3단계: 폐기 (6개월) - 전담 결제 서비스 (Stripe 기반)로 결제 마이그레이션 - PHP 모놀리스를 모듈별로 은퇴 - 30일 정상 관찰 기간 후 레거시 MySQL 인스턴스 종료
이 사례의 교훈: - 섀도 트래픽 병렬 테스트로 레거시 코드에서 아무도 몰랐던 3개의 계산 불일치 발견 - 인증 추출이 미문서화된 세션 동작으로 인해 예상보다 두 배 걸림 - 팀이 장기 프로젝트 동안 사기를 유지하기 위해 "교살 이벤트" (모듈 은퇴)를 축하
요약¶
소프트웨어 유지보수는 부차적인 것이 아니라 소프트웨어 수명 주기 비용의 대부분을 차지하며 의도적인 전략을 필요로 합니다:
- 네 가지 유지보수 유형: 수정 (버그 수정), 적응 (환경 변화 생존), 완전화 (기능 추가), 예방 (미래 리스크 감소)
- 레만의 법칙: 복잡성은 기본적으로 증가하고 품질은 저하됨; 지속적인 투자만이 유일한 대응책
- 레거시 시스템: 귀중한 인코딩된 비즈니스 로직을 포함; 빅뱅 재작성이 아닌 교살자 무화과로 점진적으로 현대화
- 리팩토링: 재작성의 안전하고 점진적인 대안 — 항상 테스트로 뒷받침
- 마이그레이션 전략: 대부분의 시스템에서 빅뱅보다 단계적 또는 병렬 방식이 더 안전
- 폐기: 소비자에게 충분한 고지; 컴파일러/런타임 경고와 마이그레이션 가이드 사용
- 지표: MTTR과 변경 실패율은 유지보수 건전성의 선행 지표
연습 문제¶
-
유지보수 분류: 다음 각 작업을 수정, 적응, 완전화, 예방으로 분류하고 분류를 정당화하세요: (a) EXIF 데이터가 있는 PNG를 업로드할 때 발생하는 충돌 수정; (b) 벤더가 v1 API를 은퇴한 후 SMS 라이브러리 업데이트; (c) UI에 다크 모드 추가; (d) 3,000줄
OrderProcessor클래스를 더 작은 구성요소로 리팩토링; (e) 마케팅 캠페인 실행 전 API에 속도 제한 추가. -
레만의 법칙 분석: 잘 알려진 오픈 소스 프로젝트 (예: Linux 커널, Python 인터프리터, Firefox)를 선택하세요. 커밋 히스토리와 릴리스 노트를 연구하세요. 레만의 법칙 I (지속적 변화)와 법칙 II (증가하는 복잡성)가 보이는 두 가지 구체적인 예를 식별하세요. 프로젝트는 복잡성 증가에 어떻게 대응했나요?
-
교살자 무화과 설계: HR 기능 (급여, 휴가 관리, 성과 평가, 온보딩)을 처리하는 모놀리식 Java 애플리케이션을 유지보수합니다. 마이크로서비스로 마이그레이션하려 합니다. 교살자 무화과 마이그레이션 계획을 설계하세요: 어떤 서비스를 먼저 추출할지와 이유를 식별하고, 라우팅 프록시 전략을 설명하며, "교살 이벤트" 완료 기준을 정의하세요.
-
리팩토링 실습: 다음 코드를 가져와 최소 세 가지 명명된 리팩토링 패턴을 적용하세요. 관찰 가능한 동작을 보존하세요. 어떤 패턴을 적용했고 왜 적용했는지 문서화하세요.
python def x(d, t, u, s): r = 0 for i in d: if i['type'] == t and i['user'] == u and i['status'] == s: r += i['amount'] if r > 1000: r = r * 0.95 return r -
폐기 정책 설계: 귀하는 인증 API의 내부 서비스 소비자 50명이 있는 회사의 플랫폼 팀입니다.
GET /auth/token?user=X&pass=Y엔드포인트 (보안 위반인 쿼리 파라미터로 자격 증명 전송)를 폐기하고 요청 본문에 자격 증명이 있는POST /auth/token으로 교체해야 합니다. 완전한 폐기 계획을 설계하세요: 타임라인, 커뮤니케이션 전략, 모니터링 접근법, 하드 전환 기준.
더 읽을거리¶
- 도서:
- Feathers, M. (2004). Working Effectively with Legacy Code. Prentice Hall. (테스트되지 않은 코드에 테스트 추가에 관한 결정판 가이드)
- Fowler, M. (2018). Refactoring: Improving the Design of Existing Code, 2nd Ed. Addison-Wesley.
-
Parnas, D. (1994). "Software Aging." Proceedings of ICSE 1994. (개념에 관한 원본 논문)
-
아티클 및 논문:
- Fowler, M. "Strangler Fig Application." martinfowler.com
- Spolsky, J. "Things You Should Never Do, Part I." Joel on Software
-
Forsgren, N., Humble, J. & Kim, G. (2018). Accelerate. IT Revolution. (DORA 지표 연구)
-
표준:
- ISO/IEC 14764:2006 — 소프트웨어 공학: 소프트웨어 수명 주기 프로세스 — 유지보수
- ISO/IEC 25010 — 시스템 및 소프트웨어 품질 모델 (ISO 9126 대체)