[리팩터링 2판 - ch.2] - 리팩터링 원칙

늘보·2022년 3월 1일
0

Refactoring

목록 보기
3/4

앞 장에서는 간단한 예시로 저자가 생각하는 리팩터링에 대해 소개했고, 이번 장에서는 시야를 넓혀 리팩터링 전반에 적용되는 원칙 몇 가지에 대해 알아보겠다. 먼저 정의부터 보자.

리팩터링 정의

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

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

두 개의 모자

저자는 소프트웨어를 개발할 때 목적이 '기능 추가'냐, 아니면 '리팩터링'이냐를 명확히 구분해 작업한다고 한다.

  • 기능 추가: 기능을 추가할 때는 '기능 추가' 모자를 쓴 다음, 기존 코드는 절대 건드리지 않고 새 기능을 추가하기만 한다.

  • 리팩터링: 리팩터링을 할 때는 '리팩터링' 모자를 쓴 다음, 기능 추가는 절대 하지 않기로 다짐한 뒤 오로지 코드 재구성에만 전념한다.

저자는 작업할 때 이 두 모자를 자주 바꿔쓴다고 하는데, 항상 자신이 쓰고 있는 모자가 무엇인지와 그에 따른 미묘한 작업 방식의 차이를 인지해야 한다고 한다.

저자는 이후에 리팩터링 커밋과 기능 추가 커밋을 분리해야 한다는 의견에는 완전히 동의하지는 않는다고 밝혔다. 리팩터링은 기능 추가와 밀접하게 엮인 경우가 매우 많기 때문이라고.

따라서 작업할 때 스스로 모자만 바꾸되, 기능/리팩터링 커밋을 나눌지는 각자의 팀에 적합한 방식을 적용하면 된다고 한다.

리팩터링하는 이유

📌 리팩터링하면 소프트웨어 설계가 좋아진다.

리팩터링하지 않으면 소프트웨어의 내부 설계(아키텍처)가 썩기 쉽다. 아키텍처를 충분히 이해하지 못한 채 단기 목표만을 위해 코드를 수정하다 보면 기반 구조가 무너지기 쉽다. 그러면 코드만 봐서는 설계를 파악하기 어려워진다. 코드 구조가 무너지기 시작하면 악효과가 누적된다. 코드만으로 설계를 파악하기 어려워질수록 설계를 유지하기 어려워지고, 설계가 부패되는 속도는 더욱 빨라진다. 반면 규칙적인 리팩터링은 코드의 구조를 지탱해줄 것이다.

📌 리팩터링하면 소프트웨어를 이해하기 쉬워진다.

프로그래밍은 여러 면에서 마치 컴퓨터와 대화하는 것과 같다. 컴퓨터에게 시킬 일을 표현하는 코드를 작성하면, 컴퓨터는 정확히 시킨 대로 반응한다. 그런데 내 소스 코드를 컴퓨터만 사용하는 게 아니다. 예컨데 몇 달이 지나 누군가 내 코드를 수정하고자 읽게 될 수 있다. 다른 프로그래머가 내 코드를 제대로 이해했다면 한 시간에 끝낼 수정을 일주일이나 걸린다면 문제다. 리팩터링은 코드가 더 잘 읽히게 도와주고, 코드의 목적이 더 잘 드러나며 내 의도를 더 명확하게 전달하도록 개선할 수 있다.

📌 리팩터링하면 버그를 쉽게 찾을 수 있다.

코드를 이해하기 쉽다는 말은 버그를 찾기 쉽다는 말이기도 한다. 리팩터링하면 코드가 하는 일을 깊이 파악하게 되면서 새로 깨달은 것을 곧바로 코드에 반영하게 된다. 프로그램의 구조를 명확하게 다듬으면 그냥 "이럴 것이다"라고 가정하던 점들이 분명히 드러나는데, 버그를 지나치려야 지날칠 수 없을 정도까지 명확해진다.

📌 리팩터링하면 프로그래밍 속도를 높일 수 있다.

