Skip to main content

2장 리팩터링 원칙

written by
saengmotmi
saengmotmi 🏆Front End Engineer

2.1 리팩터링 정의#

마틴 파울러는 리팩터링이라는 용어를 다음과 같이 정의하고 있다.

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

리팩터링(하다): [동사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러 가지 리팩터링 기법을 적용해서 소프트웨어를 재구성하다.

우리는 일반적으로 코드를 어떠한 방식으로 고치든 그 과정 자체를 리팩터링이라고 부른다. 마틴 파울러는 그럴 생각이 없는 듯 하다. 왜냐하면 특정한 '리팩터링 기법'에 따라 정리하는 것만이 리팩터링에 해당한다고 주장하기 때문이다.

1장의 예제 코드에서 보았던 함수 추출하기조건부 로직을 다형성으로 바꾸기 같은 기법들로 국한하여 생각하겠다는 뜻이다. 이러한 생각은 다음 문장에서도 잘 드러난다.

"나는 코드베이스를 정리하거나 구조를 바꾸는 모든 작업을 '재구성 restructuring'이라는 포괄적인 용어로 표현하고, 리팩터링은 재구성 중 특수한 형태로 본다. (...) 이렇게 잘게 나눔으로써 오히려 작업을 더 빨리 처리할 수 있다. 단계들이 체계적으로 구성되어 있기도 하고, 무엇보다 디버깅하는 데 시간을 뺏기지 않기 때문이다." - 80p

설명에서 알 수 있듯, 리팩터링을 진행할 때는 '약속된 기법'에 의해 '잘게 나누어 진행'해야 한다. '체계적'이며 '효율적'이기 때문이다. 그래서 마틴 파울러가 생각하는 리팩터링은 몇 주를 할당해서 진행하는 작업이 아니라 매우 짧은 호흡의 작업이다.

이 과정에서 사용자 관점에서 코드가 변경되면 안된다. 버그가 존재했다면, 차라리 그 버그는 수정되면 안된다. 왜냐하면 리팩터링의 목적은 단지 코드를 이해하고 수정하기 쉽게 만드는 것에 있기 때문이다.

2.2 두 개의 모자#

켄트 벡은 기능 추가리팩터링 두 가지 작업을 철저히 나누어 작업한다고 한다. 마틴 파울러는 이를 '두 개의 모자'에 비유했다. 예컨대 기능을 추가할 때는 '기능 추가' 모자를 쓰고 기존 코드는 절대 건드리지 않고 기능만 추가한다. 리팩터링을 할 때는 '리팩터링' 모자를 쓰고 절대 기능을 추가 하지 않고, 코드를 재구성하는 데만 집중한다.

만약 새 기능을 추가하려고 할 때 코드 구조를 바꿔야 작업하기 편하겠다는 생각이 들면, 일단 멈춰서 리팩터링 모자를 쓰고 먼저 코드를 정리한다. 리팩터링이 끝나고 나서야 다시 기능 추가를 모자를 쓴다. 전환의 간극이 짧더라도 가급적 그렇게 한다.

2.3 리팩터링하는 이유#

위에서 간략히 설명하기는 했지만 리팩터링을 해야 하는 몇 가지 이유는 다음과 같다.

1) 리팩터링 하면 소프트웨어 설계가 좋아진다#

마틴 파울러는 리팩터링을 하지 않을 경우 내부 구조(아키텍처)가 '썩기 쉽다'고 표현한다. 아키텍처를 이해하지 못한 채 단기 목표만을 위해 기능을 추가하다 보면 기반 구조가 무너질 수 있다는 것이다. 코드를 보고 설계를 유추하기 어려워지고, 코드 구조가 무너지면 이러한 붕괴는 가속화 된다. 리팩터링은 정확히 이것과 반대 효과를 낸다. 규칙적으로 리팩터링 하여 코드의 구조를 지탱하자.

2) 리팩터링 하면 소프트웨어를 이해하기 쉬워진다#

