레거시 코드의 끊임없는 딜레마와 이를 해결하는 방법

레거시 코드의 끊임없는 딜레마와 이를 해결하는 방법
Cozy CodingPosted On Jul 13, 202411 min read

image

"'탓'은 매우 이상한 선택의 단어이며, 소프트웨어 개발이 어떤 식으로 진행되는지에 대한 불성숙하거나 '완전히 마무리되지 않은' 인상을 보여줍니다.

많은 사람들이 혹은 눈치채지 못하는 사람들이 소프트웨어를 만드는 것이 조립 라인에서 일회성 토스터를 만드는 것과는 다르다는 사실을 잊곤 합니다.

코드를 만들 때 아기의 비유를 사용하면 코드 작성을 좀 더 명확하게 이해할 수 있습니다."

애기가 필요로 하는 것이나 소프트웨어 프로젝트가 필요로 하는 것은 늘 자주 변합니다; 때로는 갑자기나 서서히 변할 수도 있죠. 지름길을 찾는 것은 장기적인 결과를 초래하며, 그 장기적인 결과를 고치려고 하는 것이 미래의 어느 한 시점에는 더 어려워질 수 있습니다. 궁극적으로, 당신의 아이는 당신이 어떻게 영향을 미쳤느냐에 따라 어떠한 모습이 됩니다.

같은 아기의 비유를 사용하여, 문제가 발생한 코드가 나이를 먹을수록 점점 일관성이 없어지고 이해하기 어려워집니다. 어느 시점에서는 해당 코드가 분명히 의미가 있었을 것이고, 이제는 유산의 영역에 속하게 됩니다.

유산 코드는 다음과 같이 정의될 수 있습니다

유산 코드는 유지보수되지 않고, ad-hoc 로직으로 가득 차 있고 문서화되지 않은 상태를 말하며, 보통은 소홀히 하고 있는 장기간의 결과로 발생합니다.

Refactoring를 미룰수록 기술 부채가 커지고 있는 것처럼 보이는 카드 집에 더 많은 코드가 추가되면서 상황은 더 악화되어 갑니다.

훗날에는 비즈니스 압박이 제품에 더 큰 영향을 미치게 될 것입니다. 더 많은 기능이 필요해지면 그만큼 더 많은 수익이 생길 것입니다.

개발자로서 우리는 종종 기술 부채와 비즈니스 압박 사이에 상관관계가 있다는 것을 잊곤 합니다. 비즈니스 압박은 종종 마감일, 시장 요구, 고객 기대, 경쟁력과 같은 형태로 나타납니다. 기업이 엄청나게 급한 마감일이나 긴급한 시장 요구에 직면하면 개발팀에게 빠르게 결과물을 제공하도록 압박을 가할 수 있습니다. 이 압박은 때때로 장기적인 기술적 훌륭성보다는 납득할만한 단기적인 결과물을 우선시함으로써 기술 부채가 쌓이게 할 수 있습니다.

미납된 부채는 개발자들이 장기적인 ROI가 더 크지만 의미 있는 변경사항을 거부하는 탐탁치 않은 상황에 빠지게 합니다. 기존 로직을 변경하는 것보다 적은 조정으로 몇 가지 새 기능을 추가하는 것이 언제나 더 쉬워보일 수 있지만, 이것이 옳은 방법인 것처럼 보이지만 장기적인 희생과 단기적인 이익이 따라옵니다.

제거 과정

사용하지 않는 코드를 제거하기 위해 제거 과정을 사용하세요. 이론적으로는 쉬워 보일 수 있지만, 코드베이스에 대한 폭넓은 지식이 필요합니다. 코드베이스에서 작업한 이전 개발자 중 한 명이라도 있다면, 이 과정은 훨씬 더 간단해질 것입니다.

사용하지 않는 코드를 빠르게 찾는 가장 빠른 방법은 IntelliJ와 같은 좋은 IDE를 사용하는 것입니다. 내 의견으로는 이러한 IDE가 사용하지 않는 코드가 있는 영역을 빠르게 알려줍니다. 또 다른 대안은 대부분의 프로그래밍 언어에 대해 잘 작동하는 VS-Code입니다.

