리팩토링 2판 요약 정리 Ch.2
카테고리: Software ArchitectureDecember 10, 2021
리팩토링 2판을 번역/요약한 글입니다.
리팩토링 정의하기
리팩토링 (명사): 소프트웨어의 겉보기 동작(observable behavior)은 그대로 둔 채, 좀 더 이해하기 쉽고 변경에 유연하도록 하기 위해 소프트웨어의 내부 구조에 가해진 변화.
리팩토링 (동사): 소프트웨어의 겉보기 동작은 그대로 둔 채, 리팩토링을 여러 번 적용하여 소프트웨어의 구조를 바꾸는 행위.
- 오랜 시간 동안 현업 개발자들은 코드를 깔끔하게 정리하는 작업을 단순히 “리팩토링”이라고 불러왔지만, 위의 정의들은 코드를 깔끔하게 정리하는 특정 방식에 초점을 맞추고 있다.
- 리팩토링은 겉으로 보이는 동작은 유지한 채 내부 구조를 바꾸는 작은 단계들을 적용하고, 이러한 단계들을 연결하여 큰 변화를 만들어 내는 것에 관한 것이다.
- 각각의 리팩토링은 그 자체로 꽤 작거나, 작은 단계들로 구성되어 있기 때문에 리팩토링이 (완전히) 완료되지 않았음에도 불구하고 언제든 중단할 수 있다.
- 만약 누군가 “리팩토링 때문에 며칠 동안 코드가 망가진(broken) 상태였어요” 라고 한다면 당신은 그 사람이 하던 건 리팩토링이 아니었다고 확신할 수 있을 것이다.
- 작은 단계들은 서로 잘 결합되기 때문에 더 빨리 진행할 수 있게 해주고, 디버깅하는 데 시간을 쏟지 않게 해준다.
- 내가 내린 정의에서 “겉보기 동작(observable behavior)“라는 말을 썼는데, 이 말은 리팩토링을 시작하기 전과 후에 (전반적으로) 코드가 하는 일이 동일해야 한다는 뜻이다. 물론, 코드의 성능 특성, 모듈의 인터페이스 같은 부분이 바뀔 수도 있으나 사용자가 신경 쓰는 부분은 바뀌어서는 안 된다.
- 전반적인 동작은 그대로 둔 채 코드를 변경한다는 측면에서 리팩토링과 성능 개선은 매우 흡사하다. 이들의 차이라면, 리팩토링은 코드를 이해하기 쉽고 변경에 유연하도록 바꾼다는 것이고 성능 개선은 오직 성능에만 초점을 맞춘다는 점이다. 리팩토링을 하고 나면 성능이 좋아질 수도, 나빠질 수도 있다. 반대로, 성능 개선을 하고 나면 코드를 다루기 더 까다로워질 수도 있다.
Kent Beck의 The Two Hats 비유 🎩🎩
- 🎩 기능을 추가할 때는 기존에 존재하는 코드를 (최대한) 수정하지 않고, 기능과 그에 맞는 테스트 케이스만 추가하여 테스트한다.
- 🎩 리팩토링할 때는 기능과 테스트를 추가하지 않고 코드만 수정한다 (물론 이전에 실수로 빠뜨린 테스트 케이스가 있으면 추가하고, 리팩토링으로 인해 인터페이스 구조가 바뀌면 테스트 케이스를 수정한다).
- 내가 개발할 땐 이 두 개의 모자를 자주 바꿔쓴다. 기능 추가용 모자를 쓰고 개발을 하다가, 코드를 🐶떡같이 썼다는 사실을 깨닫곤 리팩토링용 모자로 바꿔 쓴 다음 구조를 개선해나간다. 구조가 어느 정도 깔끔해졌다고 생각되면 다시 기능 추가용 모자로 바꿔 쓰고 기능 추가를 이어나간다.
- 따라서, 내가 현재 어떤 모자를 쓰고 개발을 하고 있는지 자각할 필요가 있으며, 각 모자의 용도에 맞게 개발을 하는 것이 좋다.
왜 리팩토링을 해야 하는가?
- 리팩토링은 만병통치약이 아니다. 하지만 좋은 도구임은 틀림없기에, 다음과 같은 이유로 인해 (웬만하면) 리팩토링을 하는 것이 좋다.
리팩토링은 소프트웨어의 디자인을 개선한다
- 리팩토링을 하지 않는다면 소프트웨어의 내부 설계, 즉 아키텍처가 부식(decay)된다. 단기적인 목표를 위해 아키텍처를 완전히 이해하지 않은 채 코드를 변경한다면 코드의 구조가 망가진다. 이렇게 코드의 구조가 망가지면 코드를 읽어서 그 구조를 파악하는 것이 더 어려워진다.
- 코드의 구조를 파악하는 것이 어려워지면 그 코드를 유지 보수하는 것이 더 힘들어지고, 그로 인해 코드가 더 빨리 부식된다. 하지만 주기적인 리팩토링을 통해 이러한 일을 방지할 수 있다.
- 잘못 설계된 코드는 군데군데 중복이 존재하기 때문에 같은 동작을 하더라도 더 많은 양의 코드로 구성된다. 따라서 코드 설계를 개선하는 중요한 측면 중 하나는 바로 중복을 제거하는 것이다.
- 중복을 제거한다고 해서 코드가 더 빨라지거나 그러지는 않는다. 하지만 중복을 줄이게 되면 코드를 수정할 때 (훨씬) 쉽게 수정할 수 있게 된다. 코드가 많다는 것은 이해해야 할 코드가 더 많이 있다는 뜻이고, 그로 인해 코드를 제대로 수정하기 힘들어진다.
- 코드에 중복이 있는 경우, 코드를 수정할 때 실수로 중복된 코드들 중 하나를 바꾸지 못하게 되면 버그가 발생하게 될 것이다. 중복을 제거하게 되면 수정할 때 (한 곳에서) 한 번만 바꾸면 된다. 중복이 없는 것, 이것이 좋은 설계의 본질이다.
리팩토링은 코드를 더 이해하기 쉬운 형태로 만든다
- (비록 우리가 종종 까먹곤 하지만) 컴퓨터 보다, 코드를 읽는 사람이 훨씬 중요하다. 코드를 컴파일할 때 몇 번의 사이클을 더 돌게 된다고 해서 누가 신경 쓰겠는가? 하지만 내가 짠 코드를 제대로 이해했다면 한 시간이면 걸릴 일을 일주일 동안 하게 된다면 이건 매우 큰 문제다.
- 미래에 내 코드를 읽을 개발자를 위해서라도 리팩토링을 해서 코드를 더 읽기 쉽게 만들어야 한다. 미래의 개발자가 나 자신이 될 수도 있다.
리팩토링은 버그를 발견하기 쉽게 해준다
- (리팩토링을 통해) 코드를 이해하기 쉬워진다는 말은 버그를 찾기 쉬워진다는 말과 똑같다.
- 리팩토링은 개발자로 하여금 탄탄한 코드를 더욱 효과적으로 작성할 수 있게 해준다.
리팩토링은 프로그램을 더욱 빨리 개발할 수 있게 해준다
- 결국 앞서 살펴본 사항들은 다음으로 귀결된다: “리팩토링은 더 빠르게 개발할 수 있도록 도와준다”
- 리팩토링이 프로그램을 더욱 빨리 개발할 수 있게 한다는 말이 비직관적일 수 있다. 경력이 있는 개발자들과 이야기를 해보면, “처음에는 빠르게 개발할 수 있었지만 가면 갈수록 새로운 기능을 추가하는데 더 오랜 시간이 걸려요”와 같은 말을 종종 듣곤 한다. 새로운 기능을 어디에 어떻게 추가해야 할지 이해하는데 점점 더 많은 시간이 걸리고, 기능이 추가됐을 때 버그가 발생해서 디버깅하는 시간에 더 많은 시간을 쏟아붓는다고 한다. 때때로는 “차라리 갈아엎고 처음부터 만드는 게 나을 것 같아요”라는 말을 하곤 한다.
- 반대로, 코드 퀄리티가 좋으면 새로운 기능을 어디에 어떻게 추가해야 하는지 쉽게 찾을 수 있게 해주고, 모듈화가 잘되어있으면 변경하기 위해 이해해야 할 (기존의) 코드의 양을 줄여준다. 코드가 명확하면 버그를 만들어낼 가능성이 작아지고, 버그를 만들어낸다고 해도 디버깅을 더욱더 쉽게 할 수 있게 된다.