컴퓨터는 정확히 시킨 대로만 한다. 그렇기 때문에 언제나 컴퓨터에게 시키고 싶은 일과, 이를 표현한 코드의 차이를 충분히 줄여야 한다. 좋은 프로그램은 내가 원하는 바를 적절히 반영하고 있어야 한다.

하지만 컴퓨터와 소통하는 건 기본적이고, 결국 코드를 같이 읽어야 하는 동료와 미래의 내 자신을 위해 읽기 좋은 코드를 작성해야 한다. 누군가 기능 수정을 위해 코드를 파악하느라 헛된 시간을 소모하도록 하지 말자. 이 또한 리팩터링을 통해 달성할 수 있다.

3) 리팩터링 하면 버그를 쉽게 찾을 수 있다#

이해하기 쉬운 코드에서는 버그 또한 쉽게 발견할 수 있다.

4) 리팩터링하면 프로그래밍 속도를 높일 수 있다#

앞서 언급한 장점들을 종합하면 결국 리팩터링은 개발 속도를 높인다라는 결론에 도달하게 된다. 하지만 항상 기능 개발이 급한 경우 리팩터링은 개발 속도를 저해한다고 생각하기 쉽다.

하지만 실제는 그렇지 않다. 개발 초기를 지나고 어느 정도 코드베이스에 코드가 쌓이기 시작하면, 새로운 기능을 추가할 때 기존 코드베이스에 내가 작성한 로직을 추가하는 적절한 방법을 찾기 까지 꽤나 오랜 시간을 소모하게 된다. 버그 해결 또한 마찬가지다. 결국 차라리 새로 개발하는게 낫겠다고 생각하게 된다.

반면 코드 베이스를 깔끔하게 유지하는 팀은 모듈화가 잘 이뤄져 있는 코드를 기반으로 작업한다. 새롭게 기능이 추가될 때 작업해야 할 장소나 방법을 찾기 쉽다. 코드베이스의 모든 부분을 알지 못해도, 관심사의 분리만 잘 이뤄져 있다면 코드의 작은 부분만 신경쓰면 된다.

마틴 파울러는 이러한 현상을 설계 지구력 가설 Design Stamina Hypothesis이라고 부른다. 내부 설계에 신경을 쓸 수록 소프트웨어의 지구력이 높아지고, 빠르게 개발할 수 있는 상태를 더 오래 유지할 수 있다. 마치 사람이 달리기를 할 때 심폐 지구력이 좋아지면 더 높은 페이스로 오래 달릴 수 있듯이 말이다.

2.4 언제 리팩터링해야 할까?#

수강생들을 가르칠 때 '이 작업들이 다 끝나고 나서 리팩터링 하겠다'는 이야기를 많이 들었다. 하지만 '이 작업들이 다 끝나고 나서'라는 순간은 영영 오지 않는다. 손을 대더라도 이미 늦은 시점이 되었을 수도 있다.

마틴 파울러는 거의 1시간에 한번씩 리팩터링을 한다고 한다. 나도 작업을 진행하면서 틈이 날 때마다 코드를 고치는 편이다. 리팩터링 시점에 대한 더 구체적인 가이드가 있으면 좋겠다. 예를 들면 돈 로버츠가 제안했다는 3의 법칙 같은 것들 말이다.

  1. 처음에는 그냥 한다.

  2. 비슷한 일을 두 번째로 하게 되면(중복이 생겼다는 사실에 당황스럽겠지만), 일단 계속 진행한다.

  3. 비슷한 일을 세 번째 하게 되면 리팩터링한다.

일종의 삼진 아웃이라고 생각해도 좋을 것이다.

리팩터링의 목적 또한 조금 더 디테일하게 나눠볼 수 있을 것 같다.

1) 준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기#

리팩터링 하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다. 일을 시작하기 전에 방 정리를 먼저 하는 것과 비슷하다. 코드를 살펴보면서 구조를 살짝 바꾸면 다른 작업이 용이해질만한 부분을 찾아본다.