사용하지 않는 코드와 불필요한 파일을 삭제하고, 불필요한 매개변수를 제거하세요. 프로젝트에 많은 양의 사용하지 않는 코드가 있으면 앱에 해로울 수 있는 여러 가지 이유가 있습니다. 사용하지 않는 코드는 규모가 있는 프로젝트의 유지 보수를 상당히 어렵게 만듭니다. 또한, 개발 팀 내에서 어떤 코드 조각이 관련이 있고 활발히 작업 중인지, 그리고 무시해도 안 되는지 혼란을 줄 수 있습니다. 정말 짜증과 귀찮음을 넘어서서 사용하지도 않는 코드를 읽는 데 시간을 낭비하는 것보다 더 없습니다.

코드 실행 없이 코드 커버리지를 확인하는 방법이 있을까요?

지금으로선 알지 못해요. 그러나 일부 연구가 진행되었습니다. 단위 테스트와 코드 커버리지 메트릭스를 철저히 확인하지 않으면 코드에서 무엇이 사용될지 확인하는 것은 굉장히 어려울 수 있어요.

팀과 함께 하나의 중요한 일은 폐기 예정인 레거시 코드를 태그하는 코드 코멘트를 추가하는 것입니다.

첫 번째 단계는 태그를 달고, 이해하고, 문서화하고, 그리고 삭제하여 우리의 KLOC (천 줄 코드)를 줄이는 것입니다. 우리가 가장 좋아하는 IDE인 IntelliJ IDEA를 사용하여 이러한 태그가 달린 코드 조각들을 신속하게 스캔하고, 나중에 우리가 후회할 수 있는 부정적 영향을 줄 것이 없다고 자신감이 생겼을 때 필요한 작업을 수행할 수 있어요.

// @더 이상 사용되지 않음
// @작성자: <작성자명>
// @날짜: <날짜>
// 이 함수의 목적:
// - 설명 
// 왜 사용이 중단되었는지
// - 설명 
// 종속성:
// - 종속성 목록의 개요 제공 
type Querier interface {
  // SELECT * FROM @@users WHERE name = @name{if role !=""} AND role = @role{end}
  FilterWithNameAndRole(name, role string) ([]gen.T, error)
}

프레임워크 주기적 업그레이드

구성된 프레임워크를 꾸준히 업데이트하는 일정한 주기를 계획하세요. 너무 오랫동안 미뤄두면 마지막 프레임워크나 라이브러리 업데이트 이후에 발생한 변경 사항이 너무 많아 업그레이드가 거의 불가능해질 수 있습니다.

라이브러리와 프레임워크를 업그레이드하는 것은 성능과 신뢰성 면에서 순수 이득을 가져오며, 프레임워크가 보통 리소스를 덜 사용하는 새로운 방법을 가지고 있어 비용 절감의 가능성이 있습니다.

프레임워크를 정기적으로 업그레이드하는 것의 단점은 순이익에 비해 훨씬 적습니다. 처음에는 시간 낭비로 느껴질 수 있지만, 실패한 빌드로 추가 회귀 테스트가 필요해 일어날 수 있습니다.

나는 오랜 시간 동안 오래된 버전을 사용하는 것과 관련된 문제들이 누적되어 점점 더 문제가 복잡해지고 비용이 증가하는 것을 목격했습니다.

제품 관리/소유 그룹에게 판매하기가 종종 어려울 수 있습니다. 그러나 프로젝트를 선택한 프레임워크의 최신 안정 버전을 사용하는 것은 중요하며 비즈니스 결정에 고려되어야 합니다. 버전이 폐지되는 지점은 코드가 보안 버전 지역에 있게 됩니다. 이것은 또한 몇 가지 비즈니스 리스크를 대표합니다. 따라서 레거시 코드에서 이에 대한 접근방식을 점진적으로 취하는 것이 좋습니다. 레거시 코드가 최신 버전에 도달할 때까지 대안적인 방법일 것입니다.

프레임워크 업그레이드를 미루는 것은 앞으로 더 비용이 들 수 있습니다.

