본문 바로가기

TIL

Refactoring. 리팩터링 원칙

2장에서는 리팩터링 전반에 적용되는 원칙을 정의, 리팩터링 시기, 고려해야 할 문제 등을 통해  알아본다. 

먼저 책에서 정의 하는 리팩터링 정의를 보자.

명사로는 소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법.

동사로는 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러 가지 리팩터링 기법을 적용해서 소프트웨어를 재구성하다.라고 나와있다.

보통 많은 사람이 코드를 정리하는 작업을 모조리 '리팩터링'이라고 표현하는데 앞선 정의를 따르면 특정한 방식에 따라 코드를 수정하는 것만이 리팩터링이다. 누군가 리팩터링하다가 코드가 깨져서 며칠간 고생했다라고 한다면 리팩터링 잘못한 거라고 한다.

앞에서 래픽터링을 정의할 때 '겉보기 동작'이란 표현을 썼는데, 이 뜻은 리팩터링 하기 전과 후의 코드가 똑같이 동작해야 한다는 뜻이다. 물론 함수 추출하기를 거치면 콜스택이 달라져서 성능이 변할 수 있다. 하지만 사용자 관점에서는 달라지는 점이 없어야 한다. 또한 리팩터링 과정에서 발견된 버그는 리팩터링 후에도 그대로 남아 있어야 한다고 한다. 그만큼 전과 후가 같아야 된다는 것 같다.

TDD의 저자 켄트 벡은 리팩터링과 기능 추가를 두 개의 모자에 비유했다고 하는데, 기능 추가를 할때는 기존 코드는 건드리지 않고 새 기능을 추가하기만 하고, 리팩터링 할 때는 리팩터링 모자를 쓴 다음 기능 추가는 절대 하지 않기로 다짐한 뒤 오로지 코드 재구성에만 전념한다고 한다. 이 책의 저자인 마틴 파울러는 중간중간 모자를 빠르게 바꿔 써가며 개발을 하기도 한다고 한다. 여기서 포인트는 하기로 한 목적을 분명히 하고 진행해야 한다는 것 같다.

다음은 리팩터링하는 이유를 살펴보자

먼저 리팩터링을 하면 소프트웨어 설계가 좋아진다고 한다. 단기적인 목표만을 위해 코드를 수정하다 보면 기반 구조가 무너지기 쉽다. 따라서 규칙적인 리팩터링은 코드 구조를 지탱해 줄 것이다. 중복된 코드가 여러 곳에 나타날 수 있는데 중복 코드 제거는 설계 개선 작업의 중요한 축을 차지한다. 중복 코드를 제거하면 모든 코드가 언제나 고유한 일을 수행함을 보장할 수 있다.

두 번째 이유는 리팩터링을 하면 소프트웨어를 이해기 쉬워진다고 한다. 프로그래밍은 결국 내가 원하는 바를 정확히 표현하는 일이다. 그런데 소스 코드를 컴퓨터만 쓰는 게 아니라 다른 사람도 수정하고 읽어야 한다. 컴퓨터 입장에서는 복잡하든 간단하든 돌리는데 문제가 없지만, 사람이 본다면 이야기가 달라진다. 또한 그 사람이 나 자신일 때도 해당된다.

세 번째는 버그를 찾기 쉬워진다. 이건 위에서 한 이야기만 봐도 직관적으로 좋아질 거 같다. 구조를 명확하게 다듬으면 자연스럽게 잘못된 점들이 드러난다. 

마지막 이유는 프로그래밍 속도이다. 시스템을 오랫동안 개발하면서 기능을 추가하게 되면 점점 기능을 추가할 때마다 시간이 오래 걸린다. 하지만 지속적인 리팩터링을 통해 설계가 잘 되어있다면 로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있다. 코드가 명확해지면 버그를 만들 가능성이 줄고 디버깅 하기도 훨씬 쉽다. 이러한 효과를 저자는 설계 지구력 가설이라고 한다.

이유를 알아봤으니 언제 리팩터링을 해야할지 알아보자

리팩터링 하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다.  구조를 살짝 바꾸면 다른 작업을 하기가 훨씬 쉬워질 만한 부분을 찾는다. 예시를 들면 리터럴 값 몇 개가 방해 되는 함수가 있다고 하자. 함수를 복제해서 해당 값만 수정하면 중복 코드가 생긴다. 그럼 나중에 수정할 일이 생기면 할 일이 더 많아진다. 이럴 때는 함수 매개변수 화하기를 적용한다. 그리고 그 함수에 필요한 매개변수를 지정해서 호출하기만 하면 된다. 버그를 잡을 때도 오류를 일으키는 코드가 퍼져 있다면 한 곳으로 합치는 편이 작업하기 훨씬 편하다고 한다.

방금과 같이 준비를 위한 리팩터링 말고도 이해를 위한 리팩터링이 있다. 아까 말한 것처럼 리팩터링을 통해 코드의 의도가 더 명확하게 할 수 있기 때문에 미리 미리 리팩터링을 통해 코드에 이해한 점들을 반영하는 것이다. 코드를 분석할 때 리팩터링을 해보면, 깊은 수준까지 코드를 이해할 수 있다고 한다.