이미 만들어진 함수가 있는데, 그 함수 내의 일부 고정값 때문에 재사용할 수 없는 경우가 있을 수 있다. 이 경우 함수를 복사해 값을 조금 수정한 버전으로 만들어 쓸 수도 있고, 해당 부분을 매개변수화 할 수도 있다. 우리는 마땅히 후자를 선택해야 한다.

버그를 수정할 때도 마찬가지다. 버그가 있는 함수가 세 군데에 퍼져있다면 우선 한 곳에 모아놓고, 그 다음에 해결하는 것이 편하다.

리팩터링은 정글을 탐험할 때 지금 당장 '직진'을 외치고 앞으로 나아갈 수도 있지만, 잠시 시간을 내 더 빠른 지름길을 찾는 행위라고 할 수도 있겠다.

2) 이해를 위한 리팩터링: 코드를 이해하기 쉽게 만들기#

워드 커닝햄은 '리팩터링하면 머리로 이해한 것을 코드에 옮겨담을 수 있다'고 말했다. 리팩터링 과정에서 그 코드가 하는 일을 보다 명확하게 표현할 기회를 얻을 수 있다는 뜻이다.

조건부 로직의 구조가 이상하거나, 함수나 변수 이름을 잘못 정해서 값의 목적을 파악하는데 오래 걸리는 경우를 리팩터링을 통해 바로잡을 수 있다.

3) 쓰레기 줍기 리팩터링#

코드를 파악하는 중 비효율적인 동작을 발견할 수 있다. 원래 하려던 일이 있을 때 이런 코드들을 발견하면 갈등되기 마련이다. 간단히 처리할 수 있는 문제는 빠르게 해결하고, 그렇지 않은 경우에는 우선 메모를 해두고 나중에 처리한다.

'Kick the can down the road'라는 표현이 있다. 내리막 길에서 걷어찬 캔은 내 시야에서만 사라졌을 뿐 언젠가 반드시 다시 만나게 되리라는 뜻이고, 그렇게 될 수밖에 없을 것이다.

캠핑을 하는 사람들이 '항상 처음 봤을 때보다 깔끔하게 정리하고 떠나라'는 규칙을 지켜야 하듯이 코드를 작성하는 우리도 그렇게 해야 한다. 만날 때마다 조금씩이라도 고쳐둔다면 큰 문제도 결국 해결될 것이다.

4) 계획된 리팩터링과 수시로 하는 리팩터링#

앞서 언급한 리팩터링들은 기회가 될 때만 진행하는 리팩터링이다. 마틴 파울러는 리팩터링은 프로그래밍과 구분되는 별개의 활동이 아니라고 말한다. if문 작성 시간을 따로 잡지 않는 것처럼 프로그래밍 과정에 리팩터링을 자연스럽게 녹여야 한다고 주장한다.

사람들은 흔히 소프트웨어 개발이 뭔가 '추가'하는 과정이라고 여긴다. 기능을 추가하다보면 코드가 늘어나기 때문이다. 하지만 마틴 파울러는 뛰어난 개발자는 새 기능을 추가하기 쉽도록 코드를 '수정'하는 것이 그 기능을 가장 빠르게 추가하는 길일 수 있다고 설명한다. 때로는 새로 작성해넣는 코드보다 기존 코드의 수정량이 큰 경우가 많을 수도 있다.

그렇다고 물론 계획된 리팩터링이 무조건 나쁘다는 말은 아니다. 하지만 이미 곪은 코드 베이스를 고치는 건 쉽지 않은 일이다. 그래서 이런 대형 작업은 최소화 되어야 하고, 따라서 대부분의 리팩터링은 기회가 될 때마다 해야 한다.

5) 오래 걸리는 리팩터링#

마틴 파울러는 리팩터링이 대부분 몇 분에서 몇 시간 안에 끝난다고 말한다. 하지만 종종 팀 전체가 붙어도 몇 주가 걸리는 대규모 리팩터링도 있을 수 있다고 말한다. 예를 들면 라이브러리를 새 것으로 교체하거나 일부 코드를 다른 팀과 공유하기 위해 컴포넌트로 분리하는 작업, 혹은 얽혀 있는 의존성을 제거하는 작업일 수도 있다.