한 시스템을 오래 개발 중인 개발자들은 "초기에는 진척이 빨랐지만 현재는 새 기능을 하나 추가하는 데 훨씬 오래 걸린다"라는 말을 많이 한다. 새로운 기능을 추가할수록 기존 코드베이스에 잘 녹여낼 방법을 찾는 데 드는 시간이 늘어난다는 것이다. 게다가 기능을 추가하고 나면 버그가 발생하는 일이 잦고, 이를 해결하는 시간은 한층 더 걸린다. 이러한 부담이 기능 추가 속도를 계속 떨어뜨리면서, 차라리 처음부터 새로 개발하는 편이 낫겠다고 생각하는 지경에 이른다.

이미지를 보면, 초반에는 나쁜 설계를 하고 빠른 개발을 하는 팀이 더 시간을 단축시킬 수 있지만, 결국 좋은 설계를 통해 리팩터링을 진행하는 팀이 더 빠른 개발을 한다는 그래프다.

물론 케바케라고 생각하지만, 좋은 설계가 개발에 큰 도움이 된다는 것은 정말 맞는 것 같다.

내부 설계가 잘 된 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있다. 모듈화가 잘 되어 있으면 전체 코드베이스 중 작은 일부만 이해하면 된다. 코드가 명확하면 버그를 만들 가능성도 줄고, 버그를 만들더라도 디버깅하기가 훨씬 쉽다. 내부 품질이 뛰어난 코드베이스는 새 기능 구축을 돕는 견고한 토대가 된다.

언제 리팩터링해야 할까?

저자는 리팩터링을 할 때 거의 한 시간 간격으로 한다고 한다. 그러다 보니 자신의 작업 흐름에 리팩터링을 녹이는 방법이 여러 가지가 있다는 것을 알게 되었다고 한다.

추가: 3의 법칙

Don Roberts 라는 사람이 저자에게 제시한 가이드라고 한다.

  1. 처음에는 그냥 개발한다.
  2. 비슷한 일을 두번째로 하게 되면(중복이 생겼다는 사실에 당황스럽겠지만), 일단 계속 진행한다.
  3. 비슷한 일을 세 번째 하게 되면 리팩터링한다.

저자는 이를 '삼진 리팩터링'으로 기억하자고 한다. (난 이거 괜찮은듯?)

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

리팩터링하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다. 이 시점에 현재 코드를 살펴보면서, 구조를 살짝 바꾸면 다른 작업을 하기가 훨씬 쉬워질 만한 부분을 찾아서 리팩토링을 한다.

이처럼 준비를 위한 리팩터링으로 상황을 개선해놓으면 버그가 수정된 상태가 오래 지속될 가능성을 높이는 동시에, 같은 곳에서 다른 버그가 발생할 가능성을 줄일 수도 있다.

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

코드를 수정하려면 먼저 그 코드가 하는 일을 파악해야 한다. 그 코드를 작성한 사람은 자신일 수도 있고 다른 사람일 수도 있다.

코드가 깔끔하게 정리되어 있으면, 전에는 보이지 않던 설계가 눈에 들어오기 시작한다. 코드를 분석할 때 리팩터링을 해보면, 그렇지 않았더라면 도달하지 못했을 더 깊은 수준까지 이해하게 된다.

🔨 쓰레기 줍기 리팩터링

코드를 파악하던 중에 일을 비효율적으로 처리하는 모습을 발견할 때가 있다. 로직이 쓸데없이 복잡하거나, 매개변수화한 함수 하나면 될 일을 거의 똑같은 함수 여러 개로 작성해놨을 수 있다.

원래 하려던 작업과 관련 없는 일에 너무 많은 시간을 빼앗기긴 싫을 것이다. 그렇다고 쓰레기가 나뒹굴게 방치해서 나중에 일을 방해하도록 내버려두는 것도 좋지않다. 수정하려면 몇 시간이나 걸리고 당장은 더 급한 일이 있을 수 있다.

