http://www.yes24.com/Product/Goods/89649360?OzSrank=1
https://github.com/WegraLee/Refactoring
https://github.com/wickedwukong/martin-fowler-refactoring-2nd
마틴파울러의 그 유명한 리팩토링 책의 2판이 나왔다.
그리고 1판은 예제가 자바이지만,
2판은 예제가 자바스크립트로 되어 있다👍
아래의 내용은 책을 읽으면서 정리한 것들이다.
Chap01. 리팩터링: 첫 번째 예시
1.1 자, 시작해보자!
https://codesandbox.io/s/playrefactoring2ndeditionexample-hgne9
1.2 예시 프로그램을 본 소감
- 프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면,
먼저 기능을 추가하기 쉬운 형태로 리팩터링하고 나서 원하는 기능을 추가한다.
- 리팩터링이 필요한 이유는 변경 때문이다. 잘 작동하고 나중에 변경할 일이 절대 없다면 코대를 현재 상태로 놔둬도 아무런 문제가 없다.
- 하지만 다른 사람이 읽고 이해해야 할 일이 생겼는데 로직을 파악하기 어렵다면 뭔가 대책을 마련해야 한다.
1.3 리팩터링의 첫 단계
- 테스트 코드 부터!
- 리팩터링하기 전에 제대로 된 테스트부터 마련한다. 테스트는 반드시 자가진단하도록 만든다.
1.4 statement() 함수 쪼개기
- 수정하고 나면 곧바로 컴파일하고 테스트해서 실수한 게 없는지 확인한다. 아무리 간단한 수정이라도 리팩터링 후에는 항상 테스트하는 습관을 들이는 것이 바람직하다.
- 조금씩 변경하고 매번 테스트하는 것은 리팩터링 절차의 핵심이다.
- 리팩터링은 프로그램 수정을 작은 단계로 나눠 진행한다. 그래서 중간에 실수하더라도 버그를 쉽게 찾을 수 있다.
- 변수 이름을 더 명확하게 바꾸기! 좋은 코드라면 하는 일이 명확히 드러나야 하며, 이때 변수 이름은 커다란 역할을 한다.
자바스크립트와 같은 동적 타입 언어를 사용할 때는 타입이 드러나게 작성하면 도움이 된다. 그래서 나는 매개 변수 이름에 접두어 타입 이름을 적는데. 매개 변수의 역할이 뚜렷하지 않을 때는 부정 관사(a/an)를 붙인다. ex) aPerformance
- 컴퓨터가 이해하는 코드는 바보도 작성할 수 있다. 사람이 이해하도록 작성하는 프로그래머가 진정한 실력자다.
- 지역 변수를 제거해서 얻는 가장 큰 장점은 추출 작업이 훨씬 쉬워진다는 것이다. 유효범위를 신경 써야 할 대상이 줄어들기 때문이다.
- 임시 변수는 나중에 문제를 일으킬 수 있다. 임시 변수는 자신이 속한 루틴에서만 의미가 있어서 루틴이 길고 복잡해지기 쉽다.
- 긴 함수를 작게 쪼개는 리팩터링은 이름을 잘 지어야만 효과가 있다. 이름이 좋으면 본문을 읽지 않고도 무슨 일을 하는지 알 수 있다. 물론 단번에 좋은 이름을 짓기는 쉽지 않다. 따라서 처음에는 당장 떠오르는 최선의 이름을 사용하다가, 나중에 더 좋은 이름이 떠오를 때 바꾸는 식이 좋다.
반복문을 쪼개는 것과 성능 이슈
무엇보다도 반복문을 쪼개서 성능이 느려지지 않을까 걱정할 수 있다.
이처럼 반복문이 중복된느 것을 꺼리는 이들이 많지만, 이 정도 중복은 성능에 미치는 영향이 미미할 때가 많다. 경험 많은 프로그래머조차 코드의 실제 성능을 정확히 예측하지 못한다. 똑똑한 컴파일러들은 최신 캐싱 기법 등으로 무장하고 있어서 우리의 직관을 초월하는 결과를 내어주기 때문이다. 또한 소프트웨어 성능은 대체로 코드의 몇몇 작은 부분에 의해 결정되므로 그 외의 부분은 수정한다고 해도 성능 차이를 체감할 수 없다.
=> 결론: 특별한 경우가 아니라면 성능 이슈느 무시하라
- 우리의 목표는! 더 깔끔하면서 더 빠른 코드!
1.5 중간 점검: 난무하는 중첩 함수
1.6 계산 단계와 포맷팅 단계 분리하기
1.7 중간 점검: 두 파일(과 두 단계로)로 분리됨
- 간결함이 지혜의 정수일지 몰라도, 프로그래밍에서만큼은 명료함이 진화할 수 있는 소프트웨어의 정수다.
- 캠핑자들에게는 "도착했을 때보다 깔끔하게 정돈하고 떠난다"는 규칙이 있다. 프로그래밍도 마찬가지다. 항시 코드베이스를 작업 시작 전보다 건강하게(healthy) 만들어놓고 떠나야 한다.
1.8 "다형성"을 활용해 계산 코드 재구성하기
- 조건부 로직을 명확한 구조로 보완하는 방법!
- 다형성(poly-morphoism)을 활용!
- 이번 작업의 목표는 상속 계층을 구성해서 희극 서브클래스와 비극 서브클래스가 각자의 구체적인 계산 로직을 정의하는 것
- 조건부 로직을 다형성으로 바꾸기: 이 리팩터링은 조건부 코드 한 덩어리를 다형성을 활용하는 방식으로 바꿔준다용하는 방식으로 바꿔준다
1.9 상태 점검: 다형성을 활용하여 데이터 생성하기
1.10 마치며
- 함수 추출하기, 변수 인라인하기, 함수 옮기기, 조건부 로직을 다형성으로 바꾸기, 단계 쪼개기
- 리팩토링은 대부분 코드가 하는 일을 파악하는 데서 시작한다. 그래서 코드를 읽고, 개선점을 찾고, 리팩터링 작업을 통해 개선점을 코드에 반영한느 식으로 진행한다. 그 결과 코드가 명확해지고 이해하기 더 쉬워진다.
- 좋은 코드를 가늠하는 확실한 방법은 '얼마나 수정하기 쉬운가'다.
- 코드는 명확해야 한다. 모드를 수정해야 할 상황이 되면 고쳐야 할 곳을 쉽게 찾을 수 있고 오류 없이 빠르게 수정할 수 있어야 한다.
- 리팩터링을 효과적으로 하는 핵심은, 단계를 자게 나눠야 더 빠르게 처리할 수 있고, 코든은 절대 깨지지 않으며, 이러한 작은 단계들이 모여서 상당히 큰 변화를 이룰 수 있다는 사실을 깨닫는 것이다.
Chap02. 리팩터링 원칙
2.1 리팩터링 정의
- 리팩터링(명사): 소프트웨어의 겉보기 동작은 그대로 유지한 채(리팩터링 전과 후의 코드가 사용자 관점에서 똑같이 동작한다는 의미), 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법
- 리팩터링하다(동사): 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러 가지 리팩터링 기법을 적용해서 소프트웨어를 재구성하다.
- 특정한 방식에 따라 코드를 정리하는 것만이 리팩터링이다.
- 리팩터링은 결국 동작을 보존하는 작은 단계들을 거쳐 코드를 수정하고, 이러한 단계들을 순차적으로 연결하여 큰 변화를 만들어내는 일이다.
- 리팩터링하는 동안에는 코드가 항상 정상 작동하기 때문에 전체 작업이 끝나지 않았더라도 언제든 멈출 수 있다.
- 누군가 "리팩터링하다가 코드가 깨져서 며칠이나 고생했다"라고 한다면, 십중팔구 리팩터링한 것이 아니다.
- 리팩토링은 재구성(코드베이스를 정리하거나 구조를 바꾸는 모든 작업) 중 특수한 형태로 본다.
- 리팩토링의 목적은 코드를 이해하고 수정하기 쉽게 만드는 것
2.2 두 개의 모자## 2.2 두 개의 모자
- '기능 추가' 모자와 '리팩터링' 모자
- 항상 내가 쓰고 있는 모자가 무엇인지와 그에 따른 미묘한 작업의 방식의 차이를 분명하게 인식해야 한다.
- 기능 추가 모자: 기존 코드는 절대 건드리지 않고 새 기능을 추가하기만 할 것!
- 리팩터링 모자: 기능 추가는 절대 하지 않기로 다지한 뒤 오로지 코드 재구성에만 전념
2.3 리팩터링하는 이유
- 리팩터링이 만병통치약은 아니지만, 건강한 상태로 유지하는 데 도와주는 약임은 분명하다
- 코드만으로 설계를 파악하기 어려워질수록 설계를 유지하지 어려워지고, 설계가 부패되는 속도는 더욱 빨라진다. 반면 규칙적인 리팩터링은 코드의 구조를 지탱해줄 것이다.
- 컴퓨터에게 시키려는 일과 이를 표현한 코드의 차이를 최대한 줄여야 한다.
- 기억할 필요가 있는 것들은 최대한 코드에 담으려고 한다.
- "난 뛰어난 프로그래머가 아니에ㅛ. 단지 뛰어난 습관을 지닌 괜찮은 프로그래머일 뿐이에요."
- 내부 설계가 잘 된 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있다.
- 모듈화가 잘 되어 있으면 전체 코드베이스 중 작은 일부반 이해하면 된다.
- 설계 지구력 가설 (Design Stamina Hypethesis)
- 리팩터링하면 기존 코드의 설계를 얼마든지 개선할 수 있으므로, 설령 프로그램 요구사항이 바뀌더라도 설계를 지속해서 개선할 수 있다.
2.4 언제 리팩터링해야 할까?
돈 로버츠의 3의 법칙
1. 처음에는 그냥 한다.
2. 비슷한 일을 두 번째로 하게 되면, 일단 계속 진행한다.
3. 비슷한 일을 세 번째 하게 되면 리팩터링한다.
- 준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기
:오류를 일으키는 코드가 세 곳에 복제되어 퍼져 있다면, 우선 한 곳으로 합치는 편이 작업하기에 훨씬 편하다. 또는 질의 코드에 섞여 있는 갱신 로직을 분리하면 두 작업이 꼬여서 생기는 오류를 크게 줄일 수 있다.
- 이해를 위한 리팩터링: 코드를 이해하기 쉽게 만들기
:리팩터링하면 머리로 이해한 것을 코드에 옮겨 담을 수 있다. 그런 다음 수정한 코드를 테스트해보면 내 생각이 맞았는지 확인할 수 있다. 내가 이해한 것을 코드에 반영해두면 더 오랜 보존할 수 있을 뿐만 아니라 동료들도 알수있다.
- 쓰레기 줍기 리팩터링
:간단히 수정할 수 있는 것은 즉시 고치고, 시간이 좀 거리는 일은 짦은 메모만 남긴 다음, 하던 일을 끝내고 나서 처리한다.
- 계획된 리팩터링과 수시로 하는 리팩터링
: "준비/이해를 위한 리팩터링, 쓰레기 줍기 리팩터링"은 모두 기회가 될 때마다 진행한다. 리팩터링 시간을 일정에 따로 잡아두지 않고, 대부분의 리팩터링을 다른 일을 하는 중에 처리한다.
보기 싫은 코드를 발견하면 리팩터링ㅎ자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐한다.
무언가 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고(단, 만만치 않을 수 있다) 그런 다음에 쉽게 수정하자 (by 켄트 벡)
뛰어난 개발자는 새 기능을 추가하기 쉽도록 코드를 '수정'하는 것이 그 기능을 가장 빠르게 추가하는 길일 수 있음을 안다.
계획된 리팩터링을 하게 되는 일은 최소한으로 줄여야 한다. 리팩터링 작업 대부분은 드러나지 않게, 기회가 될 때마다 해야 한다.
- 오래 걸리는 리팩터링
: 팀 전체가 리팩터링에 매달리는 데는 회의적이다. 그보다는 주어진 문제를 몇 주에 걸쳐 조금씩 해결해가는 편이 효과적일 때가 많다. 누구든지 리팩터링해야할 코드와 관련한 작업을 하게 될 때마다 원하는 방향으로 조금씩 개선하는 식이다. 리팩터링이 코드를 깨트리지 않는다는 장점을 활용하는 것이다. (리팩토링은 테스트 코드를 통해 리팩토링 과정에서도 이전 동작을 보장해야한다)
예컨대 라이브러리를 교체할 때는 기존 것과 새 것 모두를 포용하는 추상 인터페이스부터 마련한다. (추상화로 갈아타기 전략)
- 코드 리뷰에 리팩터링 활용하기
: 코드 리뷰는 개발팀에 지식을 전파하는 데 좋다.
내 눈에는 명확한 코드가 다른 팀원에게는 그렇지 않을 수 있다.
리팩터링은 코드 리뷰의 결과를 더 구체적으로 도출하는 데에도 도움이된다. 개선안들을 제시하는 데서 그치지 않고, 그중 상당수를 즉시 구현해볼 수 있기 때문이다.
가장 좋은 방법은 작성자와 나란히 앉아서 코드를 훑어가면서 리팩터링하는 것이다. 이렇게 하면 자연스럽게 (프로그래밍 과정 안에 지속적인 코드 리뷰가 녹아 있는) pair programming(짝 프로그래밍)이 된다.
- 관리자에게는 뭐라고 말해야 할까?
: 소프트웨어 개발자는 프로다. 프로 개발자의 역할은 효과적인 소프트웨어를 최대한 빨리 만드는 것이다.
새 함수를 추가하려는데 현재 설계가 적합하지 않다면 먼저 리팩터링하고 나서 함수를 추가하는 편이 빠르다.
- 리팩터링하지 말아야 할 때
외부 api가 다루듯 호출해서 쓰는 코드라면 지전분해도 그냥 둔다. 내부 동작을 이해해야 할 시점에 리팩터링해야 효과를 제대로 볼 수 있다.
리팩터링하는 것보다 처음부터 새로 작성하는 게 쉬울 때도 리팩터링하지 않는다. (리팩터링할지 새로 작성하지를 잘 결정하려면 뛰어난 판단력과 경험이 뒷받침돼야 한다.)
2.5 리팩터링 시 고려할 문제
무언가를 언제 어디에 적용할지 판단하려면 손익을 제대로 이해해야한다.
리팩터링에 따려 오는 문제도 엄연히 있기에, 이런 문제가 언제 발생하고 어떻게 대처해야 할지를 반드시 알고 있어야 한다.
- 새 기능 개발 속도 저하
: 리팩터링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것이다.
건강한 코드의 위력을 충분히 경험해보지 않고서는 코드베이스가 건강할 때와 허약할 때의 생산성 차이를 체감하기 어렵다. 코드베이스가 건강하면 기존 코드를 새로운 방식으로 조합하기 쉬워서 복작한 새 기능을 더 빨리 추가할 수 있다.
리팩터링의 본질은 코드베이스를 예쁘게 꾸미는 데 있지 않다. 오로지 경제적인 이유로 하는 것이다. 리팩터링은 개발 기간을 단축하고자 하는 것이다. 기능 추가 시간을 줄이고, 버그 수정 시간을 줄여준다.
- 코드 소유권
: 선호하는 방식은 코드의 소유권을 팀에 두는 것이다. 그래서 팀원이라면 누구나 팀이 소유한 코드를 수정할 수 있게 한다. 이 말은 자신이 맡은 영역의 변경 사항을 관리하라는 뜻이지, 다른 사람이 수정하지 못하게 막으라는 뜻이 아니다.
- 브랜치
: 머지와 통합을 명확히 구분한다. 마스터를 브랜치로 '머지'하는 작업은 단방향이다. 브랜치만 바뀌고 마스터는 그대로다. 반면 '통합'은 마스터를 개인 브랜치로 가져와서 작업한 결과를 다시 마스터에 올리는 양방향 처리를 뜻한다.
최신 버전 관리 시스템은 복잡한 변경 사항을 텍스트 수준에서 머지하는 데는 매우 뛰어나지만, 코드의 의미는 전혀 이해하지 못한다.
머지가 복잡해지는 문제는 기능별 브랜치들이 독립적으로 개발되는 기간이 길어질수록 기하급수적으로 늘어난다. 4주간 작업한 브랜치들을 통합하는 노력은 2주간 작업한 브랜치들을 통합할 때보다 두 배 이상 든다.
TBD(Trunk-Based Development)=CI(Continuous Integreation)
CI에 따르면 모든 팀원이 하루에 최소 한번은 마스터와 통합한다. 이렇게하면 다른 브랜치들과의 차이가 크게 벌어지는 브랜치가 없어져서 머지의 복잡도를 상당히 낮출 수 있다.
하지만, CI를 적용하기 위해서는 치러야할 대가가 있다. 마스터를 건강하게 유지하고, 거대한 기능을 자게 쪼개는 법을 배우고, 각 기능을 끌 수 있는 기능 토클을 적용하여 완료되지 않은 기능이 시스템 전체를 망치지 않도록 해야한다.
머지의 복잡도를 줄일 수 있어서 CI를 선호하기도 하지만, 가장 큰 이유는 리팩터링과 궁합이 아주 좋기 때문이다. 켄트 벡이 CI와 리팩터링을 합쳐서 익스트림 프로그래밍(XP)을 만든 이유도 바로 두 기법의 궁합이 잘 맞기 때문이다.
- 테스팅
: 리팩터링하기 위해서는 (대부분의 경우에) 자가 테스트 코드를 마련해야한다.
테스트가 실패하면 가장 최근에 통과한 버전에서 무엇이 달라졌는지 살펴볼 수 있다는 데 있다.
테스트 주기가 짧다면 단 몇 줄만 비교하면 되며, 문제를 일으킨 부분이 그 몇 줄 안에 있기 때문에 버그를 훨씬 쉽게 찾을 수 있다.
자가 테스트 코드는 통합 과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와도 밀접하게 연관된다. CI에 통합된 테스트는 XP의 권장사항이자 지속적 배포의 핵심이기도 하다.
- 레거시 코드
: 보통은 테스트를 염두에 두고 설계한 시스템만 쉽게 테스트할 수 있다.
'프로그램에서 테스트를 추가할 틈새를 찾아서 시스템을 테스트해야 한다'
이러한 틈새를 만들 때 리팩터링이 활용된다. 테스트 없이 진행하기 때문에 상당히 위험하지만 문제를 해결하기 위해서라면 감내해야 할 위험이다.
레거시 시스템의 규모가 크다면 자주 보는 부분을 더 많이 리팩터링한다. 코드를 훑게 되는 횟수가 많다는 말은 그 부분을 이해하기 쉽게 개선했을 때 얻는 효과도 그만큼 크다는 뜻이니 당연히 이렇게 해야 한다.
- 데이터베이스
: 진화형 데이터베이스 설계(evolutionary database design), 데이터베이스 리팩터링 기법은 현재 널리 적용되고 있다. 이 기법의 핵심은 커다란 변경들을 쉽게 조합하고 다룰 수 있는 ㄷ이터 마이그레이션 스크립트를 작성하고, 접근 코드와 데이터베이스 스키마에 대한 구조적 변경을 이 스크립트로 처리하게끔 통합하는 데 있다.
데이터베이스 리팩터링은 환경에 여러 단계로 나눠서 릴리즈하는 것이 대체로 좋다는 점에서 다른 리팩터링과 다르다. 이렇게 하면 프로덕션 환경에서 문제가 생겼을 때 변경을 되돌리기 쉽다.
2.6 리팩터링, 아키텍처, 애그니(YAGNI)
- 리팩터링이 아키텍처에 미치는 실질적인 효과는 요구사항 변화에 자연스럽게 대응하도록 코드 베이스를 잘 설계해준다는 데 있다.
- 향후 변경에 유연하게 대처할 수 있는 유연성 메커니즘을 소프트웨어 심어두기 => 유연성 메터니즘이 오히려 변화에 대응하는 능력을 떨어뜨릴 때가 대부분
- 리팩터링을 활용하면, 앞으로 어느 부분에 유연성이 필요하고 어떻게해야 그 변화에 가장 잘 대응할 수 있을지 추측하지 않고, 그저 현재까지 파악한 요구사항만을 해결하는 소프트웨어를 구축한다. (딴, 이 요구를 멋지게 해결하도록 설계.) 리팩터링을 미루면 훨씬 힘들어진다는 확신일 들 때만 유연성 메터니즘을 미리 추가한다.
- YAGNI(You Aren't Going to Need It): 간결한 설계, 점진적 설계
- YAGNI는 아키텍처와 설계를 개발 프로세스에 녹이는 또 다른 방식이며, 리팩터링의 뒷바침 없이는 효과를 볼 수 없다.
2.7 리팩터링과 소프트웨어 개발 프로세스
- TDD(Test-Driven Development): 자가 테스트 코드와 리팩토링 묶은 것
- 애자일을 제대로 적용하려면 리팩터링에 대한 팀의 역량과 열정이 뒷받침되어 프로세스 저난에 리팩터링이 자연스럽게 스며들도록 해야 한다.
- 프로그래밍 도중 발생한 오류를 확실히 걸러내는 테스트를 자동으로 수행할 수 있어야 한다.
- 팀으로 개발하려면서 리팩터링을 하려면 각 팀원이 다른 사람의 작업을 방해하지 않으면서 언제든지 리팩터링할 수 있어야 한다. (지속적 통합을 적극 권장하는 이유)
- 자가 테스트 코드, 지속적 통합, 리팩터링이라는 세 기법은 서로 강력한 상승효과를 발휘한다.
- 지속적 배포는 소프트웨어를 언제든 릴리즈할 수 있는 상태로 유지해준다.
- 어떠한 접근법이든지 충분한 연습과 실력이 뒷받침돼야 한다.
2.8 리팩터링과 성능
- 리팩터링하면 소프트웨어가 느려질 수도 있는 건 사실이다. 하지만 그와 동시에 성능을 튜닝하기는 더 쉬워진다.
- hard real-time 시스템을 제외한 소프트웨어를 빠르게 만드는 비결은, 먼저 튜닝하기 쉽게 만들고 나서 원하는 속도가 나게끔 튜닝하는 것이다.
- 성능에 대한 흥미로운 사실은, 대부분 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비한다는 것. 코드 전체를 고르게 최적화한다면 그중 90%는 효과가 거의 없기 때문에 시간 낭비인 셈이다. 즉, 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경 쓰지 않고 코드를 다루기 쉽게 만드는 데 집중한다.
- 성능 최적화 단계가 되면, 먼저 프로파일러로 프로그램을 분석하여 시간과 공간을 많이 잡아먹는 지점을 알아내서 개선한다. 이렇게 하면 성능에 큰 영향을 주는 부분만 집중해서 최적화하기 때문에 적은 노력으로 훨씬 큰 효과를 볼 수 있다.
- 리팩터링은 성능 좋은 소프트웨어를 만드는 데 기여한다. 단기적으로 보면 리팩터링 단계에서는 성능이 느려질 수도 있다. 하지만 최적화 단계에서 코드를 튜닝하기 훨씬 쉬워지기 때문에 결국 더 빠른 소프트웨어를 얻게 된다.
2.9 리팩터링의 유래
- 스몰토크는 객체 지향 언어이기 때문에 인터페이스만 잘 정의해두면 내부 수정이 외부에 미치는 영향을 최소로 줄일 수 있었다.
2.10 리팩터링 자동화
- 자동 리팩터링 기능은 존 브랜트와 돈 로버츠가 개발한 스몰토크용 <리팩터링 브라우저>에서 최초로 등장했다.
- 현재는 에디터나 독립 도구에서도 리팩터링 기능을 제공할 정도로 자동 리팩털이이 흔해졌다.
- IDE는 구문 트리를 분석해서 리팩터링하기 때문에 단순한 텍스트 에디터와는 배교할 수 없을 만큼 유리하다.
- 언어 서버(Language Server): 구문 트리를 구성해서 텍스트 에디터에 API 형태로 제공하는 소프트웨어
2.11 더 알고 싶다면
Chap03. 코드에서 나는 악취
- '적용 방법'을 아는 것과 '제때 적용'할 줄 아는 것은 다르다
- 켄트 백은 리팩터리알 '시점'을 설명하는 데 '냄새(smell)'라는 표현을 사용했다.
- 경험에 따르면, 숙련된 사람의 직관만큼 정확한 기준은 없다. 종료 기준보다는 리팩터링하면 해결할 수 있는 문제의 징후를 제시하겠다.
3.1 기이한 이름
- 코드는 단순하고 명료하게 작성해야 한다.
- 코드를 명료하게 표현하는 데 가장 중요한 요소 하나는 바로 '이름'이다. 그래서 함수, 모듈, 변수, 클래스 등은 그 이름만 보고도 각각이 무슨 일을 하고 어떻게 사용해야 하는지 명확히 알 수 있도록 엄청나게 신경 써서 이름을 지어야한다.
- 마땅한 이름이 떠오르지 않는다면 설계에 더 근본적인 문제가 숨어 있을 가능성이 높다. 그래서 혼란스러운 이름을 잘 정리하다 보면 코드가 훨씬 간결해질 때가 많다.
3.2 중복 코드
- 함수 추출하기
- 문장 슬라이드하기
- 메서드 올리기
3.3 긴 함수
- 경험에 비춰보면 오랜 기간 잘 활용되는 프로그램들은 하나같이 짧은 함수로 구성됐다. 짧은 함수로 구성된 코드베이스를 얼핏 훑으면 연산하는 부분이 하나도 없어 보인다. 코드가 끝없이 위임하는 방식으로 작성되어 있기 때문이다.
- 간접 호출(indirection)의 효과, 즉 코드를 이해하고, 공유하고, 선택하기 쉬워진다는 장점은 함수를 짧게 구성할 때 나오는 것이다.
- 예전 언어는 서브루틴을 호출하는 비용이 컸기 때문에 짧은 함수를 꺼렸다. 하지만 요즘 언어는 프로레스 안에서의 함수 호출 비용을 거의 없애버렸다. 물론 코드를 읽는 사람 입장에서는 함수가 하는 일을 파악하기 위해 왔다 갔다 해야 하므로 여전히 부담이 된다.
- 짧은 함수로 구성된 코드를 이해하기 쉽게 만드는 가장 확실한 방법은 좋은 이름이다. 함수 이름을 잘 지어두면 본문 코드를 볼 이유가 사라진다.
- 주석을 달아야 할 만한 부분은 무조건 함수로 만든다. 그 함수 본문에는 원래 주석으로 설명하려면 코드가 담기고, 함수 이름은 동작 방식이 아닌 "의도"가 드러나게 짓는다.
- 함수 이름에는 코드의 "목적"을 드러내야한다. 여기서 핵심은 함수의 길이가 아닌, 함수의 목적(의도)과 구현 코드의 괴리가 얼마나 큰가다. 즉, "무엇으 ㄹ하는지"를 코드가 잘 설명해주지 못할수록 함수로 만드는게 유리하다.
- 함수를 짧게 만드는 작업의 99%는 함수 추출하기가 차지한다. 함수 본문에서 따로 묶어 빼내면 좋은 코드 덩어리를 찾아 새로운 함수로 만드는 것이다.
- 코드가 단 한줄이어도 따로 설명할 필요가 있다면 함수로 추출하는 게 좋다.
3.4 긴 매개변수 목록
- 매개변수를 질의 함수로 바꾸기
- 사용 중인 데이터 구조에서 값들을 뽑아 각각을 별개의 매개변수로 전달하는 코드라면 "객체 통째로 넘기기"를 적용해서 원본 데이터 구조를 그대로 전달한다. 항상 함께 전달되는 매개변수들은 "매개변수 객체만들기"로 하나로 묶어버린다. 함수의 동작 방식을 정하는 플래그 역할의 매개변수는 "플래그 인수 제거하기"로 없애준다.
3.5 전역 데이터
- 전역 데이터는 코드베이스 어디에서든 건드릴 수 있고 값을 누가 바꿨는지 찾아낼 메커니즘이 없다는 게 문제다.
- 변수 캐슐화하기. 다른 코드에서 오염시킬 가능성이 있는 데이터를 발견할 때마다 이 기법을 가장 먼저 적용한다.
3.6 가변 데이터 (Mutable Data)
- 코드와 다른 곳에서는 다른 값을 기대한다는 사실을 인식하지 못한 채 수정해버리면 프로그램이 오작동한다. 이런 이유로 함수형 프로그래밍에서는 데이터는 절대 변하지 않고, 데이터를 변경하려면 반드시 (원래 데이터는 그대로 둔 채) 변경하려는 값에 해당하는 복사본을 만들어서 변환한다는 개념을 기본으로 삼고 있다.
- 값을 다른 곳에서 설정할 수 있는 가변 데이터가 풍기는 악취는 특히 고약한다. 이럴 때는 "파생 변수를 질의 함수로 바꾸기"를!
3.7 뒤엉킨 변경
- 단일 책임 원칙(SRP: Single Responsibility Principle)이 제대로 지켜지지 않을 때 나타난다.
3.8 산탄총 수술
- 산탄총 수술은 뒤엉킨 변경과 비슷하면서도 정반대다. 두 냄새의 밑바닥에 깔린 원인과 해법의 원리는 비슷하다. 하지만 발생과정이 정 반대여서 겉으로 드러나는 현상과 이를 해결하기위한 방법은 사뭇다르다.
- 이 냄새는 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 풍긴다.
3.9 기능 편애
- 프로그램을 모듈활할 때는 코드를 여러 영역으로 나눈 뒤 영역 안에서 이뤄지는 상호작용은 최대한 늘리고 영역 사이에서 이뤄지는 상호작용은 최소로 줄이는 데 주력한다.
- 기능 편애는 흔히 어떤 함수가 자기 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용할 일이 더 많을 때 풍기는 냄새다.
- 함께 변경할 대상을 한데 모으기!
3.10 데이터 뭉치
3.11 기본형 집착
3.12 반복되는 switch문
- 중복된 switch문이 문제가 되는 이유는 조건절을 하나 추가할 때마다 다른 switch 문들도 모두 찾아서 함께 수정해야 하기 때문이다. 이럴 때 다형성은 반복된 switch문이 내뿜는 사악한 기운을 제압하여 코드베이스를 최신 스타일로 바꿔주는 세련된 무기인 셈이다.
3.13 반복문
- 지금은 일급함수(first-class function)를 지원하는 언어가 많아졌기 때문에 "반복문을 파이프라인으로 바꾸기"를 적용해서 시대에 걸맞지 않은 반복문을 제거할 수 있게됐다.
- filter나 map같은 파이프라인 연산을 사용하면 코드에서 각 원소들이 어떻게 처리되는지 쉽게 파악할 수 있다.
3.14 성의 없는 요소
3.15 추측성 일반화
- '나중에 필요할 거야'라는 생각으로 당장은 필요 없는 모든 종류의 후킹(hooking) 포인트와 특이 케이스 처리 로직을 작성해둔 코드에서 풍킨다.
- 미래를 대비해 작성한 부분을 실제로 사용하게 되면 다행이지만, 그렇지 않는다면 쓸데없는 낭비일 뿐이다. 당장 걸리적거리는 코드는 눈앞에서 치워버리자.
- 추측성 일반화는 테스트 코드 말고는 사용하는 곳이 없는 함수나 클래스에서 흔히 볼 수 있다. 이런 코드를 발견하면 테스트 케이스부터 삭제한 뒤에 "죽은 코드 제거하기"로 날려버리자.
3.16 임시 필드
3.17 메시지 체인
- 메시지 체인: 클라이언트가 한 객체를 통해 다른 객체를 얻은 뒤 방금 얻은 객체에 또 다른 객체를 요청하는 식으로, 다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말함
3.18 중개자
- 객체의 대표적인 기능 하나로, 외부로부터 세부사항을 숨겨주는 캡슐화가 있다. 캡슐화하는 과정에서 위임(delegation)이 자주 활용된다.
- 클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임하고 있다면 어떤가? 이럴 때는 "중재자 제거하기"를 활용하여 실제로 일을 하는 객체와 직접 소통하자.
3.19 내부자 거래
3.20 거대한 클래스
3.21 서로 다른 인터페이스의 대안 클래스들
3.22 데이터 클래스
- 변경하면 안되는 필드는 "세터 제거하기"로 접근을 원천 봉쇄한다.
3.23 상속 포기
- 상속 포기 냄새는 서브클래스가 부모의 동작은 필요로하지만 인터페이스는 따르고 싶지 않을 때 특히 심하게 난다. 구현을 따르지 않는 것은 이해할 수 있지만 인터페이스를 따르지 않는다는 것은 상당히 무례한 태도다.
3.24 주석
- 주석은 악취가 아닌 향기를 입힌다. 문제는 주석을 탈취제처럼 사용하는 데 있다. 주석이 장황하게 달린 원인이 코드를 잘못 작성했기 때문인 경우가 의외로 많다.
- 주석을 남겨야겠다는 생각이 들면, 가장 먼저 주석이 필요 없는 코드로 리팩터링해본다.
- 뭘 할지 모를 때라면 주석을 달아두면 좋다. 현재 진행 상황뿐만 아니라 확실하지 않은 부분에 주석에 남긴다. 코드를 지금처럼 작성한 이유를 설명하는 용도로 달 수도 있다.
Chap04. 테스트 구축하기
- 리팩터링을 제대로 하려면 불가피하게 저지르는 실수를 잡아주는 견고한 테스트 스위트가 뒷받침돼야 한다.
- 리팩터링을 하지 않더라도 좋은 테스트를 작성하는 일은 개발 효율을 높여준다.
4.1 자가 테스트 코드의 가치
- 버그 수정 자체는 대체로 금방 끝난다. 진짜 끔찍한 건 버그를 찾는 여정이다. 또한 버그를 잡는 과정에서 다른 버그를 심기도 하는데, 그 사실을 함참이 지나서야 알아채기도 한다. 그래서 또다시 그 버그를 찾느라 수많은 시간을 날린다.
- 모든 테스트를 완전히 자동화하고 그 결과까지 스스로 검사하게 만들자.
- 자가 테스트 코드 자체뿐 아니라 테스트를 자주 수행하는 습관도 버그를 찾는 강력한 도구가 된다.
- 회귀 테스트(regression test): 잘 작동하던 기능이 여전히 잘 작동하는지 확인하는 테스트
- 테스트 스위트는 강력한 버그 검출 도구로, 버그를 찾는 데 걸리는 시간을 대폭 줄여준다.
- 테스트를 작성하기 가장 좋은 시점은 프로그래밍을 시작하기 전이다. 기능을 추가해야할 때 테스트부터 작성한다. 얼핏 순서가 뒤바뀐 듯 들리지만, 전혀 그렇지 않다. 테스트를 작성하다 보면 원하는 기능을 추가하기 위해 무엇이 필요한지 고민하게 된다. 구현보다 인터페이스에 집중하게 된다는 장점도 있다(무조건 좋은 일이다). 게다가 코딩이 완료되는 시점을 정확하게 판단할 수 있다. 테스트를 모두 통과한 시점이 바로 코드를 완성한 시점이다.
- 켄트 백은 이처럼 테스트부터 작성하는 습관을 바탕으로 테스트 주도 개발 (TDD, Test-Driven Development)이란 기법을 창시했다. TDD에서는 (처음에는 통과하지 못할) 테스트를 작성하고, 이 테스트를 통과하게끔 코드를 작성하고, 결과 코드를 최대한 깔끔하게 리팩터링한느 과정을 짧은 주기로 반복한다.
- 간혹 테스트가 갖춰지지 않은 코드를 리팩터링해야 할 때도 있다. 그럴 때는 곧바로 리팩털이하지 않고, 먼저 자가 테스트 코드부터 작성한다.
4.2 테스트할 샘플 코드
4.3 첫 번째 테스트
- 자바스크립트 테스트 프레임워크들 중, 모카(mocha)를 사용!
- 실패해야 할 상황에는 반드시 실패하게 만들자.
- 특히 수많은 테스트를 실행했음에도 실패하는 게 없다면 테스트가 내 의도와는 다른 방식으로 코드를 다루는 건 아닌지 불안해진다. 그래서 각각의 테스트가 실패하는 모습을 최소한 한 번씩은 직접 확인해본다.
- 자주 테스트하라. 작성 중인 테스트 코드는 최소 몇 분 간격으로 테스트하고, 적어도 하루에 한 번은 전체 테스트를 돌려보자.
- 간결한 피드백은 자가 테스트에서 매우 중요하다. 핵심은 모든 테스트가 통과했다는 사실을 빨리 알 수 있다는 데 있다.
4.4 테스트 추가하기
- 명심하자! 테스트는 "위험 요인"을 중심으로 작성해야 한다!
- 테스트의 목적은 어디까지나 현재 혹은 향후에 발생하는 버그를 찾는 데 있다.
- 따라서 단순히 필드를 읽고 쓰기만 하는 접근자는 테스트할 필요가 없다. 이런 코드는 너무 단순해서 버그가 숨어들 가능성도 별로 없다.
- 잘못될까봐 가장 걱정하는 영역을 집중적으로 테스트하는데, 이렇게 해서 쏟는 노력의 효과를 극대화하는 것이다.
- 완벽하게 만드느라 테스트를 수행하지 못하느니, 불완전한 테스트라도 작성해 실행하는 게 낫다.
- 일반 코드와 마찬가지로 테스트 코드에서도 중복은 의심해봐야 한다.
- 공유픽스처는 사용하지말자! beforeEach 구문은 각각의 테스트 바로 전에 실행되어 asia를 초기화하기 때문에 모든 테스트가 자신만의 새로운 asia를 사용하게 된다. 이처럼 개별 테스트를 실행할 때마다 픽스처를 새로 만들면 모든 테스트를 독립적으로 구성할 수 있어서, 결과를 예측할 수 없어 골치를 썩는 사태를 예방할 수 있다.
4.5 픽스처 수정하기
4.6 경계 조건 검사하기
- 모든 일이 순조롭고 사용자도 우리 의도대로 사용하는 꽃길(happy path) 상황이 아닐때는 고려하기!
- producers와 같은 컬렉션과 마주하면 그 컬렉션이 비었을 때 어떤 일이 일어나는 지 확인하는 편이다.
- 숫자형이라면 0일 때를 검사해본다.
- 수요가 음수일 때 수익이 음수가 나온다는 것이 이 프로그램을 사용하는 고객 관점에서 말이 되는 소리일까? 음수라면 에러를 던지거나 무조건 0으로 설정하는 식으로 정상적인 경우와 다르게 처리해야 하지 않을까? 이처럼 경계를 확인하는 테스트를 작성해보면 프로그램에서 이런 특이 사황을 어떻게 처리하는 게 좋을 지 생각해볼 수 있다.
- 문제가 생길 가능성이 있는 경계 조건을 생각해보고 그 부분을 집중적으로 테스트하자.
- 테스트 코드를 작성할 때, 의식적으로 프로그램을 망가뜨리는 방법을 모색하는데, 이런 마음 자세가 생산성과 재미를 끌어올려준다.
- 실패(failure)란 검증 단계에서 실제 값이 예상 범위를 벗어났다는 뜻. 에러는 성격이 다르다.
- 어차피 모든 버그를 잡아낼 수는 없다고 생각하여 테스트를 작성하지 않는다면 대다수의 버그를 잡을 수 있는 기회를 날리는 셈이다.
- 위험한 부분에 집중하라!
4.7 끝나지 않은 여정
- 예전에는 테스트를 별도의 (그리고 실력이 좀 떨어지는) 조직에 맡겼지만, 이제는 뛰어난 소프트웨어 개발자라면 최우선으로 관심을 가지는 주제로 떠오르고 있다.
- 단위 테스트(unit test)란 코드의 작은 영역만을 대상으로 빠르게 실행되도록 설계된 테스트다.
- 버그를 발견하는 즉시 발견한 버그를 명확히 잡아내는 테스트부터 작성하는 습관을 들이자. 아주 중요한 습관이다!
- 버그 리포트를 받으면 가장 먼저 그 버그를 드러내는 단위 테스트부터 작성하자.
- 테스트 커버리지 분석은 코드에서 테스트하지 않은 영역을 찾는 데 도움될 뿐, 테스트 스위트의 품질과는 크게 상관없다.
- 리팩터링 후 테스트 결과가 모두 초록색인 것만 보고도 리팩터링 과정에서 생겨난 버그가 하나도 없다고 확신할 수 있다면 충분히 좋은 테스트 스위트라 할 수 있다.
Chap05. 리팩터링 카탈로그 보는 법
5.1 리팩터링 설명 형식
- 이름, 개요, 배경, 절차, 예시
- 리팩터링을 안전하게 수행하려면 단계를 최대한 잘게 나누고 각 단계마다 테스트해야 한다.
- 상황이 난해할수록 단계를 잘게 나누자.
5.2 리팩터링 기법 선정 기준
Chap06. 기본적인 리팩터링
6.1 함수 추출하기
- 코드 조각을 찾아 무슨 일을 하는지 파악한 다음, 독립된 함수로 추출하고 목적에 맞는 이름을 붙인다.
- 목적와 구현을 분리
- 코드를 보고 무슨 일을 하는지 파악하는 데 한참이 걸린다면 그 부분을 함수로 추출한 뒤 '무슨 일'에 걸맞는 이름을 짓는다. ('어떻게'가 아닌 '무엇을'하는지가 드러나야 한다)
- 짧은 함수의 이점은 이름을 잘 지어야만 발휘되므로 이름 짓기에 특별히 신경 써야 한다.
6.2 함수 인라인하기
6.3 변수 추출하기
6.4 변수 인라인하기
6.5 함수 선언 바꾸기
6.6 변수 캡슐화하기
6.7 변수 이름 바꾸기
6.8 매개변수 객체 맏늘기
6.9 여러 함수를 클래스로 묶기
6.10 여러 함수를 변환 함수로 묶기
6.11 단계 쪼개기
Chap07. 캡슐화
Chap08. 기능 이동
Chap09. 데이터 조직화
Chap10. 조건부 로직 간소화
Chap11. API 리팩터링
Chap12. 상속 다루기
Reference