하지만 이런 경우에라도 팀 전체가 리팩터링에 투입되는 것에는 회의적이라고 말한다. 리팩터링은 더 빠른 길을 찾아 고민하는 시간이어야 한다. 아무리 거대한 작업이라도 말이다.

라이브러리를 교체할 때는 기존 것과 새 것 모두를 포용하는 추상 인터페이스를 마련하는 방식(추상화로 갈아타기)으로 접근할 수도 있다. 일단 기존 코드가 추상 인터페이스(ex. Wrapper Class)를 호출하도록 만들고 나면 라이브러리를 훨씬 쉽게 교체할 수 있다.

6) 코드 리뷰에 리팩터링 활용하기#

코드 리뷰를 통해 개발팀 전체에 지식을 전파할 수 있다. 시니어 개발자의 노하우를 전수하거나 다른 개발자의 아이디어를 듣고 의견을 더해 발전시킬 수도 있다.

리팩터링은 코드 리뷰에도 도움이 된다. 코드를 보고 아이디어가 떠올랐을 때 직접 리팩터링 해본다. 그러다 보면 더 높은 차원의 개선안이 나오기도 한다. 이러한 과정에서 코드 리뷰의 결과물이 구체적으로 도출되기도 한다.

PR 리뷰 보다는 페어 프로그래밍으로 진행되는 리뷰 형태에서 더욱 효과적일 수 있다. 프로그래밍 과정 안에 지속적인 코드 리뷰가 녹아있기 때문이다.

7) 관리자에게는 뭐라고 말해야 할까?#

리팩터링이 '누적된 오류를 잡는 일이거나, 가치 있는 기능을 만들어내지 못하는 작업'이라고 생각하는 조직에서는 리팩터링이 금기어처럼 여겨질 수도 있다. 앞서 설명했듯 리팩터링에 몇 주씩 일정을 잡거나, 제대로 된 리팩터링이 아니라 어설픈 재구성 restructuring 작업을 하면서 코드베이스를 망가뜨리는 경험을 겪은 조직이라면 더욱 그러할 것이다.

설계 지구력 가설에 동의하지 못하는 관리자나 고객을 설득해야 하는 상황이라고 해보자. 이들은 코드 베이스의 건강 상태가 생산성에 미치는 영향을 이해하지 못한다. 마틴 파울러는 그럴 경우 '리팩터링한다고 말하지 말라'고 조언한다고 한다.

하극상이라고 생각하기 보다는 그저 '효과적인 소프트웨어를 최대한 빨리 만드는 작업'을 한다고 생각하자. 리팩터링이 바로 그 방법이다.

8) 리팩터링하지 말아야 할 때#

무조건 적으로 리팩터링 하라고 말하는 것은 아니다. 지저분하지만 굳이 고치지 않아도 될 때는 리팩터링 하지 않는다. 외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 둔다. 이러한 코드들의 내부 동작을 이해해야 할 시점이 왔을 때 작업에 착수해도 늦지 않다.

고치는 것보다 새로 만드는게 쉬울 때도 리팩터링 하지 않는다. 물론 직접 해보기 전엔 결과를 알기 쉽지 않을 수 있다. 경험의 영역이기 때문이다.

2.5 리팩터링 시 고려할 문제#

리팩터링은 좋은 기법이지만 모든 기술과 기법이 그러하듯 리팩터링 역시 발생할 수 있는 문제가 있다. 이 부분들에 대해 살펴보자.

1) 새 기능 개발 속도 저하#

리팩터링으로 인해 기능 개발 속도가 느려진다고 생각하는 사람이 많다. 하지만 마틴 파울러는 꾸준히 '리팩터링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것'이라고 설명한다.

