Chapter 2 리팩터링 원칙
리팩터링: [명사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법
리팩터링: [동사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러가지 리팩터링 기법을 적용해서 소프트웨어를 재구성하다.
특정한 방식에 따라 코드를 정리하는 것만이 리팩터링이다. 리팩터링은 결국 동작을 보존하는 작은 단계들을 거쳐 코드를 수정하고, 이러한 단계들을순차적으로 연결하여 큰 변화를 만들어내는 일이다. 개별 리팩터링은 아주 작을 수도 있고, 작은 단계 여러 개가 합쳐진 모습일 수도 있다. 따라서 리팩터링하는 동안에는 코드가 항상 정상 작동하기 때문에 전체 작업이 끝나지 않았더라도 언제든 멈출 수 있다.
누군가 "리팩터링하다가 코드가 깨져서 며칠이나 고생했다"라고 한다면, 십중팔구 리팩터링한 것이 아니다.
리팩터링하는 이유
리팩터링이 소프트웨어의 모든 문제점을 해결하는 만병통치약은 절대 아니다. 하지만 코드를 건강한 상태로 유지하는 데 도움을 주는 것은 분명하다.
리팩터링하면 소프트웨어 설계가 좋아진다.
리팩터링하지 않으면 소프트웨어의 내부 설계(아키텍처)가 썩기 쉽다. 아키텍처를 충분히 이해하지 못한 채 단기 목표만을 위해 코드를 수정하다 보면 기반 구조가 무너지기 쉽다. 그러면 코드만 봐서는 설계를 파악하기 어려워진다. 코드만으로 설계를 파악하기 어려워질수록 설계를 유지하기 어려워지고, 설계는 결국 부패한다. 반면 규칙적인 리팩터링은 코드의 구조를 지탱해줄 것이다.
같은 일을 하더라도 설계가 나쁘면 코드가 길어지기 십상이다. 사실상 같은 일을 하는 코드가 여러 곳에서 나타날 수 있기 때문이다. 그래서 중복 코드 제거는 설계 개선 작업의 중요한 한 축을 차지한다. 코드량이 줄면 수정하는데 드는 노력은 크게 줄어든다. 중복 코드를 제거하면 모든 코드가 언제나 고유한 일을 수행함을 보장할 수 있고 이 것이 바람직한 설계의 핵심이다.
리팩터링하면 소프트웨어를 이해하기 쉬워진다
코드를 이해하기 쉽게 만들려면 일하는 리듬에 변화를 줘야 한다. 리팩터링은 코드가 더 잘 읽히게 도와준다. 잘 동작하지만 이상적인 구조는 아닌 코드가 있다면 잠깐 시간을 내서 리팩터링해보자. 그러면 코드의 목적이 더 잘 드러나고 내 의도를 더 명확하게 전달하도록 개선할 수 있다.
리팩터링하면 버그를 쉽게 찾을 수 있다
코드를 이해하기 쉽다는 말은 버그를 찾기 쉽다는 말이기도 하다. 리팩터링하면 코드가 하는 일을 깊이 파악하게 되면서 새로 깨달은 것을 곧바로 코드에 반영하게 된다. 프로그램의 구조를 명확하게 다듬으면 그냥 '이럴 것이다'라고 가정하던 점들이 분명 드러나는데, 버그를 지나치려야 지나칠 수 없을 정도까지 명확해진다.
리팩터링하면 프로그래밍 속도를 높일 수 있다
지금까지 위의 내용을 "리팩터링하면 코드 개발 속도를 높일 수 있다"로 정리할 수 있다.
한 시스템을 오래 개발하게 되면 초기에는 진척이 빨랐지만 시간이 지나고, 추가 사항이 많아지면 기능을 하나 추가하는 데 훨씬 오래 걸리게 된다. 새로운 기능을 추가할수록 기존 코드베이스에 잘 녹여낼 방법을 찾는 데 드는 시간이 늘어나게 된다. 게다가 기능을 추가하고 나면 버그가 발생하는 일이 잦고, 이를 해결하는 시간은 한층 더 걸린다. 코드베이스는 패치에 패치가 덧붙여지면서 프로그램의 동작을 파악하기가 어려워진다. 이러한 부담이 기능 추가 속도를 계속 떨어뜨리면서, 차라리 처음부터 새로 개발하는 편이 낫겠다고 생각하는 지경에 이른다.
그런데 어떤 팀은 이와 전혀 다른 양상을 보인다. 기존에 작성한 코드를 최대한 활용할 수 있어서 새 기능을 더 빨리 추가한다. 이렇게 차이가 나는 원인은 소프트웨어의 내부 품질에 있다. 내부 설계가 잘 된 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있다. 모듈화가 잘 되어 있으면 전체 코드베이스 중 작은 일부만 이해하면 된다. 코드가 명확하면 버그를 만들 가능성도 줄고, 버그를 만들더라도 디버깅하기가 훨씬 쉽다. 내부 품질이 뛰어난 코드베이스는 새 기능 구축을 돕는 견고한 토대가 된다.
내부 설계에 심혈을 기울이면 소프트웨어의 지구력이 높아져서 빠르게 개발할 수 있는 상태를 더 오래 지속할 수 있다. 20년 전에는 코딩을 시작하기 전에 설계부터 완벽히 마쳐야 한다는 것이 정설이었다. 코딩 단계에 한번 들어서면 코드가 부패할 일만 남기 때문이다. 하지만 리팩터링하면 이를 바로잡을 수 있다. 리팩터링하면 기존 코드의 설계를 얼마든지 개선할 수 있으므로, 설령 프로그램 요구사항이 바뀌더라도 설계를 지속해서 개선할 수 있다. 처음부터 좋은 설계를 마련하기란 매우 어렵다. 그래서 빠른 개발이라는 숭고한 목표를 달성하려면 리팩터링이 반드시 필요하다.
언제 리팩터링해야 할까?
준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기
리팩터링하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다. 이 시점에 현재 코드를 살펴보면서, 구조를 살짝 바꾸면 다른 작업을 하기가 훨씬 쉬워질 만한 부분을 찾는다.
이해를 위한 리팩터링: 코드를 이해하기 쉽게 만들기
코드를 수정하려면 먼저 그 코드가 하는 일을 파악해야 한다. 코드의 의도가 더 명확하게 드러나도록 리팩터링할 여기는 없는지 찾아보자. 조건부 로직의 구조가 이상하지 않은지, 함수의 이름을 잘못 정해서 실제로 하는 일을 파악하는 데 시간이 오래 걸리지는 않는지 살펴보자.
코드를 분석할 때 리팩터링을 해보면, 그렇지 않았더라면 도달하지 못했을 더 깊은 수준까지 이해하게 된다.
쓰레기 줍기 리팩터링
코드를 파악하던 중에 일을 비효율적으로 처리하는 모습을 발견할 때가 있다. 로직이 쓸데없이 복잡하거나, 매개변수화한 함수 하나면 될 일을 거의 똑같은 함수 여러 개로 작성해놨을 수 있다. 이때 약간 절충을 해야 한다. 간단히 수정할 수 있는 것은 즉시 고치고, 시간이 좀 걸리는 일은 짧은 메모만 남긴 다음, 하던 일을 끝내고 나서 처리한다. 이것이 쓰레기 줍기 리팩터링이다.
물론 수정하려면 몇 시간이나 걸리고 당장은 더 급한 일이 있을 수 있다. 그렇더라도 조금이나마 개선해두는 것이 좋다. 캠핑 규칙이 제안하듯, 항상 처음 봣을 때보다 깔끔하게 정리하고 떠나자. 코드를 훑어볼 때마다 조금씩 개선하다 보면 결국 문제가 해결될 것이다. 리팩터링의 멋진 점은 각각의 작은 단계가 코드를 깨뜨리지 않는다는 사실이다. 그래서 작업을 잘게 나누면 몇 달에 걸쳐 진행하더라도 그 사이 한 순간도 코드가 깨지지 않는다.
계획된 리팩터링과 수시로 하는 리팩터링
앞에서 본 준비를 위한 리팩터링, 이해를 위한 리팩터링, 쓰레기 줍기 리팩터링은 모두 기회가 될 때만 진행한다.
보기 싫은 코드를 발견하면 리팩터링하자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야 한다.
무언가 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고 그런 다음 쉽게 수정하자.
뛰어난 개발자는 새 기능을 추가하기 쉽도록 코드를 수정하는 것이 그 기능을 가장 빠르게 추가하는 길일 수 있음을 안다. 소프트웨어 개발을 끝이 있는 작업으로 보면 안 된다. 새 기능이 필요할 때마다 소프트웨어는 이를 반영하기 위해 수정된다. 이때 새로 작성해 넣는 코드보다 기존 코드의 수정량이 큰 경우가 대체로 많다.
계획된 리팩터링이 무조건 나쁜 것은 아니다. 그동안 리팩터링에 소홀했다면, 따로 시간을 내서 새 기능을 추가하기 쉽도록 코드베이스를 개선할 필요가 있다. 이때 리팩터링에 투자한 일주일의 효과를 다음 몇 달 동안 누릴 수도 있다. 한편, 정기적으로 리팩터링 하더라도 어떤 문제는 팀원 여럿이 달려들어야 할 정도로 곪아갈 수도 있다. 하지만 이런 이유로 계획된 리팩터링을 하게 되는 일은 최소한으로 줄여야 한다. 리팩터링 작업 대부분은 드러나지 않게, 기회가 될 때마다 해야 한다.
오래 걸리는 리팩터링
리팩터링은 대부분 몇 분 안에 끝난다. 길어야 몇 시간 정도다. 하지만 팀 전체가 달려들어도 몇 주는 걸리는 대규모 리팩터링도 있다. 라이브러리를 새 것으로 교체하는 작업일 수도 있고, 일부 코드를 다른 팀과 공유하기 위해 컴포넌트로 빼내는 작업일 수도 있다. 또는 그동안 작업하면서 쌓여온 골치 아픈 의존성을 정리하는 작업일 수도 있다.
이런 상황에 처하더라도 저자는 팀 전체가 리팩터링에 매달리는 데는 회의적인 의견이다. 그보다는 주어진 문제를 몇 주에 걸쳐 조금씩 해결해가는 편이 효과적일 때가 많다. 누구든지 리팩터링해야할 코드와 관련한 작업을 하게 될 때마다 원하는 방향으로 조금씩 개선하는 식이다. 리팩터링이 코드를 깨트리지 않는 장점을 활용하자. 일부를 변경해도 모든 기능이 항상 올바르게 동작한다. 예를 들어, 라이브러리를 교체할 때는 기존 것과 새 것 모두를 포용하는 추상 인터페이스부터 마련한다. 기존 코드가 이 추상 인터페이스를 호출하도록 만들고 나면 라이브러리를 훨씬 쉽게 교체할 수 있다.
코드 리뷰에 리팩터링 활용하기
리팩터링은 다른 이의 코드를 리뷰하는 데도 도움이 된다. 리팩터링을 활용하기 전에는 코드를 읽고, 그럭저럭 이해한 뒤, 몇 가지 개선 사항을 제시했다. 리팩터링을 활용하게 되면서 새로운 아이디어가 떠오르면 리팩터링하여 쉽게 구현해넣을 수 있는지부터 살펴보게 된다. 쉽다면 실제로 리팩터링한다. 이 과정을 몇 번 반복하면 내가 떠올린 아이디어를 실제로 적용했을 때의 모습을 더 명확하게 볼 수 있다. 머리로만 상상하는 게 아니라 눈으로 직접 확인할 수 있다. 그러다 보면 리팩터링해보지 않고는 절대 떠올릴 수 없던 한 차원 높은 아이디어가 떠오르기도 한다.
관계자에게는 뭐라고 말해야 할까?
리팩터링에 회의적인 관리자일 경우 리팩터링한다고 말하지 말자.
소프트웨어 개발자는 프로다. 프로 개발자의 역할은 효과적인 소프트웨어를 최대한 빨리 만드는 것이다. 리팩터링하면 소프트웨어를 빠르게 만드는 데 아주 효과적이다. 새 함수를 추가하려는데 현재 설계가 적합하지 않다면 먼저 리팩터링하고 나서 함수를 추가하는 편이 빠르다. 버그를 수정하려면 현재 소프트웨어의 작동 방식을 이해해야 한다. 이때도 리팩터링부터 하는 편이 가장 빠르다. 프로 개발자에게 주어진 임무는 새로운 기능을 빠르게 구현하는 것이고, 가장 빠른 방법은 리팩터링이다.
리팩터링하지 말아야 할 때
외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 두자. 내부 동작을 이해해야 할 시점에 리팩터링해야 효과를 제대로 볼 수 있다.
리팩터링하는 것보다 처음부터 새로 작성하는 게 쉬울 때도 리팩터링하지 않는다. 하지만 직접 리팩터링해보기 전에는 어느 쪽이 쉬운지 확실히 알 수 없을 때가 많기 때문에 뛰어난 판단력과 경험이 뒷받침돼야 한다.
리팩터링과 소프트웨어 개발 프로세스
리팩터링의 첫 번째 토대는 자가 테스트 코드다. 프로그래밍 도중 발생한 오류를 확실히 걸러내는 테스트를 자동으로 수행할 수 있어야 한다. 테스트는 리팩터링에 굉장히 중요한 토대이다.
팀으로 개발하면서 리팩터링을 하려면 각 팀원이 다른 사람의 작업을 방해하지 않으면서 언제든지 리팩터링할 수 있어야 한다. 지속적 통합(CI)을 적극 권장하는 이유도 이 때문이다. 지속적 통합을 적용하면 팀원 각자가 수행한 리팩터링 결과를 빠르게 동료와 공유할 수 있다. 그래서 조만간 삭제될 인터페이스를 이용하여 새로운 기능을 추가하는 일을 방지할 수 있고, 리팩터링한 결과가 다른 팀원의 작업에 문제를 일으키면 즉시 알아낼 수 있다. 자가 테스트 코드 역시 지속적 통합의 핵심 요소다. 따라서 자가 테스트 코드, 지속적 통합, 리팩터링이라는 세 기법은 서로 강력한 상승효과를 발휘한다.
리팩터링과 성능
소프트웨어를 이해하기 쉽게 만들기 위해 속도가 느려지는 방향으로 수정하는 경우가 많다. '직관적인 설계 vs 성능'은 중요한 주제다. 리팩터링하면 소프트웨어가 느려질 수도 있는 건 사실이다. 하지만 그와 동시에 성능을 튜닝하기는 더 쉬워진다.
성능에 대한 흥미로운 사실은 대부분 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비한다는 것이다. 그래서 코드 전체를 고르게 최적화한다면 그중 90%는 효과가 거의 없기 때문에 시간 낭비인 셈이다.
외도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경 쓰지 않고 코드를 다루기 쉽게 만드는데 집중하자. 그리고 성능 최적화 단계가 되면 다음의 구체적인 절차를 따라 프로그램을 튜닝한다.
- 프로파일러로 프로그램을 분석하여 시간과 공간을 많이 잡아먹는 지점을 알아낸다.
- 해당 부분을 개선한다.
- 리팩터링할 때처럼 최적화를 위한 수정도 작은 단계로 나눠서 진행한다.
프로그램을 잘 리팩터링해두면 이런 식의 최적화에 도움이 된다. 일단 성능 튜닝에 투입할 시간을 벌 수 있고, 성능을 더 세밀하게 분석할 수 있다. 프로파일러가 지적해주는 코드의 범위가 더 좁아질 것이고, 그래서 튜닝하기가 쉬워진다. 코드가 깔끔하면 개선안들이 더 잘 떠오를 것이고, 그중 어떤 튜닝이 효과가 좋을지 파악하기 쉽다.