- 20여 년 전에 받아들여지던 정설은, 좋은 설계는 코딩하기 전에 완성되어야 한다는 것이었다. 하지만 리팩토링을 통해 시간이 지남에 따라 설계를 더 개선해나갈 수 있다. 사전에 좋은 설계를 한다는 것은 매우 어려운 일이므로, 리팩토링은 필수적인 요소가 되었다고 할 수 있다.
언제 리팩토링 해야 하는가?
무언가 비슷한 일을 3번 이상 하고 있다면, 리팩토링할 시간이다.
예비 리팩토링: 기능을 추가하기 쉽게 만들자
- 리팩토링하기 가장 알맞을 때는 새로운 기능을 추가하기 직전이다. 기능을 추가하기 직전에, 기존 코드를 한번 살펴보고 구조를 살짝 바꾸면 작업하기 더 수월해진다.
- 비유를 하자면, 동쪽으로 100km를 가야 할 때 곧장 동쪽으로 산을 타고 가는 것보다 20km 북쪽으로 간 다음 (차를 타고) 고속도로로 가는 것이 훨씬 빠르다. 사람들이 “그냥 바로 가!”라고 할 때, “잠깐만, 우선 지도를 보고 가장 빠른 길을 알아봐야겠어”라고 할 줄 알아야 한다. 예비 리팩토링도 이와 같은 이치이다.
이해를 위한 리팩토링: 더 이해하기 쉬운 코드로 만들자
- 코드를 수정하기 전에, 우선 그 코드가 무엇을 하는지부터 이해해야 한다.
- 리팩토링함으로써 내가 머릿속에서 이해한 것을 코드에 그대로 반영할 수 있다. 머릿속으로 이해한 것을 코드에 반영하게 되면 그 내용을 더욱더 오래 보존할 수 있고, 팀원들에게도 공유할 수 있다.
- 코드를 분석할 때, 리팩토링은 나로 하여금 더 높은 차원의 이해를 가능케 한다.
- 이해를 위해 리팩토링하는 것을 쓸모없는 것으로 치부하는 사람들은 자신들이 혼란 속에 숨겨진 기회를 보지 못한다는 것을 결코 깨닫지 못한다.
쓰레기(엉망인 코드) 수거를 위한 리팩토링
- 코드가 어떤 기능을 하는지 이해는 했지만, 로직이 쓸데없이 난해하다거나, 중복된 코드가 존재하는 등 코드가 그 기능을 잘 못할때(doing it badly) 리팩토링한다. 만약 현재 급하게 처리해야 할 일이 있는 경우 일단 메모해놓고 나중에 와서 리팩토링하고, 고치기 쉬운 일이라면 즉시 리팩토링한다.
- 리팩토링의 좋은 점은, 리팩토링의 각 작은 단계마다 코드가 고장 나지 않는다는 점이다. 비록 이 때문에 리팩토링을 완료하는 데까지 몇 달이 걸릴 수도 있지만, 부분적으로 리팩토링을 한다고 해서 코드가 절대 고장 나지 않는다.
계획적이고 기회적인 리팩토링
- 예비 리팩토링, 이해를 위한 리팩토링, 쓰레기 수거를 위한 리팩토링은 모두 기회가 될 때마다 하는 리팩토링이다. 즉, 따로 시간을 내어 이러한 리팩토링들을 하는 것이 아니란 소리다. 이러한 리팩토링들은 프로그래밍 흐름의 일부분이다.
- 새 기능을 추가하거나 디버깅할 때, 리팩토링은 내가 현재 해야 할 일을 도와줄 뿐만 아니라 미래의 일을 더 쉽게 할 수 있도록 만들어준다.
- 리팩토링은 프로그래밍과 별개의 활동이 아니다. 하지만 많은 사람들이 이 점을 놓치고 있다.
- 리팩토링을 과거의 실수를 고치거나, 혹은 이상한 코드를 깔끔하게 하는 작업이라고 보는 잘못된 시각이 존재한다. 물론 이상한 코드를 보았을 때 리팩토링 해야 하는 건 맞지만, 훌륭한 코드도 꽤 많은 리팩토링을 해야 한다.
- 좋은 개발자들은 때때로 새로운 기능을 가장 빠르게 추가하는 방법이 우선 추가하기 용이하도록 코드를 변경한 다음 기능을 추가하는 것임을 알고 있다.
- 소프트웨어 개발을 “끝냈다”라고 생각해서는 안 된다. 새로운 요구사항이 추가되면 그에 맞춰 소프트웨어를 변경해야 하기 때문이다. 간혹 새로운 코드의 양보다 기존 코드에서 변경되는 양이 더 많을 수도 있다.
- 계획적인 리팩토링이 항상 잘못된 것은 아니다. 하지만 그 빈도가 드물어야 한다. 리팩토링에 들이는 노력은 대부분 눈에 띄지 않고, 그때그때 하는 것이 좋다.
- 리팩토링 작업물과 새 기능 추가 작업물을 서로 다른 커밋으로 분리하라는 조언을 들은 적이 있다. 이렇게 했을 때의 가장 큰 이점은 리팩토링 커밋과 새 기능 추가 커밋을 따로 리뷰할 수 있기 때문이라고 한다.
- 하지만 나는 이 의견에 별로 공감하지 않는다. 리팩토링과 새 기능 추가 작업은 서로 얽혀있는 경우가 대부분이기 때문이다. 리팩토링과 새 기능 추가 작업을 서로 분리하게 되면 리팩토링 작업을 정당화하기 힘들어질 수도 있다.
- 이와 관련해서는 각자 팀 사정에 맞게 이를 적용하는 것이 바람직하다.
변경을 하기 전에, 우선 변경이 쉽도록 만들어라 (주의: 어려울 수 있다!). 그리고 나서 쉽게 변경하라. - 켄트 백
장기적인 리팩토링
- 대부분의 리팩토링은 기껏해야 몇 시간 내로 끝낼 수 있다. 하지만 때때로 몇 주 혹은 그 이상 리팩토링을 해야 하는 경우가 있을 수 있다.
- 하지만 이러한 경우에 팀 전체가 몇 주 동안 리팩토링에만 매달려 있는 것은 좋지 않다고 생각한다. 그보단 점진적으로 조금씩 해결해나가는 것이 효과적일 때가 많다.
코드 리뷰에서의 리팩토링
- 코드 리뷰는 지식이 개발팀 전반에 퍼지도록 도와준다. 또한 시니어 개발자들의 지식을 주니어 개발자들에게 전수하도록 도와준다.
- 코드리뷰는 깨끗한 코드를 작성하는 측면에서도 굉장히 중요하다. 내가 짠 코드가 나한텐 잘 읽힐지라도 다른 사람에겐 잘 읽힐지 아닐지 모르기 때문이다.
- 또한 코드리뷰는 더 많은 사람들이 유용한 아이디어를 제시하도록 도와준다. 나 스스로는 기껏해야 일주일에 몇 개 정도의 유용한 아이디어를 떠올릴 수 있겠으나, 코드 리뷰를 통해 다른 사람이 (내 코드에 대한) 유용한 아이디어를 제시하도록 할 수 있다.
- 코드 리뷰를 할 때 리팩토링을 활용하게 되면 제안을 제안으로 끝내지 않고 실체화 할 수 있다. 따라서 제안이 반영되었을 때의 코드를 상상하는 걸 넘어서, 리팩토링을 통해 실제로 제안을 반영해볼 수 있다.
- 일반적으로 많이 하는 PR을 통한 코드 리뷰 방식엔 리팩토링이 잘 맞지 않는다고 생각한다. 왜냐면 (대부분) 리뷰어가 코드 작성자 없이 코드를 검토하기 때문이다.
- 코드 리뷰를 할 땐 코드 작성자와 리뷰어가 함께 리뷰를 해야 작성자가 리뷰어에게 코드의 문맥을 설명해줄 수도 있고, 또한 리뷰어의 의도를 작성자에게 잘 전달할 수 있다 (일종의 페어 프로그래밍 방식과 흡사하다).
매니저에게 말해야 하나?
- 리팩토링을 단순히 디버깅 등으로 취급하고, 딱히 가치가 없는 일이라고 여기는 매니저가 있을 수 있다. 물론, 매니저들 (그리고 사용자들) 에게는 코드의 상태가 생산성에 어떠한 영향을 미치는지 모르는 경우가 많다.
- 매니저가 리팩토링에 딱히 관심이 없는 경우, 매니저에게 말하지 마라!
- 나는 이것이 반기를 드는 행위라고 생각하지 않는다. 개발자들은 프로다. 개발자들의 일은 효과적인 소프트웨어를 최대한 빨리 개발하는 것이다. 그리고 내 경험에 의하면, 리팩토링을 통해 더욱 빠르게 개발을 할 수 있다.
언제 리팩토링을 하지 말아야 하는가?
- (코드가 엉망일지라도) 수정할 필요가 없는 코드의 경우, 굳이 리팩토링할 필요가 없다. 또한 아예 처음부터 다시 만들어야 하는 경우도 있을 수 있다. 물론 리팩토링을 하는 게 나은지, 아예 처음부터 다시 짜는 게 나은지 결정하는 게 쉽지는 않지만.
- 직접 리팩토링해보기 전엔 (어떤 코드를 리팩토링하는 것이) 쉬운지 어려운지 판단하기 힘든 경우가 많다. 이는 뛰어난 판단력과 많은 경험이 뒷받침되어야 가능한 일이다.
리팩토링과 관련된 문제들
- 세상에 무조건 좋기만 한 것들은 거의 찾아보기 힘들다. 리팩토링은 물론 훌륭한 도구이지만, 몇 가지 문제들이 존재하기 때문에, 이러한 문제들에 어떻게 대응할 수 있는지 이해하는 것이 중요하다.
새로운 기능 개발 속도 저하
- 많은 사람들이 리팩토링에 들이는 시간으로 인해 개발 속도가 지연된다고 하지만, 리팩토링의 궁극적 목표는 개발 속도를 향상하는 것, 더 적은 노력으로 더 많은 가치를 만들어내는 것이다.
- 여전히 현업에선 리팩토링을 많이 하지 않는다고 한다. 바꿔 말하자면, 개발자들은 리팩토링을 더 자주 할 필요가 있다.
- 건강한 코드를 경험해 보지 않고서는 코드가 건강할 때와 아플 때의 생산성 차이를 말하기 힘들다. 코드가 건강하면 기존의 코드를 새로운 코드와 결합하여 복잡한 새로운 기능을 빨리 추가할 수 있게 해준다.
- 내가 생각하기에 사람들이 흔히 걸려드는 함정 중 가장 위험한 것은, 리팩토링을 “클린 코드”, “좋은 엔지니어링 습관” 등으로 정당화한다는 점이다. 리팩토링의 요점은 코드가 얼마나 빛이 나는지 보여주는 것이 아니다!
- 리팩토링을 하는 이유는 순전히 경제적인 이유에서다. 기능을 더 빠르게 추가할 수 있고, 버그를 더 빠르게 잡아낼 수 있기 때문에 하는 것이다. 반드시 경제적인 이득이 리팩토링을 하는 주된 이유가 되어야 한다.
코드 소유권
- 리팩토링의 대부분은 단순히 내부 구조만 변경하는 것이 아니라, 다른 시스템과의 관계도 변경하는 경우가 많다.
- 예를 들어 함수 이름을 변경하는 경우, 함수를 호출하는 부분을 전부 찾을 수 있으면 쉽게 리팩토링할 수 있지만 만약 호출하는 부분이 다른 팀의 코드에 있고 내가 그 팀의 저장소에 접근 권한이 없을 수도 있다.
- 혹은, 그 함수가 API로 정의되어 고객들이 사용하고 있는 경우라면 그 함수가 누구에 의해서 얼마나 사용되는지조차 알 수 없다. 이렇게 인터페이스를 정의한 사람과 독립적인 고객에 의해 사용되는 인터페이스를 외부 공개 인터페이스(published interface) 라고 한다.
- 코드 소유권이 나뉘어 있으면 리팩토링에 방해가 된다. 물론 이러한 상황에서도 리팩토링을 할 수 있지만, 제약이 따른다.
- 나는 코드 소유권을 작은 단위로 나눠 관리하는 것에 대해 반대하는 입장이다. 대신, 소유권을 “팀”에 두어 팀원 누구나 팀의 코드를 변경할 수 있는 권한을 두는 것을 선호한다.
- 개발자마다 책임지는 영역이 있을 수 있지만, 이 말은 자신이 맡은 영역을 책임지고 관리해야 한다는 뜻이지 영역에 접근하는 것 자체를 막아선 안된다.
- 이렇게 소유권을 느슨하게 정하는 방식은 여러 팀들 간에도 적용될 수 있다. 어떤 팀은 다른 팀의 개발자가 자기 팀의 브랜치에 있는 코드를 수정하고 커밋 요청을 날릴 수 있는 오픈 소스 비스무리한 방식을 사용하기도 한다.
- 이러한 방식은 (접근을 최대한 차단하는) 강력한 소유권 방식과 완전히 풀어서 변경을 통제하기 힘든 방식의 절충안이 될 수 있다.
브랜치
- 일반적으로 많은 팀들이 VCS를 이용하여 각 팀원마다 브랜치를 하나씩 파서 작업을 하다가, 작업을 끝내면 메인 브랜치에 결과물을 합쳐서 팀원과 공유하는 방식을 사용한다. 주로 프로덕션 버전으로 배포할 때 메인 브랜치에 합치는 편이다.
- 이 방식을 선호하는 사람들은 이렇게 하면 메인 브랜치와 기능 추가 이력을 깔끔하게 관리할 수 있다는 점, 그리고 무언가 잘못되었을 때 이전으로 되돌아가기 수월하다는 주장을 한다.
- 하지만 개별 브랜치에서 너무 (오랫동안) 많은 작업을 하다 보면 나중에 메인 브랜치에 합치는 것이 힘들어질 수 있다. 따라서 대부분의 사람들은 메인 브랜치에 자주 합치거나, 개별 브랜치를 메인 브랜치에 대해 자주 리베이스 하는 방법을 사용한다. 하지만 많은 사람들이 각자의 브랜치에서 작업하는 경우 이 방식은 궁극적인 해결책이 되지 못한다.
- 나는 merge와 통합(integrate)을 구분하는 편이다. 개별 브랜치를 마스터 브랜치도 “merge” 하는 것은 단방향 작업이다. 개별 브랜치만 바뀌고 마스터 브랜치는 그대로다. 반면, “통합”은 메인 브랜치의 변화를 개별 브랜치로 가져온 다음 다시 메인 브랜치에 올리는 양방향 작업이다.
- 누군가 개별 브랜치에서 작업한 내용은 메인 브랜치에 통합하기 전엔 다른 사람이 알 수 없다. 그리고 메인 브랜치에 통합하고 나면 그 결과를 내 브랜치에 merge 해야 하는데 이는 상당한 노력이 들 수 있다.
- 특히 어려운 부분은 코드의 의미적인(semantic) 변화를 다루는 것이다. 현대 VCS들은 텍스트 변화를 merge 하는 데엔 탁월하지만 코드의 의미를 알지는 못한다.
- (크고 방대한) 복잡한 브랜치를 merge 하는 것은 상당히 까다롭기 때문에, 많은 사람들은 최대한 브랜치를 작게 유지하는 것을 선호한다. 예를 들면 이틀에 한 번씩 메인 브랜치에 합치는 것과 같이 말이다. 하지만 나와 같이 또 다른 몇몇 개발자들은 그보다 더 작게 유지하는 것을 선호한다.
- 이를 CI(Continuous Integration) 혹은 Trunk-Based Development라고 부른다. CI 방법을 사용하면 각 팀원들은 적어도 하루에 한 번씩 메인 브랜치에 합친다. 이렇게 하면 각 브랜치들이 서로 너무 갈라지는(diverge) 경향을 방지할 수 있어서 merge를 좀 더 쉽게 할 수 있도록 한다.
- CI를 적용하기 위해선 메인 브랜치를 건강하게 관리하고, 큰 기능을 세분화하고, 개발 중인 작업을 토글 할 수 있는 스위치(혹은 feature flag)와 같은 것을 이용하여 코드가 망가지지 않도록 하는 방법을 연습해야 한다.
- CI는 리팩토링과 잘 어울린다. 리팩토링은 종종 코드에 많은 (작은) 변화들을 유발하는데, 이 때문에 (널리 사용되는 함수 이름을 변경하는 등의 이유로) 특히 (semantic한) merge conflict에 취약하다. 이와 같은 문제로 인해 feature branch 전략을 사용하는 팀들이 리팩토링을 중단하는 경우를 심심찮게 볼 수 있다.
- 물론 feature branch 전략을 사용하지 말라는 소리는 아니다. 오픈소스 프로젝트의 경우 이 전략이 잘 맞을 수 있다. 하지만 (회사의) 개발팀에선 방대해진 각각의 feature branch로 인해 리팩토링이 힘들어질 수 있다. 이 때문에 가능한 한 최대한 자주 (main 브랜치 등으로) 각 브랜치들을 통합하는 것을 권장한다.
- 완전한 CI 방법을 사용하지 않더라도, 최대한 자주 통합하는 것을 권장한다.
테스팅
- 리팩토링의 주요 특성 중 하나는 겉보기 동작을 바꾸지 않는다는 점이다. 리팩토링을 “잘”한다면 어떠한 것도 고장 나지 않아야 한다.
- 사람이기에 실수할 수 있는 법이다. 하지만 실수를 빠르게 캐치한다면 걱정할 것 없다. 각 리팩토링 단계에서 일어나는 변화들은 작기 때문에 잘못된 부분을 빠르게 찾아낼 수 있고, 혹여 찾아내지 못한다고 해도 VCS를 통해 이전 버전으로 되돌릴 수 있다.
- 여기서 핵심은 에러를 빨리 찾아내는 것이다. 이렇게 하기 위해선 코드에 대한 종합적인 테스트 코드를 가지고 있어야 하며, 이것들을 빠르게 돌릴 수 있어야 한다. 다시 말해, 리팩토링을 할 때 자가 테스트 코드가 있어야 한다는 말이다.
- 자가 테스트 코드는 버그를 빠르게 찾아내서 제거할 수 있기 때문에 리팩토링뿐만 아니라 새 기능을 추가하는 데에도 큰 도움이 된다.
- 자가 테스트 코드는 CI와도 연관되어 있으며, XP의 구성요소 중 일부로서 CD의 핵심 부분 중 하나이다.
레거시 코드
- 리팩토링은 레거시 시스템을 이해하는데 큰 도움을 준다. 올바르지 않은 함수 이름을 고치고, 어설픈 구문을 매끄럽게 다듬어 원석같던 프로그램을 보석으로 바꿀 수 있다.
- 하지만 테스트 없이는 거대한 레거시 시스템을 “안전하게” 리팩토링할 수 없다. 이에 대한 해답은 (당연하게도) 테스트를 추가하는 것이다.
- 물론 말로는 간단해 보이지만 실제로는 까다롭다. 일반적으로, 테스트를 염두에 두고 설계한 시스템일 경우에만 테스트하기 쉽다. 보통 이 경우 테스트 코드가 있기 마련이라서, 따로 신경 쓸 건 없다.
- 쉽게 해결할 수 있는법은 없다. 그나마 해줄 수 있는 조언은 Working Effectively with Legacy Code에 나온 지침을 따르라는 것이다. 이 책을 대강 요약해보자면, 테스트를 추가할 수 있는 틈새를 찾아서 테스트를 해보라는 것이다.
- (테스트가 없는) 레거시를 리팩토링하는 작업은 힘들고 어려운 작업이다. 이 문제를 쉽게 해결할 수 있는 방법은 아쉽게도 없다. 이것이 자가 테스트 코드를 시작부터 추가해야 한다고 내가 그토록 강조하는 이유이다.
- 만약 레거시 코드를 테스트할 수 있다고 해도 한꺼번에 고치려고 하지 않는 걸 추천한다. 서로 연관된 부분들을 찾아 조금씩 공략해 나가는 것을 추천한다. 레거시 시스템의 크기가 크다면 자주 보는 코드를 더 많이 리팩토링하라.
리팩토링, 아키텍처, 그리고 YAGNI
- 리팩토링은 사람들의 소프트웨어 아키텍처에 관한 인식을 크게 바꿔놓았다.
- 커리어 초반에 나는 코딩을 하기 전에 설계를 (거의) 끝마쳐야 한다고 배웠다. 코드를 작성하기 시작하면 아키텍처는 확정된 채 개발자들의 부주의함으로 인해 부식되어갈 뿐이었다. 리팩토링은 이러한 관점을 바꿔주었다. 몇 년 동안 서비스되던 프로그램의 아키텍처를 상당히 바꾸는 것도 가능하게 해주었다.
- 리팩토링은 변화하는 요구에 빠르게 대응할 수 있는 아키텍처를 설계하는 데에 큰 영향을 미친다.
- 코딩 전에 아키텍처를 확정하는 게 왜 문제가 되냐면, 이러한 방법은 사용자의 요구사항을 초기에 (잘) 알 수 있다고 가정하기 때문이다. 하지만 경험에 의하면 이는 일반적으로 (사실상) 불가능하다. 사용자는 실제로 소프트웨어를 사용해 보고, 업무에 미치는 영향을 직접 확인하고 나서야 정말 그들이 원하는 바를 알게 되는 경우가 허다하다.
- 미래의 변화에 대응하는 방법 중 하나는, 소프트웨어에 유연한 메커니즘을 적용하는 것이다. 예를 들어 함수를 만들 때 범용적으로 사용할 수 있겠다고 생각할 수 있다. 그래서 추후 일어날 수도 있는 다양한 상황에 대비한 매개변수들을 구성한다. 이러한 매개변수들이 유연한 메커니즘이다.
- 하지만 대부분의 메커니즘들이 그렇듯이, 항상 좋기만 한 것은 없다. 이러한 매개변수들을 당장 적용하게 되면 현재 함수의 역할에 비해 구조가 너무 복잡해진다.
- 변화가 내가 예상했던 것과는 다른 방향으로 발생할 수도 있고, 메커니즘 설계가 잘못될 수도 있다. 여러 가지를 종합적으로 고려해 보았을 때, 대부분의 경우 유연한 메커니즘으로 인해 오히려 변화에 제대로 대응하지 못하게 되는 경우가 많다.
- 리팩토링과 함께라면 다른 전략을 사용할 수 있다. 미래에 일어날 일을 추측하기 보다, 우선 현재까지 이해한 요구사항만을 해결하는 소프트웨어를 구성하고 그 요구사항을 잘 해결하도록 설계한다.
- 이후 사용자의 요구사항을 더 잘 알게 되면 리팩토링을 활용하여 아키텍처를 그에 맞게 변경하면 된다. 이 과정에서 함수의 이름같이 소프트웨어의 복잡도를 증가시키지 않는 메커니즘은 얼마든지 추가할 수 있지만, 복잡도를 증가시킬 수 있는 메커니즘은 반드시 추가하기 전에 검증을 거쳐야 한다.
- 이러한 접근법을 흔히 단순 설계법, 점진적 설계법, 혹은 YAGNI(You Aren’t Gonna Need It)라고 부른다.
- YAGNI 방법을 적용한다고 해서 사전에 아키텍처를 생각하지 말라는 뜻이 아니다. 여전히 리팩토링을 적용하기 어려운 부분이 있을 수 있고, 사전에 생각을 해놓는 것이 시간을 절약하는 경우도 있다.
- 다만 이 둘(미리 생각하느냐, 그때 가서 생각하느냐) 사이의 균형이 많이 바뀌었다. 나는 개인적으로 나중에 문제를 더 깊이 이해할 수 있게 되었을 때 처리하는 쪽을 선호한다.
리팩토링과 소프트웨어 개발 프로세스
- 리팩토링의 효과는 팀이 사용하고 있는 개발 방법에 따라 달라질 수 있다.
- 리팩토링이 사용되기 시작한 것도, CI·자가 테스트 코드·리팩토링과 같이 상대적으로 개성이 강하고 상호의존적인 방법들로 구성된 XP가 도입되면서부터이다.
- 애자일 방법론을 쓰는 프로젝트들 대부분은 단순히 “애자일” 이라는 이름만 가져다 쓴다. 실제로 애자일한 방식으로 개발하기 위해선 리팩토링에 대한 팀 구성원 모두의 역량과 의지가 뒷받침되어야 하고, 그러기 위해선 개발 프로세스에 리팩토링이 스며들어야 한다.
- 팀에서 개발하면서 리팩토링하려면 다른 팀원들을 방해하지 않으면서 언제든지 리팩토링할 수 있어야 한다. 이것이 내가 CI를 강조하는 이유이다. CI를 하게 되면 각 팀원의 리팩토링 결과를 다른 팀원과 빠르게 공유할 수 있다.
- 자가 테스트 코드도 CI의 중요한 요소 중 하나이다. 따라서 CI·자가 테스트 코드·리팩토링 사이에는 강력한 시너지가 존재한다.
- 이러한 세 방법을 적용한다면 YAGNI 설계를 적용할 수 있다. 미래의 (사용될지 안될지도 모르는) 기능을 대비해 작성한 코드보다 간단한 시스템을 수정하는게 더 쉽다.
- 또, 이러한 핵심 방법들을 적용하면 애자일 방법론이 주는 또 다른 이점을 누릴 수 있다. CD는 소프트웨어를 항상 릴리즈할 수 있는 상태를 유지해준다. 기술적인 강점이 있는 회사의 경우 아이디어를 production 코드로 변환하는 데 걸리는 시간을 획기적으로 단축시켜 고객에게 더 나은 UX를 제공할 수 있다.
- 물론 말로는 쉬운 것 같지만 실제론 어렵다. 소프트웨어 개발은 사람과 컴퓨터간의 복잡한 상호작용이 수반된 까다로운 일이기 때문이다.
리팩토링과 성능
- 흔히 리팩토링이 성능에 영향을 끼칠까 걱정을 하곤 한다. 나는 개인적으로 소프트웨어의 가독성을 증대하기 위해 프로그램이 더 느려질 수도 있는 방법들을 사용하곤 한다.
- 분명 리팩토링으로 인해 프로그램의 성능이 더 안 좋아질 수도 있다. 하지만 그와 동시에 성능 개선에 용이하도록 구조를 변경한다. 리얼타임 시스템을 제외하고, 소프트웨어를 빠르게 만드는 비법은 우선 튜닝 가능한 소프트웨어를 만든 다음 충분한 속도가 나올 때까지 소프트웨어를 튜닝하는 것이다.
-
내가 경험한 빠른 소프트웨어를 작성하는 세 가지 방법은 다음과 같다:
- 시간 분배 방법:
- 가장 엄격한 방법으로, 흔히 리얼 타임 시스템에 사용된다.
- 설계를 여러 컴포넌트로 분할하여 각 컴포넌트마다 자원을 할당하고, 할당된 자원을 초과하지 못하도록 한다. 자원을 주고받는 메커니즘은 허용될 수 있다.
- 시간 엄수에 초점을 맞추며, 인공 심장 박동기(heart pacemaker)와 같은 시스템을 만들 때 사용된다.
- 기업 정보 시스템등에는 어울리지 않는 방식이다.
- 끊임없이 주의를 기울이는 방법:
- 성능을 높이 끌어올릴 수 있다면 무엇이는 한다.
- 직관적이어서 흔히 사용되지만 잘 안된다.
- 성능 개선을 위해 코드를 변경하게 되면 대부분 이해하기 어려운 코드가 된다. 이로 인해 전체적인 개발 속도가 더뎌진다.
- 프로그램 동작 방식에 대한 편협한 시각과 컴파일러, 하드웨어 등의 동작원리에 대한 잘못된 이해가 곁들여진 성능을 개선하기 위한 방법들이 프로그램 여기저기에 존재하게 된다.
- 프로그램의 성능에 관한 흥미로운 사실은, 극히 일부 코드에서 대부분의 시간이 소비된다는 점이다. 만약 모든 코드를 균등하게 최적화 하였다면 그 중 90%는 사실상 효과가 없기에 시간낭비한 것이나 다름없다.
- 마지막 세 번째 방법은 바로 이 90% 통계에 기반한 것이다. 이 방법은 성능은 추후에 따로 짬을 내어 개선할 때까지 신경쓰지 않으면서 잘 정돈된(well-factored) 코드를 개발하는 방식이다. 이후 성능 개선작업을 할 때 특정 절차를 따라 프로그램을 튜닝한다.
- 성능 개선작업을 할 땐, 우선 프로파일러를 통해 어디서 병목이 걸리는지 살펴보고 이후 병목이 발생하는 부분을 집중적으로 개선한다.
- 이 방식을 프로그램의 전체적인 성능이 사용자가 만족할 만한 수준으로 나올 때까지 수행한다. 이렇게 하면 효율적으로 성능 개선을 할 수 있다.
- 리팩토링을 통해 더 빠르게 개발할 수 있음을 확인했다. 짧게 보자면 리팩토링으로 인해 시간이 다소 소모되겠지만, 결국에 전체로 보면 더욱 빠르게 개발할 수 있게 된다.
(성능 개선을 할 때) 만약 전반적인 시스템을 아주 잘 알고 있다고 하더라도 성능을 ⭐측정⭐하라. 절대 추측하지 마라. 열에 아홉은 틀릴것이다. - 론 제프리스
나만의 세줄 요약
- 처음부터 클린한 코드를 짜는 것은 결코 쉽지 않다. 리팩토링을 적극 활용하자.
- (다시 한번 강조) 테스트는 선택이 아닌 필수!
- 리팩토링은 기능을 추가하기 쉽게 해주고, 버그를 빨리 찾아낼 수 있게 해주며, 성능을 더 쉽게 개선할 수 있도록 해준다.