물론 상황에 맞게 적용해야 한다. 대대적인 리팩터링이 필요하지만, 추가 하려는 기능은 아주 미미하게 작을 때 고민이 될 수 있다. 이런 것들은 역시 경험의 영역이기에 정량화 하기는 쉽지 않다.

그래서 마틴 파울러는 이럴 때 준비를 위한 리팩터링 (기능을 쉽게 추가하게 만들기)을 하면 좋다고 주장한다. 새 기능을 구현해넣기 편해지겠다 싶다면 주저 없이 리팩터링을 먼저 한다. 반면 직접 건드릴 일이 없거나 크게 불편하지 않다면 하지 않는다. 혹은 어떻게 할지 뚜렷하게 떠오르지 않는다면 리팩터링을 미룰 수도 있다. 개발자라면 언제나 건강한 코드 베이스를 유지하기 위해 마땅히 노력해야 한다.

다만 마틴 파울러는 리팩터링을 '클린 코드'나 '바람직한 엔지니어링 습관'처럼 도덕적인 이유로 정당화 해서는 안된다고 말한다. 리팩터링의 본질은 '개발 시간 단축'에 있기 때문이다. 오로지 경제적인 이유로 하는 것이다. 무조건 적으로 옹호하기 보다 항상 이 점을 명심해야 한다.

2) 코드 소유권#

고치는 과정에서 다른 팀 혹은, 다른 개발자가 작업한 코드라서 수정할 수 없을 때가 있다. 이럴 때는 기존 함수를 그대로 유지하되 새 함수를 호출하도록 하거나, 호출하는 함수 이름을 바꾸는 방식을 선택할 수 있다. 인터페이스는 복잡해지지만 클라이언트에 영향을 주지 않기 위해서는 어쩔 수 없는 선택이다.

그렇기 때문에 팀원이라면 누구나 팀이 소유한 코드를 수정할 수 있어야 한다. 코드에 대한 책임은 자신이 맡은 영역의 변경 사항을 관리하라는 뜻이지 다른 사람이 수정하지 못하도록 막으라는 게 아니다.

3) 브랜치#

독립 브랜치로 작업하는 것의 장점은 명확하다. 하지만 독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터로 통합하기가 어려워진다. 그래서 수시로 merge하거나 rebase 한다. 하지만 여러 기능 브랜치에서 동시에 개발한다면 이 또한 어려울 수 있다.

마틴 파울러는 머지통합을 구분하자고 주장한다. 머지는 단방향이다. 브랜치만 바뀌고 마스터는 그대로다. 반면 통합은 마스터를 개인 브랜치로 가져와서 작업한 결과를 다시 마스터에 올리는 양방향 처리를 뜻한다. 그래서 마스터와 브랜치가 모두 변경된다. 통합하는 간격을 좁히자는 것이고, 이 것을 지속적 통합 Continuous Integration이라고 부른다.

자주 수정하고 자주 통합하는 과정이 리팩터링과 잘 어울리기 때문에 권장한다. 기능별 브랜치라도 자주 통합된다면 문제 발생 가능성을 크게 줄일 수 있다.

4) 테스팅#

리팩터링은 '겉보기 동작'을 유지하며 내부 구조를 수정하는 일이다. 하지만 겉보기 동작이 유지되는지 어떻게 알 수 있을까? 그래서 테스트가 필요하다.

테스트 주기가 짧다면 단 몇 줄만 비교하면 되고, 문제를 일으킨 부분이 그 몇줄 안에 있기 때문에 버그를 훨씬 쉽게 찾을 수 있다. 리팩터링이 실패할 수도 있다는 불안감도 해소할 수 있다.

5) 레거시 코드#

거대한 레거시 시스템을 파악할 때 리팩터링이 도움이 된다. 하지만 조금의 수정이 큰 재앙을 일으킬 수도 있다. 이 경우 역시 테스트가 필요하다.

