레거시 코드는 '누가 이걸 만들었어?' 하다가 거울 속 내 얼굴을 보게 되는 코드
레거시 코드는 과거의 선택이 현재의 도전이 된 코드

개발자라면 누구나 한 번쯤 이런 경험이 있을 겁니다. "이 코드는 누가 짰지?" 하고 깃 블레임을 돌렸더니, "어라, 내가 짰네…"
이제는 손대기 무서운 코드가 되어버린 레거시 코드는, 마치 오래된 라면 수프처럼 없으면 안 되지만 뜯으면 뭔가 찝찝한 존재입니다. 하지만 이것도 결국 누군가의 땀과 눈물로 만들어진 유산이죠. 이번 글에서는 레거시 코드의 정체와 문제점, 이를 어떻게 다루면 좋을지, 그리고 우리에게 주는 교훈까지 재미있고 실용적으로 풀어보겠습니다.
목차
레거시 코드란 무엇인가
단순히 '오래된 코드'가 아니다
레거시 코드(Legacy Code)는 단순히 오래된 코드를 의미하지 않습니다. 작성된 지 한 달밖에 안 됐어도 레거시일 수 있고, 10년 된 코드여도 잘 관리되어 있다면 레거시가 아닐 수 있습니다. 대체로 다음과 같은 특징을 가진 코드를 레거시라고 부릅니다.
- 테스트가 없다 – 변경하려고 코드를 살짝 건드렸더니 엉뚱한 다른 기능이 망가지는 미스터리가 벌어집니다.
- 문서화가 없다 – 코드를 짠 사람만 알던 비밀은 이미 역사 속으로 사라졌습니다.
- 복잡하고 이해하기 어렵다 – 한 줄 읽었는데 이해하는 데 하루가 걸리는 코드입니다.
- "잘 돌아가니까 건들지 마"라는 무언의 경고가 달려 있습니다.
유명한 정의도 하나 짚고 넘어가겠습니다. 『레거시 코드 활용 전략(Working Effectively with Legacy Code)』의 저자 마이클 페더스(Michael Feathers)는 "레거시 코드란 테스트가 없는 코드"라고 단언했습니다. 테스트가 없으면 코드가 의도대로 동작하는지 확인할 수 없고, 그래서 변경이 두려워지기 때문입니다. 정리하면 레거시 코드의 진짜 정의는 유지보수가 어렵고, 현재 기술 스택이나 팀의 이해와 잘 맞지 않는 코드입니다. 즉, 시대를 잘못 만난 코드라고 볼 수 있죠.
왜 레거시 코드는 우리를 괴롭히는가
레거시 코드는 마치 집 안 구석에 쌓여 있는 오래된 물건과 같습니다. 분명히 쓸모는 있는데, 치우려고 하면 일이 너무 커져서 결국 포기하게 되는 물건 말이죠. 레거시 코드가 개발자를 힘들게 하는 이유는 크게 세 가지입니다.
문제점 1 – 변경의 두려움
레거시 코드는 보통 테스트 코드가 없기 때문에, 한 줄을 바꿔도 다른 부분이 깨질 확률이 높습니다. 더 무서운 건, 깨졌다는 사실조차 배포 후에야 알게 된다는 점입니다.
def calculate_discount(price, discount_rate):
return price - (price * discount_rate) # 여기 문제 생기면 다 망함
# 테스트 코드가 없어서 이걸 건드리면 시스템 전체가 터질지도 모름...
문제점 2 – 복잡성의 늪
레거시 코드는 한 번 작성된 이후 여러 명의 개발자가 그 위에 계속 얹어 쌓아 올린 결과물입니다. 이 과정에서 로직이 꼬이고, 구조가 복잡해지며, 결국 아무도 전체를 이해하지 못하는 코드가 됩니다. 처음엔 깔끔했던 함수가 if문과 예외 처리로 누더기가 되는 건 시간문제죠.
문제점 3 – 기술 부채
기술 부채(Technical Debt)란 현재의 편의를 위해 미래의 문제를 떠넘기는 것입니다. 레거시 코드는 대부분 "일단 동작하게 만들고 나중에 고치자"라는 마음으로 만들어졌지만, 그 "나중"이 결국 지금이 된 거죠. 빚이 그렇듯 기술 부채도 방치하면 이자가 붙습니다. 처음엔 한 시간이면 고칠 코드가, 1년 뒤엔 일주일이 걸리는 코드가 됩니다.
레거시 코드 해결법 – 괴물을 길들이는 기술
1. 변경 전에 테스트부터 (특성화 테스트)
레거시 코드를 변경하기 전에, 반드시 테스트 코드를 먼저 작성하세요. 테스트는 레거시 코드와의 싸움에서 가장 강력한 방패입니다. 특히 코드가 '무엇을 해야 하는지' 모를 때는, 일단 현재 동작하는 그대로를 고정하는 특성화 테스트(Characterization Test)를 작성하는 것이 정석입니다. 이렇게 하면 리팩터링 후에도 동작이 그대로인지 비교할 수 있습니다.
# 기존 함수
def calculate_total(price, tax):
return price + tax
# 테스트 코드 (현재 동작을 고정한다)
def test_calculate_total():
assert calculate_total(100, 10) == 110
assert calculate_total(200, 20) == 220
2. 리팩터링은 작은 단위로
레거시 코드는 한꺼번에 다 고치려다 보면 더 큰 문제가 생깁니다. 테스트로 안전망을 친 뒤, 작은 단위로 나누어 조금씩 리팩터링하세요. 아래는 if/elif로 분기하던 코드를 딕셔너리 디스패치로 정리한 예시입니다.
# 리팩터링 전
def process_order(order):
if order["type"] == "physical":
print("Processing physical product")
elif order["type"] == "digital":
print("Processing digital product")
# 리팩터링 후
def process_physical_order():
print("Processing physical product")
def process_digital_order():
print("Processing digital product")
def process_order(order):
order_processors = {
"physical": process_physical_order,
"digital": process_digital_order,
}
order_processors[order["type"]]()
3. 기술 빚을 갚는 습관 만들기
레거시 코드는 기술 빚의 결과물입니다. 빚을 갚기 위해서는 주기적으로 코드를 검토하고, 오래된 부분을 조금씩 개선해야 합니다. 한 번에 큰 시간을 내기 어렵다면, 기능을 수정하러 들어간 김에 그 주변만 깔끔하게 다듬고 나오는 '보이스카우트 규칙(들어왔을 때보다 깨끗하게 두고 나가기)'을 추천합니다. 여기에 더해 다른 개발자와 함께 보는 코드 리뷰, 일정에 미리 넣어두는 정기적인 리팩터링 시간이 큰 도움이 됩니다.
4. 문서화와 대화
레거시 코드를 다룰 때는 팀원들과의 소통이 중요합니다. 특히 "왜 이렇게 고쳤는지"를 커밋 메시지나 PR 설명, 코드 주석으로 남기면, 미래의 자신과 동료에게 든든한 이정표가 됩니다. 코드는 '무엇을' 하는지 보여주지만, '왜' 그렇게 했는지는 기록하지 않으면 아무도 모릅니다.
해결법 요약
| 전략 | 핵심 | 기대 효과 |
|---|---|---|
| 특성화 테스트 | 현재 동작을 그대로 고정 | 변경 두려움 해소 |
| 작은 단위 리팩터링 | 한 번에 조금씩 | 복잡성 점진적 해소 |
| 기술 빚 갚기 | 정기 리뷰·보이스카우트 규칙 | 부채 이자 축소 |
| 문서화와 대화 | '왜'를 기록 | 지식 유실 방지 |
레거시 코드가 주는 교훈
레거시 코드는 단순히 "나쁜 코드"가 아닙니다. 관점을 바꾸면 세 가지 의미를 발견할 수 있습니다. 첫째, 역사의 산물입니다. 누군가는 이 코드를 통해 실제 문제를 해결하고 가치를 창출했습니다. 둘째, 배움의 기회입니다. 레거시 코드를 다루면서 설계, 디버깅, 리팩터링 역량이 크게 향상됩니다. 셋째, 협업의 중요성을 일깨워 줍니다. 레거시 코드는 혼자 해결하기 어려운 경우가 많아, 팀워크가 필수입니다.
마무리
레거시 코드는 개발자에게 도전 과제이자 성장의 기회입니다. 괴물처럼 보일지 몰라도, 올바른 접근법과 마음가짐으로 다가간다면 그것은 당신을 더 뛰어난 개발자로 만들어 줄 것입니다. 다음에 레거시 코드를 만난다면, 이렇게 말해보세요.
"너와 내가 함께 성장할 시간이야!"
물론 현실에서 실제로 마주치면 겁부터 먹을 게 뻔합니다. 하지만 이렇게 글로 정리하다 보니, 저 역시 실무에서 레거시 코드를 만났을 때 도망치지 않고 한 걸음씩 개선해 나가야겠다는 다짐을 하게 됩니다. 여러분의 레거시 코드와의 싸움에도 건투를 빕니다.