필요한 경우에만 코드를 다시 작성하세요

누군가가 만든 코드베이스를 다시 작성하는 것은 매력적일 수 있지만, 다른 사람이 한 작업 위에 새로운 것을 만들기를 좋아하는 사람은 없습니다. 여기서 강조하고 싶은 것은 이것을 피하는 것이 좋을 수도 있다는 점입니다. 보통 고려해야 할 시간 대 효과 대 비용 혜택 요인이 있습니다. 때때로 코드를 다시 작성하면 새로운 버그를 도입하거나 숨겨진 기능을 제거할 수도 있습니다.

만약 재작성의 길을 택하기로 한다면, 이전 학습을 기반으로 새로운 요구 사항에 맞게 코드를 처음부터 새롭게 작성하는 것이 매우 중요합니다.

  • 코드가 읽기 어려워지는 경우
  • 새로운 요구 사항이 너무 많이 벗어나고 코드가 새로운 요구 사항과 더 이상 유효하지 않을 수도 있는 특정 가정에 따라 작성된 경우
  • 더 나은 설계 가능성이 제시되는 경우
  • 새로운 기술이 사용 가능해지면, 언어 업그레이드 등이 될 수 있습니다. 새로운 기술이 도움이 될 수 있는지 확인하기 위해 최신 정보를 유지하는 것이 중요합니다.

새 코드를 깨끗하게 유지하세요

새 코드를 깨끗하게 유지하는 것은 코드의 문제를 피하는 한 가지 방법입니다. "깨끗하다"가 무엇을 의미하는지 궁금할 수 있습니다. 이는 상대적이며 개인에 따라 주관적일 수 있습니다.

우리는 깨끗한 코드가 더 경험 많은 사람들에게 공유된 소프트웨어 엔지니어링 최상의 실천 지식을 종합한 것이라는 데에 동의할 수 있습니다.

  • 깨끗한 코드는 집중력이 있습니다. 각 함수, 클래스 또는 모듈은 주변 세부 사항에 방해받지 않고 한 가지 일을 잘 수행해야 합니다.
  • 깨끗한 코드는 읽기 쉽고 장황하지 않아야 합니다. 모호한 줄임말 사용을 피하십시오. 모든 것이 약어로 표현되면 아무도 이해하지 못합니다.
  • 깨끗한 코드는 디버깅하기 쉽습니다.
  • 깨끗한 코드는 유지보수하기 쉽습니다. 다른 개발자가 쉽게 읽고 개선할 수 있습니다.
  • 깨끗한 코드는 높은 성능을 발휘합니다.
  • 깨끗한 코드는 DRY 원칙을 준수합니다. 코드 반복은 모든 악의 근원입니다. 중복 코드는 로직이 변경될 때 여러 곳에서 변경해야 하는 문제를 일으키므로 오류가 발생할 수 있습니다. 실제 운영 환경에서 결과는 참혹할 수 있습니다.
  • 깨끗한 코드는 공백, 주석 및 명명에 관한 언어 관례를 따르며 명확하고 쉽게 읽을 수 있습니다. 많은 언어에 대한 스타일 가이드가 제공되며 대부분의 이러한 최상의 실천 방법은 여러분이 선호하는 IDE로 비공식 표준으로 내장되어 있습니다.

상속된 코드의 품질을 제어할 수는 없으며 그것에 대해 비난해선 안 돼요.

간단한 사실은, 상속받았고 유지보수를 위해 보수비를 받고 있다면, 그에 대해 불평할 수 없어요.

코드를 자주 공부하고 자주 문서화하는 것이 중요합니다. 코드에 숨겨진 오류와 호환성 문제를 발견하여 코드베이스를 다듬어서 다음 개발자가 당신에 대해 불평할 일이 없도록 해보세요.

코드를 자주 공부하고 자주 문서화하세요

상속된 코드베이스와 작업하는 것은 시간이 지남에 따라 더 쉬워집니다.