그렇더라도 조금이나마 개선해두는 것이 좋다.

🔨 계획된 리팩터링과 수시로 하는 리팩터링

보기 싫은 코드를 발견하면 리팩터링하자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야한다. 무언가 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고(단, 만만치 않을 수 있다) 그런 다음 쉽게 수정하자.

그동안 리팩터링에 소홀했다면, 따로 시간을 내서 새 기능을 추가하기 쉽도록 코드베이스를 개선할 필요가 있다. 이때 리팩터링에 투자한 일주일의 효과를 다음 몇 달 동안 누릴 수도 있다.

그러나 계획된 리팩터링을 하게 되는 일은 최소한으로 줄여야 한다. 리팩터링 작업 대부분은 드러나지 않게, 기회가 될 때마다 해야한다.

🔨 오래 걸리는 리팩터링

가끔 팀 전체가 해도 몇 주가 걸리는 대규모 리팩터링이 있다. 저자는 이러한 상황에 팀 전체가 리팩터링을 하는 것에는 회의적이다. 그보다는 주어진 문제를 몇 주에 걸쳐 조금씩 해결해가는 편이 효과적일 때가 많다고 한다. 리팩터링이 코드를 깨뜨리지 않는다는 장점을 활용하는 것.

🔨 코드 리뷰에 리팩터링 활용하기

코드 리뷰는 개발팀 전체에 지식을 전파하는 데 좋다. 경험이 더 많은 개발자의 노하우를 더 적은 개발자에게 전수할 수 있다. 대규모 소프트웨어 시스템의 다양한 측면을 더 많은 사람이 이해하는 데도 도움된다. 깔끔한 코드를 작성하는 데에도 굉장히 중요하다.

내 눈에는 명확한 코드가 다른 팀원에게는 그렇지 않을 수 있다. 코드리뷰에 리팩터링을 접목하는 구체적인 방법은 리뷰의 성격에 따라 다르다.

흔히 쓰는 풀리퀘스트(코드 작성자 없이 검토하는 방식)에서는 그리 효과적이지 않다. 코드 작성자가 참석해야 맥락을 설명해줄 수 있고 작성자도 리뷰어의 변경 의도를 제대로 이해할 수 있으므로, 이왕이면 작성자가 참석하는 방식이 좋다.

저자가 경험한 가장 좋은 방법은 작성자와 나란히 앉아서 코드를 훑어가면서 리팩터링하는 것이다. 이렇게 하면 자연스럽게(프로그래밍 과정 안에 지속적인 코드 리뷰가 녹아 있는) 짝 프로그래밍이 된다.

❌ 리팩터링하지 말아야 할 때

지금까지의 이야기가 무조건 리팩터링을 권장한다고 들릴 수도 있는데, 리팩터링하면 안되는 상황도 있다.

저자는 지저분한 코드를 발견해도 굳이 수정할 필요가 없다면 리팩터링하지 않는다고 한다. 외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 둔다고 한다. 내부 동작을 이해해야 할 시점에 리팩터링해야 효과를 제대로 볼 수 있다.

또한 리팩터링하는 것보다 처음부터 새로 작성하는게 쉬울 때도 리팩터링하지 않는다고 한다. 그렇지만 이 결정은 저자 본인도 쉽지 않아서, 판단력과 경험이 뒷받침 되어야 한다고 함.

리팩터링 시 고려할 문제

저자는 리팩터링이 많은 팀에서 적극적으로 도입해야 할 중요한 기법이라고 하지만 그에 딸려오는 문제점도 분명히 있다고 한다. 따라서 이런 문제가 언제 발생하고 어떻게 대처해야 하는지 반드시 알고 있어야 된다고 한다.

새 기능 개발 속도 저하