또 다른 시기는 코드를 보면서 문제가 될 것 같은 부분을 찾았을때 걸릴 시간을 판단해 리팩터링을 진행하거나 추후에 진행할 수 있게 체크해 두는 것이다.

리팩터링은 프로그래밍 과정의 일부로 생각하고 지속적으로 하는게 가장 좋다. 물론 아예 안 하는 것보다는 계획적으로나마 리팩터링을 하는 게 좋다고 말한다. 그렇지만 리팩터링은 프로그래밍을 하면서 같이 가져가야 하는 과정이다. 그래서 저자는 기능 추가 커밋과 리팩터링 커밋을 분리하는 것을 별로 좋아하지 않는다.

리팩터링을 하게 되면 당연히 대규모 리팩터링을 해야할 때도 있다. 이럴 때도 그냥 리팩터링만 하는 게 아니라 팀의 구성원이 나눠서 오래 걸리더라도 조금씩 자주 하는 게 좋다고 한다. 라이브러리를 새 것으로 바꾸는 작업 같은 경우 기존 것과 새것 모두를 포용하는 추상 인터페이스부터 마련하고 기존 코드가 추상 인터페이스를 호출하도록 만들고 나면 훨씬 쉽게 라이브러리를 교체할 수 있다.

코드 리뷰에 리팩터링을 활용하는것도 굉장히 좋다고 한다. 단순히 개선 사항을 제시하기보다. 리팩터링은 코드 리뷰의 결과를 더 구체적으로 도출하는데 도움이 된다. 상당수를 즉시 구현해볼 수 있기 때문에 큰 성취감도 맛볼 수 있다. 하지만 단순히 풀 리퀘스트로 올려진 방식을 리뷰하는 것보다. pair programming일 때 특히 도움이 된다.

리팩터링 하지말고 처음부터 작성하는 것이 나을 때도 분명히 있다고 한다. 하지만 이러한 판단력은 당연히 경험이 뒷받침되어야 한다.

다음으로 리팩터링 시 고려할 문제들을 보자

많은 사람들이 리팩터링 때문에 새 기능을 개발하는 속도가 느려진다고 여기지만, 리팩터링의 궁극적인 목적은 개발 속도를 높이는 데 있다. 앞에서 이야기한 것처럼 새 기능을 추가하기 전에 리팩터링을 거치면 이게 장기적으로 볼 때는 생산성을 증가시키는 것 같다.  저자는 리팩터링의 본질은 코드베이스를 예쁘게 꾸미는 데 있지 않고 오로지 경제적인 이유로 하는 것이라고 한다.

다음으로 코드 소유권이다 결론 부터 말하면 저자는 코드 소유권을 작은 단위로 나눠 엄격히 관리하는 데 반대하는 입장이다. 저자가 선호하는 방식은 코드의 소유권을 팀에 두는 것이다. 그래서 팀원이라면 누구나 팀이 소유한 코드를 수정할 수 있게 한다.

다음은 브랜치에 관한 고려 사항이다. 보통 팀원마다 브랜치를 하나씩 맡아서 작업하다가 결과물이 어느 정도 쌓이면 마스터 브랜치에 통합해서 다른 팀원과 공유한다. 이 방식을 선호하는 이들은 작업이 끝나지 않은 코드가 마스터에 섞이지 않고, 기능이 추가될 때마다 버전을 명확히 나눌 수 있고, 기능에 문제가 생기면 이전 상태로 쉽게 되돌릴 수 있어서 좋다고 한다. 이러한 기능 브랜치 방식에는 단점이 있다고 한다. 독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터로 통합하기 어려워진다고 한다. 그래서 많은 사람들이 마스터를 개인 브랜치로 수시로 리베이스 하거나 머지한다. 하지만 여러 기능 브랜치에서 동시에 개발이 진행될 때는 이런 식으로 해결할 수 없다고 한다.  여기서 저자는 머지와 통합을 명확히 구분하는데, 마스터를 브랜치로 '머지' 하는 작업은 단방향 이다. 브랜치만 바뀌고 마스터는 그대로이다. 반면, '통합'은 마스터를 개인 브랜치로 가져와서 작업한 결과를 다시 마스터에 올리는 양방양 처리를 뜻한다. 그래서 마스터와 브랜치가 모두 변경된다. 통합한 마스터를 내 브랜치에 머지하는 데는 상당한 노력이 든다고 한다. 최신 버전 관리 시스템은 복잡한 변경 사항을 텍스트 수준에서 머지하는 데는 뛰어나지만, 코드의 의미는 전혀 이해하지 못한다. 가령 함수 이름이 바뀌는 정도는 가볍게 통합해준다.  하지만 다른 브랜치에서 함수를 호출하는 코드를 추가했는데, 내 브랜치에서는 그 함수의 이름을 변경했다면 프로그램이 동작하지 않게 된다.