첫 번째 단계는 코드베이스에 대해 더 많이 배우기 위해 의식적인 결정을 내리는 것입니다. 이는 코드베이스를 개선하는 데 도움이 될 것입니다. 평균 프로그래머는 문서화를 즐기지 않지만, 여러분에게 가치있는 투자입니다. 여러분은 유지 관리할 계획이 없을 수도 있는 전체 위키 페이지를 작성하는 대신 이 접근 방식으로 점진적인 단계를 거칠 수 있습니다. 코드베이스에 직접 문서를 작성해보세요.

Java를 사용 중이라면 Javadoc Spec은 많은 IDE에서 우수하게 작동하며 주석과 메타데이터를 기반으로 자동 문서화를 생성하는 데 도움이 됩니다.

Golang에는 유사한 패키지인 GoDoc이 있으며, Python에서는 MkDocs와 같은 다양한 옵션이 있습니다. MkDocs는 HTML 문서를 자동으로 생성하는 데 사용되며, FastAPI git hub에서 보여지는 Material 테마와 같이 멋지게 보이는 문서를 만들 수 있습니다.

2024-07-13-TheConstantDilemmaofLegacyCodeandHowtoDealwithIt_1.png

GitHub과 GitLab에는 포괄적인 위키 모듈도 제공됩니다. 클라우드 버전 관리 도구에 따라 옵션으로 사용할 수 있습니다.

문서는 코드에 가까운 위치에 있어야 하며, 핵심 비지니스 문서는 비기술자들이 앱을 이해할 수 있도록 액세스할 수 있는 위키 페이지에 저장될 수 있습니다.

문서화를 주요 과점으로 설정하면 다른 사람들이 레거시 코드를 이해하는 데 도움이 되며 코드가 작동하는 방식을 기억할 수 있도록 도와줍니다.

당신의 Git 리포 README.md 파일을 무시하지 말아요. 이 작은 것이 새로운 팀 개발자들의 입문 시간을 몇 주에서 몇 시간으로 줄일 수 있는 극히 중요한 요소랍니다.

큰 프로젝트를 컴포넌트 또는 기능으로 구성하여 조직하는 데 도움이 돼요. 이는 팀원들이 전체 프로젝트가 어떻게 구성되어 있는지 더 잘 파악하도록 도와줍니다.

개발 속도를 높여줘요. 정보를 더 빨리 찾기 때문에 개발도 더 신속해져요. 우수한 문서를 작성하는 게 목표가 아니라 개인들이 더 생산적일 수 있게 도와주는 일부 문서가 필요한 거죠.

요즘 A.I가 새로운 석유라는 말이 나오는데요. ChatGPT와 같은 A.I를 활용해 문서 작업을 일부 자동화할 수도 있어요.

한 해가 지난 후에도 GPT가 출시된 이후로, 많은 개발자들이 A.I 도구를 활용하여 생산성을 향상시키는 데 미흡하다는 사실에 여전히 놀라고 있습니다. 이를 활용할 시 생산성을 최소 2배 향상시킬 수 있습니다.

이 글을 읽고 계신 소프트웨어 개발자이시라면, 기억해야 할 한 가지는 A.I를 활용해 자신을 더 생산적으로 만드는 데 도움이 되는 방법을 찾는 사람들은 AI에 교체되지 않을 것이라는 것입니다.

대신 리팩토링을 시도하세요

리팩토링은 코드의 구조를 변경하지 않으면서 작은 부분들을 고의로 변경하는 것을 의미하며, 즉 기능을 유지한 채 수정하는 것입니다.

위 모든 변화가 종합적으로 영향을 미치는 동시에, 변화가 점진적인 단계적 조정이라는 사실은 위험을 최소화합니다.

이 방식은 일반적으로 재구축보다 선호되며, 반복적으로 수행된다면 강력한 전략이 될 수 있습니다.

― Martin Fowler

마틴 파울러의 이 말은 나에게 울립니다; 간단해 보일 수 있지만, 본인 이외의 사람도 이해할 수 있는 코드를 작성하는 것은 굉장히 어렵습니다.

코드 구조와 가독성을 향상시키기 위해 점진적이고 점차적인 변경을 하는 것이 항상 좋은 아이디어입니다. 중대하고 철저한 변경은 피하고, 새로운 버그를 도입할 수 있으니 쉽게 식별하고 수정할 수 있습니다.