많은 사람이 리팩터링 때문에 새 기능을 개발하는 속도가 느려진다고 여기지만, 리팩터링의 궁극적인 목적은 개발 속도를 높이는 데 있다. 하지만 리팩터링으로 인해 진행이 느려진다고 생각하는 사람이 여전히 많다. 아마도 이 점이 실전에서 리팩터링을 제대로 적용하는 데 가장 큰 걸림돌인 것 같다.

  • 리팩터링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것이다.

저자가 볼 때 가장 위험한 오류는 리팩터링을 '클린 코드'나 '바람직한 엔지니어링 습관'처럼 도덕적인 이유로 정당화하는 것이라고 하다.

리팩터링의 본질은 코드베이스를 예쁘게 꾸미는 데 있지 않다. 오로지 경제적인 이유(개발 기간을 단축하기 위해)로 하는 것이다.

리팩터링은 기능 추가 시간을 줄이고, 버그 수정 시간을 줄여준다. 이를 명확히 이해하는 설계자, 관리자, 개발자, 고객이 많아질수록 앞에서 본 소프트웨어 개발 진행 그래프에서 '좋은 설계' 곡선을 더 많이 볼 수 있다.

코드 소유권이 엄격할 때

리팩터링하다 보면 모듈의 내부뿐 아니라 시스템의 다른 부분과 연동하는 방식에도 영향을 주는 경우가 많다. 함수 이름을 바꾸고 싶고 그 함수를 호출하는 곳을 모두 찾을 수 있다면, 간단히 함수 선언바꾸기로 선언 자체와 호출하는 곳 모두를 한번에 바꿀 수 있다.

하지만 이렇게 간단하지 않을 때도 있다. 함수를 호출하는 코드의 소유자가 다른 팀이라서 나에게는 쓰기 권한이 없을 수 있다. 또는 바꾸려는 함수가 고객에게 API로 제공된 것이라면!! 누가 얼마나 쓰고 있는지를 고사하고, 실제로 쓰이기나 하는지조차 모를 수 있다.

이런 함수는 인터페이스를 누가 선언했는지에 관계없이 클라이언트가 사용하는 '공개된 인터페이스'에 속한다. 코드 소유권이 나뉘어 있으면 리팩터링에 방해가 된다. 클라이언트에 영향을 주지 않고서는 원하는 형태로 변경할 수 없기 때문이다. 그렇다고 리팩터링을 할 수 없는건 아니다. 여전히 훌륭하게 개선할 수 있지만 제약이 따를 뿐이다.

이런 경우, 저자는 코드 소유권을 느슨하게 하는 것이 중요하다고 한다.

브랜치

현재 흔히 볼 수 있는 팀 단위 작업 방식은 버전 관리 시스템을 사용하여 팀원마다 코드베이스의 브랜치를 하나씩 맡아서 작업하다가, 결과물이 어느 정도 쌓이면 마스터 브랜치에 통합해서 다른 팀원과 공유하는 것이다.

이런 방식에는 단점이 있는데, 각각의 브랜치에서 개발하는 기간이 길어질수록 문제가 기하급수적으로 커진다는 것이다. 이런 문제를 해결하기 위해 지속적 통합(CI)이 등장했다. CI에 따르면 모든 팀원이 최소 하루에 한 번은 마스터와 통합한다. 이렇게 하면 다른 브랜치들과의 차이가 크게 벌어지는 브랜치가 없어져서 머지의 복잡도를 상당히 낮출 수 있다고 한다.

하지만 CI를 적용하기 위해서는 치러야 할 대가가 있다. 마스터를 건강하게 유지하고, 거대한 기능을 잘게 쪼개는 법을 배워야 하기 때문이다. 이는 리팩토링의 역할과 일치한다.

하지만 풀타임 개발팀이라면 리팩터링 부담이 너무 클 수 있다. 그래서 CI를 완벽히 적용하지는 못하더라도 통합 주기만큼은 최대한 짧게 잡아야 한다고 한다.

테스팅

리팩터링의 두드러진 특성은 프로그램의 겉보기 동작은 똑같이 유지된다는 것이다. 절차를 지켜 제대로 리팩터링하면 동작이 깨지지 않아야 한다.

