February 9, 2022 • ☕️ 7 min read
리팩터링 원칙
리팩터링의 사전적 정의는 다음과 같다.
소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법
리팩터링은 결국 동작을 보존하는 작은 단계들을 거쳐 코드를 수정하고, 이러한 단계들을 순차적으로 연결하여 큰 변화를 만들어내는 일이다. 리팩터링은 “재구성” 의 특수한 한 형태다. 단계를 작게 나눔으로써 구성을 체계화할 수 있고, 디버깅 시간을 단축할 수 있다.
리팩터링을 하더라도 사용자 관점에서는 달라지는 점이 없어야 한다. 리팩터링 과정에서 발견된 버그는 리팩터링 후에도 그대로 남아 있어야 한다.
리팩터링은 성능 최적화와 비슷하다. 리팩터링의 목적은 코드를 이해하고 수정하기 쉽게 만드는 것이다.
켄트 백은 소프트웨어 개발의 목적을 ‘기능 추가’ 또는 ‘리팩터링’ 으로 나누고, 이를 두 개의 모자라고 명명한다.
‘기능 추가’ 시에는 기존 코드는 절대 건드리지 않고 새 기능을 추가하기만 한다. 반면 ‘리팩터링’ 시에는 기능 추가는 절대 하지 말아야 한다. (테스트도 새로 만들지 않는다)
✔️ 3의 법칙 (1) 처음에는 그냥 한다. (2) 비슷한 일을 두 번째로 하게 되면, 일단 계속 진행한다. (3) 비슷한 일을 세 번째 하게 되면 리팩터링한다.
✔️ 준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기 리팩터링하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다. 이 시점에 현재 코드를 살펴보면서, 구조를 살짝 바꾸면 다른 작업을 하기가 훨씬 쉬워질 만한 부분을 찾는다. 버그를 잡을 때도 마찬가지다. 상황을 개선해놓으면 버그가 수정된 상태가 오래 지속될 가능성을 높이는 동시에, 같은 곳에서 다른 버그가 발생할 가능성을 줄여준다.
✔️ 이해를 위한 리팩터링: 코드를 이해하기 쉽게 만들기 코드를 수정하려면 먼저 그 코드가 하는 일을 파악해야 한다. 그 코드의 의도가 더 명확하게 드러나도록 리팩터링할 여지는 없는지 창아본다.
✔️ 쓰레기 줍기 리팩터링 간단히 수정할 수 있는 것은 즉시 고치고, 시간이 좀 걸리는 일은 짧은 메모만 남긴 다음, 하던 일을 끝내고 나서 처리한다.
✔️ 코드 리뷰에 리팩터링 활용하기 코드 리뷰는 개발팀 전체에 지식을 전파하는 데 좋으며, 다른 사람의 아이디어를 얻을 수 있다. 리팩터링은 다른 이의 코드를 리뷰하는 데도 도움이 된다. 리팩터링은 코드 리뷰의 결과를 더 구체적으로 도출하는 데에도 도움된다. 내가 떠올린 아이디어를 실제로 적용했을 때의 모습을 더 명확하게 볼 수 있다. cf) 짝 프로그래밍
✔️ 관리자에게는 뭐라고 말해야 할까? “리팩터링한다고 말하지 말라”. 구체적인 방법은 개발자가 판단해야 한다. 프로 개발자에게 주어진 임무는 새로운 기능을 빠르게 구현하는 것이고, 가장 빠른 방법은 리팩터링이다.
✔️ 리팩터링하지 말아야 할 때 지저분한 코드를 발견해도 굳이 수정할 필요가 없다면 리팩터링하지 않는다. 내부 동작을 이해해야 할 시점에 리팩터링해야 효과를 제대로 볼 수 있다. 리팩터링하는 것보다 처음부터 새로 작성하는 게 쉬울 때도 리팩터링하지 않는다. 이 결정에는 뛰어난 판단력과 경험이 뒷받침되어야 한다.
✔️ 새 기능 개발 속도 저하
리팩터링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것이다.
균형점을 잡아야 한다. 준비를 위한 리팩터링이라면 주저하지 않고 시작한다. 반면 내가 직접 건드릴 일이 거의 없거나, 불편한 정도가 그리 심하지 않다고 판단되면 리팩터링하지 않는다. 코드베이스를 건강하게 만드는 것에 대해 팀원들에게 공감대를 형성해야 한다. 그러나 리팩터링의 본질은 코드베이스를 예쁘게 꾸미는 것이 아니며, 오로지 경제적인 이유다. 리팩터링은 개발 기간을 단축하고자 하는 것이다.
✔️ 코드 소유권 리팩터링하다 보면 모듈의 내부뿐 아니라 시스템의 다른 부분과 연동하는 방식에도 영향을 주는 경우가 많다. 코드 소유권이 나뉘어 있으면 리팩터링에 방해가 된다. 하지만 제약이 따르더라도 리팩터링을 해야 한다. 저자가 선호하는 방식은 코드의 소유권을 팀에 두는 것이다. 그래서 팀원이라면 누구나 팀이 소유한 코드를 수정할 수 있게 한다.
✔️ 브랜치 브랜치-마스터를 사용하는 방식은 독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터로 통합하기가 어려워진다는 단점이 있다. 기능별 브랜치의 통합 주기는 짧게 관리해야 한다. 이 방식을 지속적 통합(CI; Continuous Integration), 또는 트렁크 기반 개발(TBD; Trunk-Based Development)이라 한다. CI는 코드 전반에 거쳐 자잘하게 수정해야 하는 경우가 많을 때 도움이 된다.
✔️ 테스팅 리팩터링을 하면서 프로그램의 겉보기 동작은 똑같이 유지해야 한다. 실수하여 동작이 깨지더라도 오류를 재빨리 해결할 수 있어야 한다. 리팩터링하기 위해서는 자가 테스트 코드를 마련해야 한다. 자가 테스트 코드는 리팩터링을 할 수 있게 해줄 뿐만 아니라, 새 기능 추가도 훨씬 안전하게 진행할 수 있도록 도와준다. 또한 리팩터링 과정에서 버그가 생길 위험이 아주 크다는 불안감을 해소할 수 있다. 자가 테스트 코드는 통합 과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와도 밀접하게 연관된다.
✔️ 레거시 코드 레거시 시스템을 파악할 때 리팩터링이 굉장히 도움된다. 대규모 레거시 시스템에는 테스트를 보강해야 한다. 프로그램에서 테스트를 추가할 틈새를 찾아서 시스템을 테스트해야 한다. 이러한 틈새를 만들 때 리팩터링이 활용된다. 서로 관련된 부분끼리 나눠서 하나씩 공략하는 것이 좋다. 코드의 한 부분을 훑고 넘어갈 때마다 예전보다 조금이라도 개선하려고 노력하며, 레거시 시스템의 규모가 크다면 자주 보는 부분을 더 많이 리팩터링한다.
✔️ 데이터베이스 데이터베이스의 변경도 다른 리팩터링과 마찬가지로 전체 변경 과정을 작고 독립된 단계들로 쪼개는 것이 핵심이다. 그러나 데이터베이스 리팩터링은 프로덕션 환경에 여러 단계로 나눠서 릴리스하는 것이 대체로 좋다는 점에서 다른 리팩터링과 다르다. 이렇게 하면 프로덕션 환경에서 문제가 생겼을 때 변경을 되돌리기 쉽다.
리팩터링은 소프트웨어 아키텍처를 바라보는 관점을 완전히 바꿔놓았다. 리팩터링은 요구사항 변화에 자연스럽게 대응하도록 코드베이스를 잘 설계해 준다.
향후 변경에 유연하게 대처할 수 있는 유연성 메커니즘을 소프트웨어에 심어둘 수 있다. 함수 정의 시 다양한 예상 시나리오에 대응하기 위한 매개변수들을 추가하는 것이 그 예다. 유연성 메커니즘을 구현하는 데 추가적으로 치러야 할 비용들이 있지만, 리팩터링을 활용하면 다르게 접근할 수 있다.
미래에 필요해질 유연성과 그 변화에 가장 잘 대응할 수 있는 추측 대신, 그저 현재까지 파악한 요구사항만을 해결하는 소프트웨어를 구축한다. 진행하면서 아키텍처도 그에 맞게 리팩터링해서 바꾼다.
이런 식의 설계 방식을 ‘간결한 설계’, ‘점진적 설계’, ‘YAGNI(you aren’t going to need it’) 등으로 부른다. 나중에 문제를 더 깊이 이해하게 됐을 때 처리하는 쪽이 훨씬 나을 수도 있다.
리팩터링과 함께 퍼지기 시작한 XP(익스트림 프로그래밍)의 특징은 지속적 통합, 자가 테스트 코드 리팩터링 등의 개성이 강하면서 상호 의존하는 기법들을 하나로 묶은 프로세스라는 점이다.
XP는 수 년에 걸쳐 애자일의 부흥을 이끌었다. 애자일을 제대로 적용하기 위해서는 리팩터링이 필수적이다. 자가 테스트 코드, 지속적 통합, 리팩터링의 세 기법은 서로 강력한 상승효과를 발휘한다.
지속적 배포는 소프트웨어를 언제든 릴리스할 수 있는 상태로 유지해준다. 게다가 위험요소도 줄이고, 비즈니스 요구에 맞춰 릴리스 일정을 계획할 수 있다. 이처럼 견고한 기술적 토대를 바탕으로 프로덕션 코드 반영까지의 시간을 단축하고 버그를 줄여줘 소프트웨어의 신뢰성도 높일 수 있다.
리팩터링하면 소프트웨어가 느려질 수도 있는 건 사실이지만, 그와 동시에 성능을 튜닝하기는 더 쉬워진다. 소프트웨어를 빠르게 만드는 비결은, 먼저 튜닝하기 쉽게 만들고 나서 원하는 속도가 나게끔 튜닝하는 것이다.
빠른 소프트웨어를 작성하는 방법 세 가지
✔️ 시간 예산 분배 방식 설계를 여러 컴포넌트로 나눠서 컴포넌트마다 자원(시간과 공간) 예산을 할당한다. 컴포넌트는 할당된 자원 예산을 초과할 수 없다. 시간 예산 분배 방식은 멍격한 시간 엄수를 강조한다.
✔️ 끊임없이 관심을 기울이는 것 대부분 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비한다.
✔️ 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경 쓰지 않고 코드를 다루기 쉽게 만드는 데 집중한다. 먼저 프로그램을 분석하여 시간과 공간을 많이 잡아먹는 지점을 알아내고, 그 부분들을 개선한다. 최적화를 위한 수정도 작은 단계로 나눠서 진행하며, 사용자가 만족하는 성능에 도달할 때까지 최적화 대상을 찾아서 제거하는 일을 계속한다.
프로그램을 잘 리팩터링해 두면 성능에 투입할 시간을 벌 수 있으며, 리팩터링이 잘 되어 있는 프로그램은 성능을 더 세밀하게 분석할 수 있다. 단기적으로 보면 리팩터링 단계에서는 성능이 느려질 수도 있지만, 최적화 단계에서 코드를 튜닝하기 훨씬 쉬워지기 때문에 결국 더 빠른 소프트웨어를 얻게 된다.