Michael C. Feathers의 이 기사가 좋은 시작점이며, 코드베이스를 점진적으로 개선하는 방법에 대한 좋은 예제를 제공합니다.

또 다른 코드 리팩터링의 좋은 출처는 Martin Fowler의 "Refactoring: Improving the Design of Existing Code"입니다. 이 책은 코드를 효과적으로 리팩터링하는 많은 팁을 제공합니다.

코드를 여러 작은 모듈로 분리하기

좋은 코드를 만드는 것은 복잡할 수 있습니다. 종종 매우 복잡한 종속성과 서브시스템 간 감춰진 접촉점이 있습니다. 게다가, 최종 사용자가 시스템을 예측할 수 없는 방법으로 사용할 것이므로, 다소 미묘한 결함이 있는 모호한 논리를 만들기에 이상적인 환경이 됩니다.

레거시 코드를 다루기 시작할 때, 첫 번째 단계는 모듈로 나누어 호환성, 버그 또는 보안 문제에 대해 점진적으로 수정하는 것입니다. 코드를 작은 모듈로 나누는 것은 문제를 해결하는 데 도움이 될 뿐만 아니라 의존성과 상호 호환성을 쉽게 식별할 수도 있습니다. 이를 위해서는 더는 관련이 없는 데드 코드의 줄을 삭제하고 업데이트해야 할 코드를 다시 작성하는 것이 필요합니다.

항상 당신의 잘못이 아닙니다, 아니요?

결함이 누군가의 "잘못"인 유일한 경우는 그들이 그것을 고의로 넣었을 때입니다. 만약 당신이 그 결함이 명백하다고 생각한다 해도, 원래의 코드 작성자가 그렇지 않았을 가능성이 높습니다. 코드를 작성한 사람은 그 당시에 많은 업무를 정신적으로 해결해야만 했을 것입니다.

"요구 사항이 잘못 해석될 수 있다는 문제도 있습니다. 경계 사례가 항상 명확하지 않을 수 있습니다. 나의 핀테크 분야에서, 1억 달러 규모의 프로젝트를 다룰 때, '북극성 경로' 자체가 항상 명확하지는 않습니다. 우린 이미 그 문제를 해결했다고 생각할지도 모르겠죠.

당신과 팀에 도움이 될 수 있는 한 가지는, 결함에 대해 누구도 '비난'받을 필요가 없다는 것을 인정하는 것입니다. 개발자가 결함을 고의로 넣은 경우에만 결함이 생겼다고 비난 받아야 하며, 테스터가 결함을 찾았지만 보고하지 않은 경우에만 결함이 생겼다고 비난받아야 합니다.

다른 모든 경우에서 결함은 소프트웨어 개발에서 사업 비용으로 간주됩니다. 그것은 변화무쌍한 환경의 일부에서 변하지 않는 변수입니다.

표준 알고리즘의 손으로 쓴 버전 쓰기를 그만두세요"

디버깅을 어렵게 만드는 하나의 이유입니다. 찾고 있는 곳이 고쳐야 할 곳이 아닐 수 있습니다. 코드가 쓰레기 값으로부터 자신을 보호하지 않은 경우 결과가 쓰레기인지 판단하기 어렵습니다.

시스템이 용인해주는 정도가 클수록 문제가 감지되지 않을 수 있습니다. 이로 인해 문제의 근본원인이 더욱 멀어질 수 있습니다.

이 문제를 해결하기 위해서 코드를 너무 똑똑하고 간결하게 작성하는 것을 피하세요. 표준 알고리즘의 직접 작성 버전을 피하고, 검증된 라이브러리를 추가하는 것이 더 쉽기 때문에 단축키를 사용하지 마세요. 유산 코드를 개선하는 것보다 초록 필드 코드를 작성하는 것이 더 쉽다고 말하듯이 새로운 입증된 라이브러리를 추가하는 것이 어렵습니다.

과도한 추상화는 결과의 감소로 이어질 수 있습니다