하지만 실수를 저지른다면? 실수하더라도 재빨리 해결하면 문제가 되지 않는다. 리팩터링은 단계별 변경 폭이 작아서 도중에 발생한 오류의 원인이 될만한 코드 범위가 넓지 않다. 원인을 못 찾더라도 버전 관리 시스템을 이용하여 가장 최근에 정상 작동하던 상태로 되돌리면 된다.

여기서 핵심은 오류를 재빨리 잡는 데 있다. 실제로 이렇게 하려면 코드의 다양한 측면을 검사하는 테스트 스위트(test suite)가 필요하다. 그리고 이를 빠르게 실행할 수 있어야 수시로 테스트하는 데 부담이 없다. 달리 말하면 리팩터링하기 위해서는 (대부분의 경우에) 자가 테스트 코드 를 마련해야 한다는 뜻이다.

자가 테스트 코드는 통합 과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와도 밀접하게 연관된다. CI에 통합된 테스트는 지속적 배포(CD - Continuous Delivery)의 핵심이기도 하다.

레거시 코드

레거시 코드를 파악할 때 리팩터링이 큰 도움이 된다. 그러나 이런 대규모 레거시 코드를 테스트 코드 없이 명료하게 리팩터링 하기는 어렵다.

따라서 이를 해결하기 위해서는 당연히 '테스트 보강'을 해야 하는데, 쉽게 해결할 방법은 없고 계속 소프트웨어가 설계된 구조를 생각하며 테스트 코드를 작성해야 한다고 한다.

저자가 선호하는 방식은 서로 관련된 부분끼리 나눠서 하나씩 공략하는 것이라고 한다. 코드의 한 부분을 훑고 넘어갈 때마다 예전보다 조금이라도 개선하려고 노력하는 것이다.(캠핑 규칙 - 처음 왔을 때보다 깨끗하게 치우자)

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

저자가 프로그래밍을 시작한지 얼마 되지 않을 때, 코딩 시작 전에 소프트웨어 설계와 아키텍처를 어느 정도, 심지어 거의 완료해야 한다고 배웠다고 한다. (나 또한 이 부분은 저자와 생각이 일치한다) 일단 코드로 작성된 뒤로는 아키텍처를 바꾸기 힘들고, 부주의로 인해 부패할 일만 남았다고 배웠다고 한다.

저자는 리팩터링을 활용하면 수년 동안 운영되던 소프트웨어라도 아키텍처를 대폭 변경할 수 있었다고 한다.

  • 앞으로 어느 부분에 유연성이 필요하고 어떻게 해야 그 변화에 가장 잘 대응할 수 있을지 추측하지 않고, 그저 현재까지 파악한 요구사항만을 해결하는 소프트웨어를 구축한다.

  • 진행하면서 사용자의 요구사항을 더 잘 이해하게 되면 아키텍처도 그에 맞게 리팩터링해서 바꾼다. 그 과정에서 소프트웨어의 복잡도에 지장을 주지 않는 메커니즘(함수, 변수명 변경 등)은 마음껏 추가하지만, 복잡도를 높일 수 있는 유연성 메커니즘은 반드시 검증을 거친 후에 추가한다.

이런 식의 설계를 점진적 설계(incremental design) 또는 YAGNI("you aren't going to need it = 필요 없을거다")라고 부른다.

YAGNI는 아키텍처와 설계를 개발 프로세스에 녹여내는 또 다른 방식이며, 리팩터링의 뒷받침 없이는 효과를 볼 수 없다고 한다.

YAGNI를 받아들인다고 해서 선제적인 아키텍처에 완전히 소홀해도 된다는 뜻은 아니다. 리팩터링으로는 변경하기 어려워서 미리 생각해두면 시간이 절약되는 경우도 얼마든지 있다.

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