<레거시 코드 활용 전략>이라는 책에서는 '프로그램에서 테스트를 추가할 틈새를 찾아서 시스템을 테스트 해야 한다'고 조언한다. 이러한 틈새를 만들 때 리팩터링이 필요하다. 위험하지만 감내해야 할 위험이다. 어렵지만 방법이 없다. 그래서 처음부터 테스트가 필요한 이유다.

6) 데이터베이스#

프라모드 사달게가 개발한 '진화형 데이터베이스 설계'와 '데이터베이스 리팩터링' 기법을 사용하면 커다란 변경들을 쉽게 조합하고 다룰 수 있다. 마이그레이션 스크립트를 작성하고, 접근 코드와 데이터베이스 스키마에 대한 구조적 변경을 스크립트로 처리하도록 할 수 있다(ex. yoyo migration).

마틴 파울러는 데이터베이스 리팩터링은 프로덕션 환경에 여러 단계로 나눠서 릴리스 하라고 조언한다. 이렇게 하면 변경에서 문제가 생겼을 때 되돌리기 쉽기 때문이다.

예를 들면 필드 이름을 바꿀 때 첫 번째 커밋에서는 새로운 데이터베이스 필드를 추가만 하고 사용하지는 않는다. 그 다음 기존 필드와 새 필드를 동시에 업데이트 한다. 그 다음에는 데이터베이스를 읽는 클라이언트들을 새 필드로 사용하는 버전으로 조금씩 교체한다. 그 후 예전 필드를 삭제한다.

2.6 리팩터링, 아키텍처, 애그니(YAGNI)#

마틴 파울러는 우리가 작성하는 소프트웨어에 변경에 유연하게 대처할 수 있는 유연성 메커니즘 flexibility mechanism을 심어두라고 조언한다.

일반적으로 변경에 유연하게 대처한다고 하면 함수에 매개변수를 추가하는 등의 방법을 생각하기 쉽다. 하지만 예측하면 안된다. 그저 현재까지 파악한 요구사항만을 해결하는 코드를 짜고, 추후 발생하는 변경은 리팩터링으로 해결한다. 이름을 바꾸는 등의 리팩터링은 실컷 해도 좋지만, 매개변수를 추가하는 등의 복잡도를 높일 수 있는 유연성 메커니즘은 반드시 검증을 거친 후 추가한다.

이런 방식을 점진적 설계 incremental design 혹은 YAGNI (You aren't going to need it) 등으로 부른다.

2.7 리팩터링과 소프트웨어 개발 프로세스#

리팩터링과 테스트 코드 작성을 묶어 테스트 주도 개발(TDD) 라고 부른다.

리팩터링의 첫 번째 토대는 테스트 코드 작성이다. 오류를 확실히 걸러내는 테스트를 자동으로 수행하도록 해야 한다. 마틴 파울러는 테스트의 중요성을 계속해서 강조한다. 이를 기반으로 수시로 리팩터링하고, 또 이러한 변경을 수시로 코드 베이스에 통합해야 한다.

이 세 가지 요소들이 합쳐졌을 때 YAGNI에 기반한 설계 방식으로 개발을 진행할 수 있다. 세 가지 조건이 YAGNI의 토대인 동시에 YAGNI로 인해 개발이 빨라질 수 있다. 하지만 이를 실천하는 건 쉽지 않으므로, 충분한 연습과 실력이 뒷받침 되어야 한다.

2.8 리팩터링과 성능#

마틴 파울러는 리팩터링의 결과 소프트웨어가 느려질 수 있다고 인정한다. 하지만 그렇다 하더라도 성능을 튜닝하기 쉽게 리팩터링하고, 그 다음 필요한 만큼 성능을 튜닝하면 된다고 말한다.

대부분의 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비한다. 코드 전체를 고르게 최적화 한다고 가정할 경우 그 중 90%는 효과가 없는 수정이라는 뜻이고, 이는 곧 시간 낭비다. 따라서 성능을 최적화해야 하는 시점이 왔을 때, 프로파일러로 동작을 분석하고 코드를 고쳐야 한다. 그 전까지는 코드를 다루기 쉽게 유지하는 것에만 집중한다.