Refactoring 2장
리팩터링 원칙
2장 리팩터링 원칙
앞 장의 예시로 리팩터링이 무엇인지 감 잡았을 것이다.
이를 토대로 이번 장에서 리팩터링 전반에 적용되는 원칙 몇 가지를 이야기하는 챕터.
목차
-
2-1. 리팩터링 정의
-
2-2. 두 개의 모자
-
2-3. 리팩터링하는 이유
-
2-4. 언제 리팩터링해야 할까?
-
2-5. 리팩터링 시 고려할 문제
-
2-6. 리팩터링, 아키텍처, 애그니(YAGNI)
-
2-7. 리팩터링과 소프트웨어 개발 프로세스
-
2-8. 리팩터링과 성능
-
2-9. 리팩터링의 유래
-
2-10. 리팩터링 자동화
-
2-11. 더 알고 싶다면
2-1. 리팩터링 정의
수 많은 다른 소프트웨어 개발 용어와 마찬가지로 '리팩터링'도 개발자 사이에서 두리뭉실한 의미로 통용된다.
저자는 리팩터링이라는 용어를 더 구체적으로 사용하며 엄격하게 정의해야 더 유용하다 생각한다.
명사로 사용할때의 리팩터링과 동사로 사용할때의 리팩터링 의미를 보자.
리팩터링[명사]: 소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법
앞선 1장에서 본 함수 추출하기와 조건부 로직을 다형성으로 바꾸기처럼 이름이 붙어있는 리팩터링 기법들이 명사 정의에 해당된다.
리팩터링[동사]: 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러 가지 리팩터링 기법을 적용해서 소프트웨어를 재구성한다.
위에서 제시한 정의를 따르면 특정한 방식에 따라 코드를 정리하는 것만이 저자가 정의하는 리팩터링이다.
또한 리팩터링하는 동안에는 코드가 항상 정상 작동하기 때문에 전체 작업이 끝나지 않았더라도 언제든 멈출 수 있다.
누군가 "리팩터링하다가 코드가 깨져서 며칠이나 고생했다"라고 한다면, 십중팔구 리팩터링한 것이 아니다.
앞에서 리팩터링 전과 후의 코드가 똑같이 동작해야 한다는 뜻에서 일부러 겉보기 동작이라는 표현을 사용했다.
하지만 완전히 똑같지는 않고 함수 추출하기를 거치면 콜스택이 달라져 성능이 변할 수 있고 함수 선언 바꾸기나 함수 옮기기같은 리팩터링을 하면 모듈의 인터페이스가 바뀔 때가 많다.
한편, 리팩터링 과정에서 발견된 버그는 리팩터링 후에도 그대로 버그로 남아 있어야 한다.
(아무도 발견하지 못한 버그는 수정해도 괜찮다)
리팩터링은 성능 최적화와 비슷하지만 목적이 다를 뿐, 리퍽태링의 목적은 코드를 이해하고 수정하기 쉽게 만드는 것이므로 성능은 좋아질 수도, 나빠질 수도 있다. 반면에 성능 최적화는 오로지 속도 개선에만 신경 쓴다.
그러므로 목표한 성능에 반드시 도달해야 한다면 코드는 수정하기 더 어렵게 바뀔 수도 있음을 인지해야 한다.
2-2. 두개의 모자
소프트웨어를 개발할 때 목적이 '기능 추가'인지 아니면 '리팩터링'이냐를 명확히 구분해 작업한다.
켄트 벡은 이 모습을 두 개의 모자(two hats)에 비유했다.
기능을 추가할 때는 '기능 추가' 모자를 쓴 다음 기존 코드는 절대 건드리지 않고 새로운 기능을 추가하기만 한다.
'기능 추가'의 진척도는 테스트를 추가해서 통과하는지 확인하는 방식으로 측정한다.
반면에 리펙터링 할때는 '리팩터링'모자를 쓴다음 기능 추가는 절대 하지 않기로 다짐한 뒤에 오로지 코드 재구성에만 전념한다.
전체 작업 시간이 짧다 해도, 항상 내가 쓰고 있는 모자가 무엇인지와 그에 따른 미묘한 작업 방식의 차이를 분명하게 인식해야 한다.
2-3. 리팩터링하는 이유
리팩터링이 소프트웨어의 모든 문제점을 해결하는 만병통치약은 아니지만 코드를 건강한 상태로 유지하는 데 도와주는 약임은 분명하다.
리팩터링은 다방면으로 활용할 수 있고, 반드시 그래야 하는 도구다.
2-3-1. 리팩터링하면 소프트웨어 설계가 좋아진다
리팩터링을 하지않을수록 소프트웨어의 내부 설계(아키텍처)가 썩기 쉽다.
또한 코드만으로 설계를 파악하기 어려워질수록 설계를 유지하기 어렵고, 설계가 부패되는 속도는 더욱 빨라진다.
같은 일을 하는 코드가 여러 곳에 나타날 수 있기 때문이다.
중복 코드 제거는 설계 개선 작업의 중요한 한 축을 차지하며 코드량을 줄인다고 시스템이 빨라지는 것이 아니다.
프로그램의 용량이 속도에 영향을 주는 경우는 별로 없지만 코드량이 줄면 수정하는 데 드는 노력은 크게 달라진다.
코드가 길수록 실수 없이 수정하기 어려워 진다.
이해해야 할 코드량도 늘어난다.
같은 동작 혹은 비슷한 동작을 하는 코드가 산재해 있다면 한 부붑ㄴ만 살짝 바꿔서는 시스템이 예상대로 작동하지 않을 수 있다.
반면 중복 코드를 제거하면 모든 코드가 언제나 고유한 일을 수행함을 보장할 수 있으며 이는 바람직한 설계의 핵심이다.
2-3-2. 리팩터링하면 소프트웨어를 이해하기 쉬워진다
프로그래밍은 마치 컴퓨터와 대화하는 것과 같다.
컴퓨터에게 시킬 일을 표현하는 코드를 작성하면, 컴퓨터는 정확히 시킨 대로 반응한다.
그래서 컴퓨터에게 시키려는 일과 이를 표현한 코드의 차이를 최대한 줄여야 한다.
프로그래밍은 내가 원하는 바를 정확히 표현하는 일이다.
문제는 프로그램을 동작시키는 데만 신경 쓰다 보면 나중에 그 코드를 다룰 개발자를 배려하지 못한다는 데 있다.
잘 작동하지만 이상적인 구조는 아닌 코드가 있다면, 시간을 내서 리팩터링을 하자.
하게되면 코드의 목적이 더 잘 드러나게, 내 의도를 더 명확하게 전달하도록 개선할 수 있다.
다른 사람을 배려하기 위해서가 아닌 사실 다른 사람이 나 자신일 때가 많다.
그래서 더더욱 리팩터링이 중요하고 게으른 프로그래머 일수록 이전에 작성한 코드를 전혀 머리에 담아두지 않는다.
다시 말해 코드를 보면 알 수 있는 것들은 의도적으로 기록하지 않는다.
내 기억 용량을 초과할까봐 두렵기 때문에 기억할 필요가 있는 것들은 최대한 코드에 담으려고 한다.
2-3-3. 리팩터링하면 버그를 쉽게 찾을 수 있다
코드를 이해하기 쉽다는 말은 버그를 찾기 쉽다는 말이기도 하다.
코드를 리팩터링하게 되면 코드가 하는 일을 파악하게 되면서 새로 깨달은 것을 곧바로 코드에 반영하게 된다.
켄트 백의 말로는 리팩터링하는 뛰어난 습관을 지니면 괜찮은 프로그래머, 훌륭한 프로그래머가 될 수 있다고 한다.
2-3-4. 리팩터링하면 프로그래밍 속도를 높일 수 있다.
지금까지 저자가 제시한 리팩터링 장점을 한 마디로 정리하면 다음과 같다.
리팩터링은 개발 속도에 커다란 이점을 가져다 준다.
내부 설계와 가독성이 개선되고 버그가 줄어든다는 장점은 모두 서비스 품질 향상에 직결된다.
하나의 시스템을 오래 개발한 개발자들과 얘기하다 보면 초기에는 진척이 빨랐지만 현재는 새 기능을 하나 추가하는데 훨씬 오래 걸린다는 말을 한다.
게다가 기능을 추가하고 나면 버그가 발생하는 일이 잦고, 해결하는 시간은 더 걸린다.
그러고나면 프로그램의 동작을 파악하기가 거의 고대 유적 발굴만큼 어려워진다.
이 과정을 그래프로 표현하면 대략 다음과 같다.
모듈화가 잘 되어 있으면 전체 코드베이스 중 작은 일부분만 이해하면 된다.
코드가 명확하면 버그를 만들 가능성도 줄고, 버그를 만들더라도 디버깅하기가 훨씬 쉽다.
내부 품질이 뛰어난 코드는 새로운 기능 구축을 돕는 견고한 토대가 된다.
이를 저자는 **지구력 가설(Design Stamina Hypothesis)**이라고 부른다.
정말 그런지는 증멸할 수 없어서 '가설'이라고 표현한다.
하지만 뛰어난 프로그래머들의 경험이 이를 뒷받침한다.
처음부터 뛰어나고 완벽한 좋은 설계를 마련하기란 매우 어렵다.
그래서 빠른 개발이라는 숭고한 목표를 달성하려면 리팩터링이 반드시 필요하다.
2-4. 언제 리팩터링해야 할까?
저자는 프로그래밍할 때 거의 한 시간 간격으로 리팩터링한다.
하다보니 작업의 흐름에 리팩터링을 녹이는 방법이 여러 가지임을 알게 됬다.
3의 법칙
돈 로버츠(Don Roberts)가 제시한 가이드다.
처음에는 그냥 한다.
비슷한 일을 두 번째로 하게 되면(중복이 생겼다는 사실에 당황스럽겠지만) 일단 계속 진행한다.
비슷한 일을 세 번째 하게되면 리팩터링 한다.
야구를 좋아하는 사람은 '스트라이크 세 번이면 리팩터링하라(삼진 리팩터링)'로 기억하자.
2-4-1. 준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기
리팩터링하기 가장 좋은 시점은 코드에 기능을 새로 추가하기 직전이다.
코드를 살펴보면서, 구조를 살짝 바꾸면 다른 작업을 하기가 훨씬 쉬워질 만한 부분을 찾는다.
함수를 복제해서 해당 값만 수정해도 되지만 그러면 중복 코드가 생긴다.
이 부분을 변경할일이 생기면 원래 코드와 복제한 코드를 모두 수정해야 하며, 심한 경우 복제한 코드가 어디 있는지까지 일일이 찾아내야 한다.
이럴 때는 리팩터링 모자를 쓰고 함수 매개변수화하기를 적용한다.
그러고 나면 함수에 필요한 매개변수를 지정해서 호출하기만 하면 된다.
버그를 잡을 때도 마찬가지다.
준비를 위한 리팩터링으로 상황을 개선해 놓으면 버그가 수정된 상태가 오래 지속될 가능성을 높이는 동시에 같은 곳에서 다른 버그가 발생할 가능성을 줄일 수도 있다.
2-4-2. 이해를 위한 리팩터링: 코드를 이해하기 쉽게 만들기
코드를 파악할 때마다 코드의 의도가 더 명확하게 드러나도록 리팩터링할 여지는 없는지 찾아본다.
조건부 로직의 구조가 이상하지 않은지 살펴보기도 하고, 함수 이름을 잘못 정해서 실제로 하는 일을 파악하는 데 시간이 오래 걸리지는 않는지도 살펴본다.
내가 이해한 것을 코드에 반영해두면 오래 보존할 수 있을 뿐만 아니라 동료들도 알 수 있다.
이런 방식은 자잘한 세부 코드에 이해를 위한 리팩터링이다.
랄프 존슨은 이런 초기 단계의 리팩터링을 창문 밖을 잘 내다보기 위한 창문 닦기에 비유한다.
코드를 분석할 때 리팩터링을 해보면, 도달하지 못했을 더 깊은 수준까지 이해하게 된다.
이해를 위한 리팩터링을 의미 없이 코드를 만지작거리는 것이라고 무시하는 이들은 복잡한 코드 아래 숨어있는 다양한 기회를 결코 발견할 수 없다.
2-4-3. 쓰레기 줍기 리팩터링
코드를 파악하던 중에 일을 비효율적으로 처리하는 모습을 발견할 때가 있다.
로직이 쓸데없이 복잡하거나, 매개변수화한 함수 하나면 될 일을 거의 똑같은 함수 여러 개로 작성해놨을 수 있다.
이때 절충을 해서 원래 하려던 작업과 관련 없는 일에 너무 많은 시간을 빼앗기긴 싫을 것이다.
그렇다고 쓰레기가 나뒹굴게 방치해서 나중에 일을 방해하도록 내버려두는 것도 좋지 않다.
저자의 해결책은 간단히 수정할 수 있는 것은 즉시 고치고, 시간이 좀 걸리는 일은 짧은 메모만 남긴 다음, 하던 일을 끝내고 나서 처리한다.
이것이 이해를 위한 리팩터링의 변형인 쓰레기 줍기 리팩터링이다.
리팩터링의 멋진 점은 각각의 작은 단계가 코드를 깨뜨리지 않는다는 사실이다.
그래서 작업을 잘게 나누면 몇 달에 걸쳐 진행하더라도 그 사이 한 순간도 코드가 깨지지 않기도 한다.
2-4-4. 계획된 리팩터링과 수시로 하는 리팩터링
앞에서 저자가 소개한 리팩터링들은 준비를 위한 리팩터링, 이해를 위한 리팩터링, 쓰레기 줍기 리팩터링은 모두 기회가 될 때만 진행한다.
개발에 들어가기 전에 리팩터링 일정을 따로 잡아두지 않고, 기능을 추가하거나 버그를 잡는 동안 리팩터링도 함께 한다.
저자는 리팩터링 시간을 일정에 따로 잡아두지 않고 대부분의 리팩터링을 다른 일을 하는 중에 처리한다.
보기 싫은 코드를 발견하면 리팩터링하자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야 한다.
코드가 깔끔해질 수록 리팩터링하기가 더 쉽다.
무언가 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고(단, 쉽지 않다) 그런 다음 쉽게 수정하자 -켄트 벡-
리팩터링 작업 대부분은 드러나지 않게, 기회가 될 떄마다 해야 한다.
버전 관리 시스템에서 리팩터링 커밋과 기능 추가 커밋을 분리해야 한다는 조언을 들은 적이 있다.
이렇게 할 때의 큰 장점은 두 가지 활동을 구분해서 별개로 검토하고 승인할 수 있다는 것이다.
하지만 나는 이 견해에 완전히 동의하지는 않는다.
리팩터링은 기능 추가와 밀접하게 엮인 경우가 너무나 많기 때문에 굳이 나누는 것은 시간 낭비일 수 있다.
리팩터링 커밋을 분리한다고 해서 무조건 좋은 것은 아님을 명심하고, 우리팀에 적잡한 방식을 실험을 통해 찾아내야 한다.
2-4-5. 오래걸리는 리팩터링
팀 전체가 달려들어도 몇 주는 걸리는 대규모 리팩터링이 존재한다.
패키지를 새 것으로 교체하는 작업일 수 있고, 일부 코드를 다른 팀과 공유하기 위해 컴포넌트로 빼내는 작업일 수도 있다.
저자는 이런 상황에 처한 팀 전체가 리팩터링에 매달리는 데 회의적이다.
예를 들어 패키지를 교체할 때는 기존 것과 새로운 것 모두를 포용하는 추상 인터페이스부터 마련한다.
기존 코드가 이 추상 인터페이스를 호출하도록 만들고 나면 라이브러리를 훨씬 쉽게 교체할 수 있다.
이 전략을 추상화로 갈아타기라 한다.
2-4-6. 코드 리뷰에 리팩터링 활용하기
코드 리뷰는 개발팀 전체에 지식을 전파하는 데 좋다.
리팩터링은 다른 이의 코드를 리뷰하는데도 도움 된다.
코드 리뷰에 리팩터링을 접목하는 구체적인 방법은 리뷰의 성격에 따라 다르다.
흔히 쓰는 풀요청 모델(pull request model)에서는 그리 효과적이지 않다.
또한 코드 리뷰에서 코드 작성자가 참석해야 맥락을 설명해줄 수 있고 작성자도 리뷰어의 변경 의도를 제대로 이해할 수 있으므로, 이왕이면 참석자가 참석하는 방식이 좋다.
저자가 경험한 가장 좋은 방법은 작성자와 나란히 앉아서 코드를 훑어가며 리팩터링하는 것이다.
이렇게 하면 자연스럽게 짝 프로그래밍이 된다.
2-4-7. 관리자에게는 뭐라고 말해야 할까?
저자가 가장 많이 받은 질문 중 하나는 "관리자에게 리팩터링에 대해 어떻게 말해야 하나요?"다.
관리자와 고객의 입장에서는 리팩터링이 금기된 조직도 있다.
설상가장 리팩터링이 아닌 어설픈 재구성 작업을 하며 코드 베이스가 망가진다면 개발팀에 대한 불신은 증폭된다.
관리자가 기술에 정통하고 설계 지구력 가설도 잘 이해하고 있다면 오히려 권장할 뿐만 아니라 팀이 리팩터링을 충분히 하고 있는지 살펴보기도 한다.
하지만 상당수의 관리자와 고객은 코드 베이스가 생산성에 미치는 영향을 모른다.
이런 상황에 있는 이들에게는 리팩터링한다고 말하지 말라고 조언한다.
하극상은 아니다, 하지만 소프트웨어 개발자는 프로다.
프로 개발자의 역할은 효과적인 소프트웨어를 최대한 빨리 만드는 것이다.
경험상 리팩터링하면 소프트웨어를 빠르게 만드는 데 아주 효과적이다.
구체적인 방법은 개발자가 판단해야 한다.
프로 개발자에게 주어진 임무는 새로운 기능을 빠르게 구현하는 것이고, 가장 빠른 방법은 리팩터링이다.
그래서 리팩터링부터 한다.
2-4-8. 리팩터링하지 말아야 할 때
지금까지의 이야기가 무조건 리팩터링을 권장한다고 들릴 수 있는데, 리팩터링하면 안되는 상황도 있다.
저자는 지저분한 외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 둔다.
내부 동작을 이해해야 할 시점에 리팩터링해야 효과를 제대로 볼 수 있다.
또한, 리팩터링하는 것보다 처음부터 새로 작성하는게 쉬울 떄도 리팩터링하지 않는다.
이러한 결정은 내리기 쉽지 않다.
리팩터링할지 새로 작성할지를 잘 결정하려면 뛰어난 판단력과 경험이 뒷받침되어야 한다.
그래서 이 판단에 대해서는 한마디 조언으로 표현하긴 어렵다.
2-5. 리팩터링 시 고려할 문제
저자는 누군가 특정한 기술, 도구, 아키텍처 등을 내세울 때마다 항상 문제점을 찾는다.
무언가를 어디에 적용할지 판단하려면 손익을 제대로 이해해야 한다.
문제가 언제 발생하고 어떻게 대처해야 할지를 반드시 알고 있어야 한다.
2-5-1. 새 기능 개발 속도 저하
많은 사람이 리팩터링 떄문에 새로운 기능을 개발하는 속도가 느려진다고 여기지만, 리팩터링의 궁극적인 목적은 개발 속도를 높이는데 있다.
하지만 리팩터링으로 인해 진행이 느려진다고 생각하는 사람이 여전히 많다.
아마도 이 점이 실전에서 리팩터링을 제대로 적용하는 데 가장 큰 걸림돌인 것 같다.
리팩터링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것이다.
떄로는 리팩터링이 필요해 보이지만, 추가하려는 새 기능이 아주 작아서 기능 추가부터 하고 싶은 상황에 마주칠 수 있다.
이럴 때에는 프로 개발자로서 가진 경험을 잘 발휘해서 결정한다.
저자는 균형점을 잡는 방법을 설명하기는 쉽지 않고, 정량화하기는 더더욱 어렵다고 한다.
따라서 경험에 근거해 손익을 계산하고 현명하게 대처해야 한다고 말하는 것 같다.
건강한 코드의 위력을 충분히 경험해보지 않고서는 코드 베이스가 건강할 때와 허약할 때의 생산성 차이를 체감하기 어렵다.
코드 베이스가 건강하면 기존 코드를 새로운 방식으로 조합하기 쉬워서 복잡한 새로운 기능을 더 빨리 추가할 수 있다.
개발 속도 저하를 이유로 리팩터링을 금기하는 비생산적인 문화를 관리자 탓으로 돌리는 사람이 많지만, 오히려 개발자가 스스로 그렇게 생각하는 경우도 많이 봤다.
심지어 관리자가 리팩터링에 호의적임에도 리팩터링하면 안되는 줄 아는 사람도 있다.
개발팀을 이끌고 있다면 코드 베이스가 더 건강해지는 것을 추구한다는 사실을 팀원들에게 명확히 밝혀야 한다.
앞에서 말한 리팩터링을 할지 말지 판단하는 능력은 수년에 걸친 경험을 통해 서서히 형성된다.
리더는 리팩터링 경험이 부족한 이들이 이런 능력을 빠르게 갖추도록 개발 과정에서 많이 이끌어줘야 한다.
하지만 저자는 사람들이 빠지기 쉬운 가장 위험한 오류는 리팩터링을 **클린 코드(Clean Code)**나 바람직한 엔지니어링 습관처럼 도덕적인 이유로 정당화 하는 것이라고 말한다.
리팩터링은 오히려 경제적인 이유로 하는 것이다.
리팩터링은 개발 기간을 단축하고자 하는 것이며, 기능 추가 시간을 줄이고, 버그 수정 시간을 줄여준다.
스스로 그렇게 인식하는 데 그치지 말고 다른 사람과 대화할 때도 이점을 명심해야 한다.
리팩터링의 동기는 경제적인 효과에 있으며 이를 명확히 이해하는 개발자, 관리자, 고객이 많아질수록 위에서 본 소프트웨어 개발 진행 그래프 사진에서 좋은 설계 곡선을 더 많이 볼 수 있다.
2-5-2. 코드 소유권
리팩터링하다 보면 모듈의 내부뿐만 아니라 시스템의 다른 부분과 연동하는 방식에도 영향을 주는 경우가 많다.
함수 이름을 바꾸고 싶고 그 함수를 호출하는 곳을 모두 찾을 수 있다면, 간단히 함수 선언 바꾸기로 선언 자체와 호출하는 곳 모두를 한 번에 바꿀 수 있다.
하지만 코드 수정에 대한 소유권이 나뉘어 있으면 리팩터링에 방해가 된다.
2-5-3. 브랜치
현재 흔히 볼 수 있는 팀 단위 작업 방식은 버전 관리 시스템을 사용하여 팀원마다 코드베이스의 브랜치를 하나씩 맡아서 작업하다가, 결과물이 어느 정도 쌓이면 마스터 브랜치에 통합해서 다른 팀원과 공유하는 것이다.
이렇게 하면 어떤 기능 전체를 하나의 브랜치에만 구현해놓고, 프로덕션 버전으로 릴리즈할 때가 돼서야 마스터에 통합하는 경우가 많다.
이 방식의 장점은 작업이 끝나지 않은 코드가 마스터에 섞이지 않고 기능이 추가될 때마다 버전을 명확히 나눌 수 있고, 기능에 문제가 생기면 이전 상태로 쉽게 되돌릴 수 있어서 좋다고 한다.
하지만 이런 브랜치 방식에는 단점이 있다.
독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터로 통합하기가 어려워 진다.
이 과정을 줄이고자 마스터를 개인 브랜치로 수시로 리베이스하거나 머지한다.
저자는 머지와 통합을 명확히 구분하며, 마스터를 브랜치로 '머지'하는 작업은 단방향이고, 브랜치만 바뀌고 마스터는 그대로다.
반면에, '통합'은 마스터를 개인 브랜치로 가져와서 작업한 결과를 다시 마스터에 올리는 양방향 처리를 뜻한다.
그래서 통합은 마스터와 브랜치가 모두 변경된다.
누군가 개인 브랜치에서 작업한 내용을 마스터에 통합하기 전까지 다른 사람이 그 내용을 볼 수 없다.
통합한 뒤에는 마스터에서 달라진 내용을 내 브랜치에 머지해야 하는데, 상당한 노력이 들 수 있다.
머지가 복잡해지는 문제는 기능별 브랜치들이 독립적으로 개발되는 기간이 길어질수록 기하급수적으로 늘어난다.
그렇기 때문에 기능별 브랜치의 통합 주기를 2~3일 단위로 짧게 관리해야 한다고 주장하는 사람도 많다.
저자는 더 짧아야 한다고 생각하며 이 방식을 지속적인 통합(CI), 또는 트렁크 기반 개발(TBD)이라 한다.
CI에 따르면 모든 팀원이 하루에 최소 한 번은 마스터와 통합한다.
이렇게 하면 다른 브랜치들과의 차이가 크게 벌어지는 브랜치가 없어져서 머지의 복잡도를 상당히 낮출 수 있다.
하지만 CI를 적용하기 위해서 치러야할 대가가 있다.
마스터를 건강하게 유지하고, 거대한 기능을 잘게 쪼개는 법을 배우고, 각 기능을 끌 수 있는 기능 토글을 적용하여 완료되지 않은 기능이 시스템 전체를 망치지 않도록 해야 한다.
머지의 복잡도를 줄일 수 있어서 CI를 선호하지만, 가장 큰 이유는 리팩터링과 궁합이 아주 좋기 때문이다.
리팩터링을 하다보면 코드베이스 전반에 걸쳐 자잘하게 수정하는 부분이 많을 때가 있다.
특히 기능별 브랜치 방식에서는 리팩터링을 도저히 진행할 수 없을 정도로 심각한 머지 문제가 발생하기 쉽다.
켄트 벡이 CI와 리팩터링을 합쳐서 익스트림 프로그래밍: XP를 만든 이유도 두 기법이 궁합이 잘 맞기 때문이다.
기능별 브랜치를 사용하는 적절한 때는 잘모르는, 믿지 못하는 프로그매러로부터 이따금 커밋이 들어오는 오픈 소스 프로젝트라면 기능별 브랜치 방식이 적합할 수 있다.
참고로, CI를 적용하는 편이 소프트웨어를 배포하는 데 훨씬 효과적이라는 객관적인 증거가 있으니 고려하기 바란다.
저자가 추천하는 소프트웨어에 CI를 적용하는 기법이 담긴 책이다.
2-5-4. 테스팅
리팩터링의 두드러진 특성은 프로그램의 겉보기 동작은 똑같이 유지된다는 것이다.
핵심은 오류를 재빨리 잡는 데 있다.
이렇게 하려면 코드의 다양한 측면을 검사하는 테스트 스위트가 필요하다.
그리고 이를 빠르게 실행할 수 있어야 수시로 테스트하는 데 부담이 없다.
달리 말하면 리팩터링하기 위해서는 자가 테스트 코드를 마련해야 한다는 뜻이다.
테스트 코드를 갖추기란 실현 불가능할 정도로 무리한 요구라고 생각하는 독자도 있을 것이다.
테스트에 어느 정도 노력을 기울여야 하는 것은 사실이지만 효과는 상당하다.
자가 테스트 코드는 리팩터링을 할 수 있게 해줄 뿐만 아니라, 새로운 기능을 추가할 때도 훨씬 안전하게 진행할 수 있도록 도와준다.
또한, 테스트가 실패한다면 가장 최근에 커밋한 버전에서 무엇이 달라졌는지 살펴볼 수 있다는 데 있다.
테스트 주기가 짧다면 단 몇 줄만 비교하면 되며, 문제를 일으킨 부분이 그 몇 줄 안에 있기 때문에 버그를 훨씬 쉽게 찾을 수 있다.
자가 테스트 코드는 통합 과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와도 밀접하게 연관된다.
CI에 통합된 테스트는 XP의 권장사항이자 지속적인 배포 CD의 핵심이기도 하다.
2-5-5. 레거시 코드
물려받은 레거시 코드는 대체로 복잡하고 테스트도 제대로 갖춰지지 않은 것이 많다.
무엇보다 다른 사람이 작성한 것이므로 생각만 해도 끔찍하다.
대규모 레서기 시스템을 테스트 코드 없이 명료하게 리팩터링하기는 어렵다.
정답은 당연히 테스트 보강이다. 단순 노동에 가까울 수 있다는 점을 제외하면 간단히 할 수 있어 보이지만, 막상 해보면 생각보다 까다롭다.
저자는 "레거시 코드 활용 전략"(에이콘, 2018)에 나온 지침을 충실히 따르라고 추천한다.
주요 내용을 표현하면 '프로그램에서 테스트를 추가할 틈새를 찾아서 시스템을 테스트해야 한다는'것이다.
이러한 틈새를 만들 때 리팩터링이 활용된다.
테스트 없이 진행하기 때문에 상당히 위험하지만 문제를 해결하기 위해서라면 감내해야 할 위험이다.
이럴 때 자동 리팩터링 도구가 있다면 큰 도움이 된다.
난감한 상황에서 빠져나오기 위한 더 쉬운 방법은 아쉽게도 없다.
처음부터 자가 테스트 코드를 작성해야 한다고 강조한 이유가 바로 이 때문이다.
테스트를 갖추고 있어도 레거시의 코드를 아름다운 코드로 단번에 리팩터링 할 수 없다.
저자가 선호하는 방식은 서로 관된 부분끼리 나눠서 하나씩 공략하는 것이다.
2-5-6. 데이터베이스
저자가 초판에서는 데이터베이스는 리팩터링하기 어려운 영역이라도 했지만, 현재는 틀린말이 되버렸다.
프라모드 사달게(Pramod Sadalage)가 개발한 진화형 데이터베이스 설계와 데이터베이스 리팩터링 기법은 널리 적용되고 있다.
데이터베이스 리팩터링은 프로덕션 환경에 여러 단계로 나눠서 릴리스하는 것이 대체로 좋다는 점에서 다른 리팩터링과 다르다.
이렇게 하면 프로덕션 환경에서 문제가 생겼을 때 변경을 되돌리기 쉽다.
필드 이름을 바꿀 때 첫 번째 커멧에서는 새로운 데이터베이스 필드를 추가만하고 사용하지는 않는다.
그다음에 데이터베이스를 읽는 클라이언트들은 새 필드를 사용하는 버전으로 조금씩 교체한다.
이 과정에서 발생하는 버그도 해결하면서 클라이언트들을 새 필드를 사용하는 버전으로 조금씩 교체한다.
더는 필요가 없어진 예전 필드를 삭제한다. 이렇게 데이터베이스를 변경하는 방식은 병렬수정(parallel change) 또는 팽창-수축(expand-contract)의 일반적인 예이다.
2-6. 리팩터링, 아키텍처, 애그니(YAGNI)
리팩터링은 소프트웨어 아키텍처를 바라보는 관점을 완전히 바꿔놓았다.
리팩터링에 숙련되면 수년 동안 운영되던 소프트웨어라도 아키텍처를 대폭 변경할 수 있었다.
이 책의 부제처럼 리팩터링은 기존 코드의 설계를 개선할 수 있다.
하지만 레거시 코드는 변경하기 어려울 떄가 많다. 특히 탄탄한 테스트 코드가 뒷받침해주지 못하면 더더욱 어렵다.
리팩터링이 아키텍처에 미치는 실직적인 효과는 요구사항 변화에 자연스럽게 대응하도록 코드 베이스를 잘 설계해준다는 데 있다.
한 가지 방법은 향후 변경에 유연하게 대처할 수 있는 유연성 메커니즘을 소프트웨어에 심어두는 것이다.
가령 함수를 정의하다 보면 범용적으로 사용할 수 있겠다는 생각이 들 때가 있다. 그래서 다양한 예상 시나리오에 대응하기 위한 매개변수들을 추가한다.
이런 매개변수가 유연성 메커니즘이다. 물론 메커니즘들이 대개 그렇듯 치러야 할 비용이 있다.
리팩터링을 활용하면 다르게 접근할 수 있다. 앞으로 어느 부분에 유연성이 필요하고 어떻게 해야 그 변화에 가장 잘 대응할 수 있을지 추측하지 않고, 현재까지 파악한 요구사항만을 해결하는 소프트웨어를 구축한다.
진행하면서 사용자의 요구사항을 더 잘 이애하게 되면 아키텍처도 그에 맞게 리팩터링해서 바꾼다. 과정에서 소프트웨어의 복잡도에 지장을 주지 않는 메커니즘은 마음껏 추가하지만, 복잡도를 높일 수 있는 유연성 메커니즘은 반드시 검증을 거친 후에 추가한다.
호출하는 측에서 항상 같은 값을 넘기는 매개변수는 매개변수 목록에 넣지 않고, 매개변수를 추가해야 할 시점이 오면 간단한 리팩터링 기법인 함수 매개변수화하기로 해결한다.
리팩터링을 미루면 훨씬 힘들어진다는 확인 들 때만 유연성 메커니즘을 미리 추가한다.
이러한 설계 방식을 간결한 설계, 점직적 설계, YAGNI(you aren't going to need it: 필요 없을 거다)등으로 부른다.
YAGNI를 문자 그대로 해석해서 적용할 때도 있지만, 아키텍처를 전혀 고려하지 말라는 뜻은 아니다.
저자가 바라보는 TAGNI는 아키텍처와 설계를 개발 프로세스에 녹이는 또 다른 방식이며, 리팩터링의 뒷받침 없이는 효과를 볼 수 없다.
YAGNI를 받아들인다고 해서 선제적인 아키텍처에 완전히 소홀해도 된다는 뜻은 아니다.
리팩터링으로는 변경하기 어려워서 미리 생각해두면 시간이 절약되는 경우도 얼마든지 있다.
이러한 경향은 진화형 아키텍처(evolutionary architecture) 원칙이 발전하는 계기가 됐다. (진화형 아키텍처는 아키텍처 관련 결정을 시간을 두고 반복해 내릴 수 있다는 장점을 활용하는 패턴과 실천법을 추구한다).
2-7. 리팩터링과 소프트웨어 개발 프로세스
리팩터링 시 고려할 문제를 이야기한 실천법에 따라 리팩터링의 효과가 크게 달라진다는 것을 발견했을 것이다.
실제로 리팩터링이 퍼지기 시작한 것도 익스트림 프로그래밍에 도입됐기 때문이다.
XP의 두드러진 특징은 지속적 통합, 자가 테스트 코드, 리팩터링 등의 개성이 강하면서 상호 의존하는 기법들을 하나로 묶은 프로세스라는 점이다.
참고로 자가 테스트 코드와 리팩터링을 묶어서 **테스트 주도 개발(TDD)**이라 한다.
리팩터링의 첫 번째 토대는 자가테스트 코드다. 다시 말해 프로그래밍 도중 발생한 오류를 확실히 걸러내는 테스트를 자동으로 수행할 수 있어야 한다.
테스트는 리팩터링에 중요한 토대이기 때문에 저자는 뒤에 나올 5장 전체를 테스트라는 주제에 할애했다.
지속적인 통합을 적극 권장하는 이유도 각 팀원이 다른 사람의 작업을 방해하지 않으며 언제든지 리팩터링할 수 있어야 하기 때문이다.
따라서 자가 테스트 코드, 지속적 통합, 리팩터링이라는 세 기법은 서로 강력한 상승효과를 발휘한다.
이상적인 세 가지 실천법을 적용한다면 설명한 YAGNI 설계 방식으로 개발을 진행할 수 있다.
리팩터링과 YAGNI는 서로 긍정적인 영향을 준다.
지속적 배포는 소프트웨어를 언제든 릴리스할 수 있는 상태로 유지해준다.
이처럼 견고한 기술적 토대를 갖추면 좋은 아이디어를 프로덕션 코드로 반영하는 시간을 엄청나게 단축할 수 있어서 고객에게 더 나은 서비스를 제공할 수 있다.
또한, 고치는 데 시간을 잡아먹는 버그의 수를 줄여줘서 소프트웨어의 신뢰성도 높일 수 있다.
지금까지의 설명이 실무에 적용하기는 만만치 않지만, 저자가 소개하는 접근법으로 복잡도를 다루는 데 효과적이라고 검증된 것이다.
물론 어떠한 접근법이든지 충분한 연습과 실력이 뒷받침되어야 한다.
2-8. 리팩터링과 성능
리팩터링하면 프로그램 성능이 느려질까봐 걱정하는 사람이 많다.
저자는 실제로 소프트웨어를 이해하기 쉽게 만들기 위해 속도가 느려지는 방향으로 수정하는 경우가 많다.
'직관적인 설계 vs 성능'은 중요한 주제이며 저자가 성능을 무시하는 이유는 프로그램의 속도가 느려지더라도 성능을 더 좋은 속도로 튜닝하기 좋아지기 때문이다.
따라서 저자는 먼저 튜닝하기 쉽게 만들고 나서(성능을 무시하더라도) 원하는 속도가 나게끔 또 튜닝을 하는 것이다.
저자는 빠른 소프트웨어를 작성하는 방법 세 가지를 경험했다.
가장 엄격한 방법은 시간 예산 분배방식으로, 하드 리얼타임 시스템에서 많이 사용한다.
이 방식은 설계를 여러 컴포넌트로 나눠서 컴포넌트마다 자원(시간과 공간) 예산을 할당한다.
두 번째 방법은 끊임없이 관심을 기울이는 것이다.
프로그래머는 누구나 높은 성능을 유지하기 위해 무슨 일이든 한다.
정작 컴파일러와 런타임과 하드웨어의 동작을 제대로 이해하지 못한 채 작성할 때도 많다.
성능 개선을 위한 세 번째 방법은 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경 쓰지 않고 코드를 다루기 쉽게 만드는데 집중한다.
그러다 성능 최적화 단계가 되면 다음의 구체적인 절차를 따라 프로그램을 튜닝한다.
리팩터링할 때처럼 최적화를 위한 수정도 작은 단계로 나눠서 진행한다.
물론 각 단계마다 컴파일과 테스트를 거치고 프로파일러를 다시 실행해본다.
성능이 개선되지 않았다면 수정 내용을 되돌린다.
리팩터링은 성능 좋은 소프트웨어를 만드는 데 기여한다. 단기적으로 보면 리팩터링 단계에서는 성능이 느려질 수도 있다.
하지만 최적화 단계에서 코드를 튜닝하기 훨씬 쉬워지기 때문에 결국 더 빠른 소프트웨어를 얻게 된다.
2-9. 리팩터링의 유래
'리팩터링'이란 용어의 정확한 유래는 찾을 수 없었다.
실력 있는 프로그래머는 항상 자신의 코드를 정리하는 데 어느 정도의 시간을 할애해왔다.
복잡하고 지저분한 코드보단 깔끔한 코드가 수정하기 쉽고 처음부터 깔끔하게 작성하는 경우는 거의 없다는 것을 경험을 통해 알기 때문이다.
리팩터링의 중요함을 깨달은 워드 커닝햄과 켄트 벡은 1980년대부터 스몰토크를 활용해 개발해왔다.
스몰토크는 기능이 풍부한 소프트웨어를 빠르게 작성할 수 있는 굉장히 역동적인 환경인데, 당시 개발 환경 중에서 리팩터링을 활용해보기에 특히 좋았다.
두 사람의 아이디어가 커뮤니티에 큰 반향을 일으키며 리팩터링이란 개념이 중요한 요소로 자리 잡았다.
당시 커뮤니티를 주도하던 인물중에 랄프 존슨(디자인 패턴, GoF 멤버)이 있었고, 랄프는 효율적이고 유연한 프레임워크를 개발하는데 리펙터링이 어떻게 기여하는지를 연구했다.
랄프의 박사 과정 학생인 빌 옵다이크는 리팩터링의 잠재 가치를 간파하고 스몰토크를 넘어 다른 언어들에도 적용할 수 있으리라 생각했다.
빌은 박사 연구 주제로 리팩터링을 개발 도구에서 지원하는 방안을 파고들었고, 그중에서 C++ 개발에 유용한 리펙터링에 관심이 많았다.
의미 보존 리펙터링 기법들을 연구한 결과, 리펙터링 후에도 의미가 보존되는지를 검증하는 방법과 이를 도구로 구현하는 방법을 발표했다.
빌의 박사 학위논문은 리팩터링에 대한 최초의 연구 업적이다.
이후에 존 브랜드와 돈 로버츠는 빌이 제기한 리팩터링 도구 아이디어를 발전시켜서 스몰토크 환경을 위한 최초의 리팩터링 도구인 <리팩터링 브라우저>를 개발했다.
시간이 지나 여러 경험을 통해 저자는 리팩터링이 정말 중요한 기법임을 깨닫게 됐다.
하지만 당시에는 실무 프로그래머가 읽을 만한 책이 하나도 없었고, 앞에서 언급한 리팩터링 전문가 중에서도 책을 쓰겠다는 사람도 없었다.
그래서 저자가 리팩터링 전문가들의 도움을 받아 이책의 초판을 쓰게 된 것이다.
다행히 업계에서 리팩터링이란 개념을 잘 받아들였고, 모든 프로그래머가 리팩터링이란 용어를 쓰게 되었다.
도구도 다향하게 나왔으며 리팩터링은 주류 개발 기법으로 자리 잡았다.
2-10. 리팩터링 자동화
리팩터링과 관련하여 지난 수 십년 사이에 일어난 가장 큰 변화는 자동 리팩터링을 지원하는 도구가 등장한 것이다.
예를들어 인텔리제이나 이클릭스에서 원하는 항목을 클릭하는 것만으로 처리할 수 있다.
과거에 비해 현재는 에디터나 독립 도구에서도 리팩터링 기능을 제공할 정도로 자동 리팩터링이 흔해졌다.
물론 완성도는 제법 차이가 나지만 원인은 도구 자체에 있기도하고 언어마다 리팩터링을 자동화할 수 있는 범위가 다르기 때문이다.
이 책에서는 도구들의 차이는 분석하지 않지만 그 이면에 깔린 원칙들은 잠시 짚어볼 가치가 있다.
리팩터링을 자동화하는 가장 어설픈 방법은 소스 코드의 텍스트를 직접 조작하는 것이다.
가령 '찾아 바꾸기' 기능으로 이름을 변경하거나, 변수 추출하기를 위해 간단히 코드를 재구성하는 식이다.
이 방식은 허점이 많기 때문에 테스트해보기 전에는 결과를 신뢰해서는 안 된다.
정적 타입 언어라면 훨씬 안전하게 구현할 수 있는 리팩터링 수가 늘어난다.
간단히 함수 이름 바꾸기를 적용하는 경우에 안전하면서도 자동으로 처리할 수 있다.
2-11. 더 알고 싶다면
아직 2장인데 참고 문헌을 나열하기 어색하지만, 이 책에서 다루는 리팩터링의 기본 이상이 궁금할 때 살펴볼 만한 자료를 소개하기에는 지금이 적기인 것 같다.
리팩터링 연습에 주력한 책을 원한다면 윌리엄 웨이크(William C. Wake)가 쓴 '리팩터링 워크북(2006)'을 추천한다.
또 다른 책으로 소프트웨어 패턴 분야에 지대한 영향을 준 '디자인 패턴'책에서 가장 핵심적인 패턴을 골라, 코드를 그 패턴대로 재구성하기 위해 조슈아 케리에프스키(Joshua Kerievsky)의 '패턴을 활용한 리팩터링(2011)'에서 리팩터링하는 방법을 다룬다.
최신 자료를 보고 싶다면 이 책의 깃허브 페이지와 리팩터링 웹사이트를 참고하기 바란다.
리팩토링 깃허브 방문하기
공식 리팩터링 사이트 방문하기
3. 코드에서 나는 악취
냄새 나면 당장 갈아라.
-켄트 벡 할머니의 육아 원칙-