명사로서 리팩터링
동사로서 리팩터링
※ 겉보기 동작을 유지한다 : 리팩터링하기 전과 후의 코드가 똑같이 동작해야한다는 의미, 심지어 리팩터링 과정에서 발견되는 버그도 리팩터링 후에 그대로 남아 있어야 한다. (단, 아무도 발견하지 못한 숨은 버그는 수정해도 괜찮다.)
앞에서 제시한 정의를 따르면 특정한 방식에 따라 코드를 정리하는 것만이 리팩터링이다. 동작을 보존하는 작은 단계들을 거쳐 코드를 수정하고, 이러한 단계들을 순차적으로 연결하여 큰 변화를 만들어내는 일이다.
개별 리팩터링은 그 자체로 아주 작을 수도 있고, 작은 단계 여러 개가 합쳐진 모습일 수도 있다.
리팩터링하는 동안에는 코드가 항상 정상 작동하기 때문에 전체 작업이 끝나지 않았더라도 언제든 멈출 수 있다.
코드베이스를 정리하거나 구조를 바꾸는 모든 작업을 재구성(restructuring) 이라는 포괄적인 용어로 표현하고, 리팩터링은 재구성 중 특수한 한 형태로 볼 수 있다.
한 번에 바꿀 수 있는 작업을 수많은 단계로 잘게 나눔으로써 체계적으로 리팩터링을 진행할 수 있다.
성능 최적화와 리팩터링
소프트웨어를 개발할 때 목적이 '기능 추가'냐 '리팩터링' 이냐를 명확히 구분해 작업해야한다. (켄트 벡은 이를 두개의 모자에 비유했다)
기능을 추가할 때는 '기능 추가' 모자를 쓴 다음 기존 코드는 절대 건드리지 않고 새 기능을 추가하기만 한다.
'리팩터링'모자를 쓴 다음 기능 추가는 절대 하지 않기로 다짐한 뒤 오로지 코드 재구성에만 전념한다.
항상 내가 쓰고 있는 모자가 무엇인지 그에 따른 미묘한 작업 방식의 차이를 분명하게 인식해야 한다.
리팩터링하면 소프트웨어 설계가 좋아진다.
리팩터링하면 소프트웨어를 이해하기 쉬워진다.
리팩터링하면 버그를 쉽게 찾을 수 있다.
리팩터링하면 프로그래밍 속도를 높일 수 있다.
3의 법칙
1. 처음에는 그냥 한다.
2. 비슷한 일을 두 번째로 하게 되면(중복이 생겼다는 사실에 당황스럽겠지만), 일단 계속 진행한다.
3. 비슷한 일을 세 번째 하게 되면 리팩터링한다.
준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기
이해를 위한 리팩터링(Comprehension Refactoring): 코드를 이해하기 쉽게 만들기
쓰레기 줍기 리팩터링
계획된 리팩터링과 수시로 하는 리팩터링
개발에 들어가기 전에 리팩터링 일정을 따로 잡아두지 않고, 기능을 추가하거나 버그를 잡는 동안 리팩터링도 함께 한다.
보기 싫은 코드를 발견하면 리팩터링하자. 잘 작성된 코드라도 수많은 리팩터링을 거쳐야 한다.
코드를 작성할 때 처음 계획했던 것과 다르게 적절히 타협하며 작성할 때가 있다. 어제는 적합했던 기준이 오늘 하는 다른 작업에는 맞지 않을 수 있다. 이렇게 상황이 변해 기준을 변경해야 할 때 코드가 이미 깔끔하다면 리팩터링하기 쉽다.
무언가 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고, 그런 다음 쉽게 수정하자
소프트웨어 개발을 끝이 있는 작업으로 보면 안된다. 새 기능이 필요할 때마다 소프트웨어는 이를 반영하기 위해 수정된다. 이 때 새로 작성해 넣는 코드보다 기존 코드의 수정량이 큰 경우가 대체로 많다.
계획된 리팩터링이 무조건 나쁜건 아니지만, 한번에 큰 작업이 되는 계획된 리팩터링은 최소한으로 줄이고 기회가 될 때마다 해야한다.
버전 관리 시스템에서 리팩터링 커밋과 기능 추가 커밋을 분리하는 경우가 많은데, 리팩터링은 기능 추가와 밀접하게 엮인 경우가 많기 때문에 굳이 나누는 것은 시간 낭비 일 수 있다. 리팩터링 커밋을 분리한다고 해서 무조건 좋은 것은 아님을 명심하고, 팀에 적합한 방식을 실험을 통해 찾아내야 한다.
오래 걸리는 리팩터링
코드 리뷰에 리팩터링 활용하기
리팩터링하지 말아야 할 때
새 기능 개발 속도 저하
리팩터링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것이다.
코드 소유권
코드 소유권이 나뉘어 있으면 리팩터링에 방해가 된다. 클라이언트에 영향을 주지 않고서는 원하는 형태로 변경할 수 없기 때문이다.
코드의 소유권이 작은 단위로 나눠져 있다면 관리가 복잡해진다. 코드 소유권에 대한 한가지 전략은 코드의 소유권을 팀에 두는 것이다.
대규모 시스템 개발 시 다른 팀 사람이 자기 팀 코드의 브랜치를 따서 수정하고 커밋을 요청하는 오픈소스 개발 모델과 유사한 전략을 권장하기도 한다. 이렇게 하면 함수의 클라이언트도 바꿀 수 있다.
브랜치
현재 흔히 볼 수 있는 팀 단위 작업방식은 버전 관리 시스템을 사용하여 팀원마다 코드 베이스의 브랜치를 하나씩 맡아서 작업하다가 결과물이 어느정도 쌓이면 마스터 브랜치에 통합해서 다른 팀원과 공유하는 것이다.
문제를 해결하기 위해 기능별 브랜치의 통합 주기를 매우 짧게 가져가야 한다. 이 방식을 CI(Continuous Integration) 또는 TBD(Trunk-Based Development)라고 한다.
CI에 따르면 모든 팀원이 하루에 최소 한 번은 마스터와 통합한다.
CI는 리팩터링과 궁합이 좋다.
CI를 완벽히 적용하지는 못하더라도 통합 주기만큼은 최대한 짧게 잡아야 한다.
테스팅
리팩터링 실수로 동작을 깨트린다면 오류를 빨리 잡아야 한다.
자가 테스트 코드는 통합 과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와도 밀접하게 연관된다.
레거시 코드
레거시 시스템을 파악할 때 리팩터링이 큰 도움이 된다. 레거시 시스템에 잘 갖춰진 테스트 코드가 존재한다면 제 기능과 맞지 않은 함수 이름을 바로 잡고 어설픈 프로그램 구문을 매끄럽게 다듬어서 프로그램을 개선할 수 있지만, 레거시 코드에는 테스트가 없는 경우가 많다. 대규모 레거시 시스템을 테스트 코드 없이 리팩터링하기는 어렵다.
쉽게 해결할 수는 없지만 <레거시 코드 활용 전략> 이라는 책의 지침을 충실히 따르는 것이다.
이런 문제들을 만들지 않기 위해 처음부터 자가 테스트 코드를 작성해야 한다.
테스트를 갖추고 있더라도 복잡한 레거시 코드를 단번에 리팩터링하기는 어려우므로 서로 관련된 부분끼리 나눠서 하나씩 공략해야 한다.
데이터베이스
데이터베이스도 리팩터링을 하기 위한 기법이 존재한다 ( <진화형 데이터베이스 설계> <데이터베이스 리팩터링> )
이 기법들의 핵심은 커다란 변경들을 쉽게 조합하고 다룰 수 있는 데이터 마이그레이션 스크립트를 작성하고, 접근 코드와 데이터베이스 스키마에 대한 구조적 변경을 이 스크립트로 처리하게끔 통합하는 것이다.
ex) 필드의 이름을 변경하는 경우, 데이터 구조의 원래 선언과 이 데이터 구조를 호출하는 코드를 모두 찾아서 한 번에 변경해야한다.
다른 리팩터링과 마찬가지로 이 기법도 전체 변경 과정을 작고 독립된 단계들로 쪼개야 한다.
데이터베이스 리팩터링은 프로덕션 환경에서 여러 단계로 나눠서 릴리스하는 것이 대체로 좋다는 점에서 다른 리팩터링과 다르다.
리팩터링이 아키텍처에 미치는 실질적인 효과는 요구사항 변화에 자연스럽게 대응하도록 코드베이스를 잘 설계해준다는 데 있다.
코딩 전에 아키텍처를 확정지으려 할 때의 대표적인 문제는 소프트웨어 요구사항을 사전에 모두 파악해야 한다는 것이다. 막상 해보면 실현할 수 없는 목표일 때가 많다.
리팩터링을 활용하면 그저 현재까지 파악한 요구 사항만을 해결하는 소프트웨어를 구축한다. 단 이 요구를 멋지게 해결하도록 설계한다. 진행하면서 사용자의 요구사항을 더 잘 이해하게 되면 아키텍처도 그에 맞게 리팩터링해서 바꾼다. 그 과정에서 소프트웨어의 복잡도에 지장을 주지 않는 메커니즘은 마음껏 추가하지만, 복잡도를 높을 수 있는 유연성 메커니즘(flexibility mechanism)은 반드시 검증을 거친 후에 추가한다.
예상되는 변경을 미리 반영하는 리팩터링을 미루면 나중에 얼마나 어려워질지 가늠해보면 판단에 도움될 때가 많다. 리팩터링을 미루면 훨씬 힘들어진다는 확신이 들 때만 유연성 메커니즘을 미리 추가한다.
※ 유연성 메커니즘 : 예를 들어 함수를 정의할 때 범용적으로 사용될 것을 예상하여 다양한 시나리오에 대응하기 위해 매개변수를 미리 추가하는 등의 메커니즘. 확장을 생각해서 구현을 하다보면 현재의 쓰임새에 비해 함수가 너무 복잡해지고 추 후 수정하기 더 어려워지는 문제가 있을 수 있다.
이런 식으로 설계하는 방식을 간결한 설계(simple design), 점진적 설계(incremental design), YAGNI(You aren't going to need it(필요 없을 거다)) 등으로 부른다.
YAGNI를 받아들인다고 해서 선제적인 아키텍처에 완전히 소홀해도 된다는 뜻이 아니다. 다만 나중에 문제를 더 깊이 이해하게 됐을 때 처리하는 쪽이 훨씬 낫다.
XP의 두드러진 특징은 지속적 통합, 자가 테스트 코드, 리팩터링 등 개성이 강하면서 상호 의존하는 기법들을 하나로 묶은 프로세스라는 점이다. (참고로 자가 테스트 코드와 리팩터링을 묶어 TDD라 한다)
애자일을 제대로 적용하려면 리팩터링에 대한 팀의 역량과 열정이 뒷받침 되어 프로세스 전반에 리팩터링이 자연스럽게 스며들도록 해야 한다.
리팩터링의 첫 번째 토대는 자가 테스트 코드이다. 프로그래밍 도중 발생한 오류를 확실히 걸러내는 테스트를 자동으로 수행할 수 있어야 한다.
팀으로 개발하면서 리팩터링을 하려면 각 팀원이 다른 사람의 작업을 방해하지 않으면서 언제든지 리팩터링할 수 있어야 한다. 지속적 통합(CI)를 적극 권장하는 이유가 바로 이 때문이다.
세 실천법(자가 테스트 코드, CI, 리팩터링)을 적용한다면 YAGNI 설계 방식으로 개발을 진행할 수 있다.
핵심 실천법을 갖췄다면 애자일의 다른 요소가 주는 이점까지 취할 수 있는 토대를 마련할 수 있다.
실제로 리팩터링을 하다보면 코드를 이해하기 쉽게 만들기 위해 속도가 느려지는 방향으로 수정하는 경우가 많다. 리팩터링을 하면 소프트웨어가 느려질 수 있는 건 사실이다. 하지만 그와 동시에 성능을 튜닝하기는 더 쉬워진다.
하드 리얼타임 시스템을 제외한 소프트웨어를 빠르게 만드는 비결은, 먼저 튜닝하기 쉽게 만들고 나서 원하는 속도가 나게끔 튜닝하는 것이다.
빠른 소프트웨어를 작성하는 세 가지
1) 시간 예산 분배 (time budgeting) 방식
2) 끊임없이 관심을 기울이는 것
3) 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경쓰지 않고 코드를 다루기 쉽게 만드는 것
프로그램을 잘 리팩터링해두면 최적화에 두 가지 면에서 도움이 된다.
1) 리팩터링이 잘 되어 있다면 기능 추가가 빨리 끝나서 성능에 집중할 시간을 더 벌 수있다.
2) 리팩터링이 잘 되어 있는 프로그램을 성능을 더 세밀하게 분석할 수 있다.
단기적으로보면 리팩터링 단계에서는 성능이 느려질 수 있지만, 최적화 단계에서 코드를 튜닝하기 훨씬 쉬워지기 때문에 결국 더 빠른 소프트웨어를 얻게 된다.