이런 문제들 때문에 브랜치 통합 주기를 짧게 관리해야 한다고 주장하는 사람들이 많다. 이 방식을 지속적 통합 또는 트렁크 기반 개발이라고 한다. CI에 따르면 모든 팀원이 하루에 최소 한 번은 마스터와 통합한다. CI를 적용하기 위해서는 마스터를 건강하게 유지하고, 거대한 기능을 잘게 쪼개는 법을 배우고, 각 기능을 끌 수 있는 기능 토글을 적용하여 완료되지 않은 기능이 시스템 전체를 망치지 않도록 해야 한다. 또한 CI는 리팩터링과 궁합이 아주 좋기 때문에 선호한다. 켄트 벡이 CI와 리팩터링을 합쳐서 익스트림 프로그래밍을 만든 이유도 두 기법의 궁합이 잘 맞아서 그랬다고 한다. 물론 CI를 적용할때 기능별 브랜치를 많이 사용한다. 하지만 리팩터링 부담이 너무 크다. 오픈 소스 같은 경우는 기능별 브랜치 방식이 더 적합할 수 있다. 

다음은 테스팅에 대해 고려할 사항이다. 리팩터링 과정에서 실수해서 동작이 깨질 수도 있다. 하지만 빠르게 잡으면 문제없다. 여기서 빠르게 잡을 수 있게 해 주는 게 코드를 다양한 측면으로 검사하는 테스트이다. 리팩터링을 위해서는 자가 테스트 코드가 필요하다. 자가 테스트 코드는 통합 과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와도 밀접하게 연관된다. CI에 통합된 테스트는 익스트림 프로그래밍(XP)의 권장사항이자 지속적 배포(CD)의 핵심이기도 하다. 또한 자가 테스트 코드와 리팩터링을 묶어서 TDD(테스트 주도 개발)이라 한다. 나머지 고려할 사항으로 레거시 코드와 데이터 베이스 측면이 있다.

위에서 개발 프로세스 이야기가 나온김에 개발 프로세스에 대해 정리하고 나머지는 다음에 다루겠다. 

최초의 애자일 소프트웨어 방법론 중 하나로 등장한 XP는 그 후 수년에 걸쳐 애자일의 부흥을 이끌었다고 한다. 그에 따라 상당수의 프로젝트에서 애자일 사고가 주류로 자리 잡았는데, 현재 제대로 애자일을 적용한 프로젝트는 찾기 힘들다고 한다. 적용하기 위해서는 리팩터링이 자연스럽게 전반적으로 스며들어야 한다고 한다.

리팩터링의 첫 번째 토대는 자가 테스트 코드이다. 위에서 이야기 했듯이 프로그래밍 도중 발생한 오류를 확실히 걸러내는 테스트를 자동으로 수행할 수 있어야 한다.

팀으로 개발하면서 리팩터링을 하려면 각 팀원이 다른 사람의 작업을 방해하지 않으면서 언제든지 리팩터링 할 수 있어야 한다.  지속적 통합을 적극 권장하는 이유도 바로 이 때문이다. 지속적 통합을 적용하면 팀원 각자가 수행한 리팩터링 결과를 빠르게 동료와 공유할 수 있다. 따라서 자가 테스트 코드 , 지속적 통합, 리팩터링이라는 세 기법은 서로 강력한 상승효과를 발휘한다. 위의 세 실천법을 적용한다면 YAGNI 설계 방식으로 개발을 진행 할 수 있다고 한다.

YAGNI가 뭐냐면 기존에 유연성 매커니즘을 통해 소프트웨어를 설계하는 방식과 다르게 리팩터링을 기반으로 다르게 접근할 수가 있는데 유연성이 필요하고 어떻게 해야 앞으로 그 변화에 가장 잘 대응할 수 있을지 추측하지 않고, 그저 현재까지 파악한 요구사항만을 해결하는 소프트웨어를 구축한다. 진행하면서 요구사항을 더 잘 이해하게 되면 아키텍처도 그에 맞게 리팩터링 하는 것이다. 이런식으로 설계하는 방식을 간결한 설계 점진적 설계 , YAGNI(you aren't going to need it)이라고 한다. 저자는 선제적인 아키텍처에 소홀해도 된다고 말하는 게 아니다. 분명히 리팩터링으로는 변경하기 어려워서 미리 생각해두면 시간이 절약되는 경우도 얼마든지 있다고 한다. 하지만 나중에 문제를 더 깊이 이해하게 됐을 때 처리하는 쪽이 훨씬 낫다고 생각한다.

 

리팩터링에 대해 개념적인 부분을 전반적으로 다뤘다. 얘기로만 들었던 TDD와 CI/CD 가 리팩터링과 어떤 관계인지 조금은 알게 된거 같다. 

 

 

참고 및 출처: <리팩터링 2판> (마틴 파울러 지음, 개앞맵시, 남기혁 옮김, 한빛미디어, 2020)