일부 inferred된 의존성은 계약의 숨겨진 부분이 됩니다. 그러한 것들을 발견할 도구가 있긴 하지만, implicit한 가정들은 일반적으로 좋지 않습니다. 이게 동일한 이슈입니다. 명시적인 코드는 그것을 고려하도록 강요하지만, implicit한 코드는 실제로 괜찮다고 가정할 수 있게 합니다.

필요한 것들과 원하는 것들의 균형을 유지하는 법을 배우세요

시장에 출시하는 데 걸리는 시간 (TTM) 자체가 코딩 문제가 되는 것은 아닙니다. TTM은 종종 한 가지 사용 사례에 대해 작동하는 프로토 타입 버전을 작성하게 만들기 때문에 코드가 장기적으로 최적화되지 않습니다. 이것의 부산물은 TTM 코드가 모두가 수정하기를 두려워하는 레거시 코드가 되는 것입니다.

종종 경계 경우를 다루지 않아야 할 때가 있습니다. 예를 들어 프로토타입 버전이 시스템의 기초가 될 경우, 그러한 처리되지 않은 경계 경우들은 시스템의 취약한 부분이 됩니다; 이것이 바로 "greenfield" (새로운) 코드를 작성하는 것이 "brownfield" (설립된, 레거시) 코드를 유지하는 것보다 쉬운 이유입니다. 일반적으로 설정된 코드의 많은 곳에는 평가되지 않은 경계 경우들이 있습니다.

코드를 이해하는 한 가지 방법은 유닛 테스트를 작성하는 것입니다. 또한 정적 코드 분석기와 같은 코드 품질 도구를 사용하여 코드에 잠재적인 문제를 식별할 수 있습니다.

테스트를 작성하면 리팩터링 프로세스가 훨씬 더 관리 가능해지고 레거시 코드가 향상되며 이를 제거할 수 있습니다. 이전에 레거시 코드를 다시 작성하지 말아야 한다는 원칙에 대해 언급했습니다. 만약 테스트 케이스가 있다면 이 규칙이 변화됩니다.

이는 우리가 점진적인 방식으로 접근할 때 더 큰 자신감을 줍니다. 비용이 분산되어 한꺼번에 발생하지 않습니다. 리더십에게 기술 부채 스프린트가 필요하다고 설득하려는 노력을 하면서 동시에 기능을 제공하고 수익을 창출해야 한다면 이는 제로섬 게임입니다. 점진적으로 이를 접근하는 것이 더 쉽습니다.

낭송을 채택하고 코드를 사랑하라. 당신은 영원히 부모가 되어야만 하는 것이 아닙니다.

맨트는 베다 산스크리트어의 "om"에서 비롯된 신비로운 진동이며, 삶을 구조화하는 지침으로 동의어로 사용됩니다. 전형적으로 이는 사람이 마음을 진정시키기 위해 반복하는 단어, 소리 또는 구절입니다. 매번 되풀이되는 맨트의 진동은 여러분의 팀이 코드의 완전한 소유권을 취할 수 있는 새로운 습관을 형성하는 데 힘을 줍니다.

맨트는 여러분 개인 또는 팀이 담당하는 코드 기반물에 대한 소유권과 책임감을 강조합니다. 이는 팀 구성원들이 코드의 무결성을 보호하고 그것이 잘 유지되며 미래 필요에 대비하여 기능을 향상시키기 위해 헌신적으로 노력한다는 것을 전달합니다.

주요 포인트

유산 코드(legacy code)를 다루는 것은 분명히 어려울 수 있습니다. 완전히 피하고 아무것도 하지 않는 것이 유혹적일 수 있지만, 과거의 실수를 학습 과정의 일부로 받아들이는 학습 환경을 조성해야 합니다. 서로 원망하지 않고 손가락질하지 않는 긍정적이고 협력적인 환경을 유지하는 것이 중요합니다.

과거의 코딩 실수를 무시하거나 방치하는 대신 그 책임을 짊어지고 그것으로부터 배우며 지속적으로 서로를 격려하여 개선하는 것은 코드가 처음부터 레거시가 되는 것을 방지하는 데 중요합니다.