실제로 리팩터링이 퍼지기 시작한것도 익스트림 프로그래밍(XP)에 도입됐기 때문이다. XP의 두드러진 특징은 지속적 통합, 자가 테스트 코드, 리팩터링 등의 개성이 강하면서 상호 의존하는 기법들을 하나로 묶은 프로세스라는 점이다.

  • 자가 테스트 코드와 리팩터링을 묶어서 테스트 주도 개발(TDD)이라고 한다.

애자일 소프트웨어 방법론이 등장한 이후, 몇 년간의 지속적인 시도를 통해, 이제는 애자일이 주류가 되었다. 하지만 저자는 현재 '애자일'을 내세우는 프로젝트 중에는 이름만 애자일인 경우가 대부분이라고 한다.

애자일을 제대로 적용하려면 리팩터링에 대한 팀의 역량과 열정이 뒷받침되어 프로세스 전반에 리팩터링이 자연스럽게 스며들도록 해야 한다.

리팩터링과성능

직관적인 코드 vs 성능은 중요한 주제다. 저자가 성능보다 직관적인 코드를 중요하게 여기는 이유는, 직관적인 코드를 작성하면 성능을 튜닝하기 더 쉬워지기 때문이라고 한다. 소프트웨어를 빠르게 만드는 비결은, 먼저 튜닝하기 쉽게 만들고 나서 원하는 속도가 나게끔 튜닝하는 것이라고 한다.

  • 여기서 튜닝은 조절? 혹은 수정 이라고 생각하면 되지 않을까 싶다.

책에서 '론 제프리'라는 사람이 쓴 '아무것도 안 만드는 데도 시간이 걸린다' 글을 보고 결론만 내자면 다음과 같다.

시스템에 대해 잘 알더라도 섣불리 추측하지 말고 성능을 측정해봐야 한다. 대부분의 상황에서 자신이 잘못 알고 있었음을 깨닫게 된다고.

성능에 대한 흥미로운 사실은, 대부분 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비한다는 것이다. 그래서 코드 전체를 고르게 최적화한다면 그 중 90%는 효과가 거의 없기 때문에 시간 낭비라고 말한다.

즉, 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경 쓰지 않고 코드를 다루기 쉽게 만드는 데 집중한다. 그러다 성능 최적화 단계가 되면 다음의 구체적인 단계를 따라 성능을 개선한다.

  • 먼저 프로파일러(React의 Profiler API, Vue.js의 vue-devtools, Chrome의 devtools 등)로 프로그램을 분석하여 시간과 공간을 많이 잡아먹는 지점을 찾아낸다.

  • 찾으면 개선하자!

결론

제일 인상 깊게 본 건 아래 문구였다.

저자가 볼 때 가장 위험한 오류는 리팩터링을 '클린 코드'나 '바람직한 엔지니어링 습관'처럼 도덕적인 이유로 정당화하는 것이라고 하다.

리팩터링의 본질은 코드베이스를 예쁘게 꾸미는 데 있지 않다. 오로지 경제적인 이유(개발 기간을 단축하기 위해)로 하는 것이다.

지금까지 리팩터링을 하는 이유는 유지 보수에 도움이 되고, 코드의 가독성을 높여 동료 개발자가 쉽게 코드 구조를 파악하게 하는데에 목적이 있다고 생각했다. 또한 리팩터링을 통해 클린코드가 완성된다고 생각했다.

그런데 이것도 편견이었다. 결국 리팩터링 또한 생산성을 높여, 개발기간 단축이 가장 큰 목적인 것을 알게 된게 큰 소득인듯 함.

추가로, 난 지금까지 YAGNI를 기준으로 볼 때, 테스트 코드 / 지속적 통합(CI) / 리팩터링 중 리팩터링 한 가지만 해왔다. 테스트 코드는 작성 경험이 있지만, 너무 시간이 많이 든다고 생각하는데.. 아마 테스트 코드 작성이 더 익숙해지면 저자의 말처럼 생산성이 올라가지 않을까 싶다.

주저리주저리 그러니까 결론은 CI도 해봐야겠다!

참고 링크

0개의 댓글