on
리팩토링이란? 리팩토링 원칙에 대하여
리팩토링의 정의
리팩토링[명사] : 소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법
리팩토링(하다)[동사] : 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러 가지 리팩토링 기법을 적용해서 소프트웨어를 재구성하다
재구성(restructuring) : 코드베이스를 정리하거나 구조를 바꾸는 모든 작업
겉보기동작(observable behavior) : 리팩토링 하기 전과 후의 코드가 똑같이 동작하는 것
리팩토링은 코드를 변경하지만 프로그램의 전반적인 기능은 그대로 유지한다는 점이 성능 최적화와 비슷하다.
리팩토링의 목적은 코드를 이해하고 수정하기 쉽게 만드는 것이기 때문에 프로그램의 성능은 좋아질수도, 나빠질 수도 있다.
하지만 성능최적화는 오로지 속도 개선에만 신경 쓰기때문에 더 다루기 어려운 코드로 바뀔 수 있다.
리팩토링하는 이유
리팩토링하면 소프트웨어 설계가 좋아진다.
내부설계(아키텍쳐)를 제대로 파악하지 못한 채 단기 목표만을 위해 코드를 수정하다 보면 기반 구조가 쉽게 무너지고 악효과가 누적된다.
설계를 파악하기 어려워질수록 설계를 유지하기 힘들어지고 설계가 무너지는 속도는 빨라진다.
설계가 나쁘면 중복 코드가 많아지고 코드가 길어지기 쉽다.
중복 코드를 제거하면 모든 코드가 언제나 고유한 일을 수행함을 보장할 수 있고 규칙적인 리팩토링은 코드의 구조를 지탱해준다.
리팩토링하면 소프트웨어를 이해하기 쉬워진다.
프로그래밍은 컴퓨터에게 일을 시킬 코드를 만드는 것이지만
사람이 읽고 수정하고 작업하는 것이기 때문에 사람이 보고 파악하기 쉽게 만들어야 한다.
제대로 이해하면 한시간이면 가능할 코드가 파악하기 힘든 코드라면 일주일이 걸릴 수도 있다.
그 사람은 다른 개발자가 될 수도 있고 나중의 내가 될 수도 있다.
리팩토링은 코드가 더 잘 읽히게 도와준다. 코드의 목적이 더 잘 드러나게, 내 의도를 더 명확하게 전달하도록 개선할 수 있다.
리팩토링하면 버그를 쉽게 찾을 수 있다.
코드를 이해하기 쉽다는 말은 버그를 쉽게 찾을 수 있다는 말이기도 하다.
리팩토링을 하면 코드가 하는 일을 깊이 파악하게 되면서 새로 깨달은 것을 곧바로 코드에 반영하게 된다. 프로그램의 구조를 다듬으면 막연히 ‘이럴 것이다~’ 하는 점들이 분명히 드러나고 버그를 지나칠 수 없을 만큼 명확해진다.
리팩토링하면 프로그래밍 속도를 높일 수 있다.
내부설계가 잘 된 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있다.
모듈화가 잘 되어 있으면 전체 코드베이스 중 작은 일부만 이해하면 된다. 코드가 명확하면 버그를 만들 가능성도 줄고, 버그를 만들더라도 디버깅하기가 쉽다.
내부 품질이 뛰어난 코드베이스는 새 기능 구축을 돕는 견도한 토대가 된다.
리팩토링에 시간을 투자하면서 오히려 코드 개발 속도가 늘어나는 것이 아닌가 하지만 내부 설계가 튼튼할수록 프로그램의 지구력이 높아진다.
리팩토링을 하면 기존 코드의 설계를 얼마든지 개선할 수 있기때문에 프로그램 요구사항이 바뀌더라도 설계를 지속해서 개선할 수 있다.
언제 리팩토링 해야할까? 리팩토링의 시기
3의 법칙 - 돈 로버츠 Don Roberts
1. 처음에는 그냥 한다.
2. 비슷한 일을 두 번째로 하게 되면 일단 계속 진행한다.
3. 비슷한 일을 세 번째 하게 되면 리팩터링 한다.
준비를 위한 리팩토링 : 기능을 쉽게 추가하게 만들기
리팩토링 하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다.
현재 코드를 살펴보면서, 구조를 살짝 바꾸면 다른 작업을 하기가 훨씬 쉬워질 만한 부분을 찾는다.
버그를 찾을 때도 마찬가지이다. 오류를 일으키는 코드가 세곳에 복제되어 있다면 우선 한곳으로 모아 놓으면 수정하기가 쉽다.
이해를 위한 리팩토링 : 코드를 이해하기 쉽게 만들기
코드를 수정하려면 먼저 그 코드가 하는 일을 파악해야 한다.
그 코드의 의도가 더 명확하게 드러나도록 할 여지는 없는 지, 조건부 조직의 구조가 이상하지 않은 지, 함수 이름을 잘 못 정해서 실제로 하는 일을 파악하는 데 시간이 오래 걸리지는 않는 지도 살펴본다.
그러면 코드가 깔끔해지고 이전에 보이지 않던 설계가 눈에 들어오기 시작한다.
쓰레기 줍기 리팩토링
코드를 파악하던 중에 로직이 쓸데없이 복잡하거나, 매개변수화한 함수 하나면 될 일을 거의 똑같은 함수 여러 개로 작성해놓는 등 일을 비효율적으로 처리하는 모습을 발견할 때가 있다.
이때 간단히 수정할 수 있는 것은 즉시 고치고, 시간이 좀 걸리는 일은 짧은 메모만 남기고, 하던 일을 끝내고 나서 처리하는 것을 쓰레기 줍기 리팩토링이라고 한다.
코드리뷰에 리팩토링 활용하기
코드 리뷰는 개발팀 전체에 지식을 전파하는 데 좋다. 경험이 더 많은 개발자의 노하우를 더 적은 개발자에게 전수할 수 있고, 대규모 소프트웨어 시스템의 다양한 측면을 더 많은 사람이 이해하는 데 도움이 되고 , 깔끔한 코드를 작성하는 데에도 굉장히 중요하다.
리팩토링은 코드 리뷰의 결과를 더 구체적으로 도출하는 데에 도움이 된다. 개선안들 중 상당수를 즉시 구현해볼 수 있기 때문이다.
코드의 작성자와 나란히 앉아서 코드를 훑어가면서 리팩토링하게 되면 자연스럽게 (프로그래밍 과정안에 지속적인 코드 리뷰가 녹아있는) 짝 프로그래밍이 된다.
리팩토링 하지 말아야 할 때
- 기술을 모르는, 코드 베이스의 건강 상태가 생산성에 미치는 영향을 모르는 관리자와 고객이 있는 상황에서는 하지말자. 리팩터링은 누적된 오류를 잡는 일이거나, 가치 있는 기능을 만들어 내지 못하는 작업이라는 오해를 가지고 있는 조직도 있다.
- 외부 api 다루듯이 호출해서 쓰는 코드 같은 지저분한 코드여도 수정할 필요가 없는 코드라면 하지 않는다. 리팩토링은 내부 동작을 이해해야 할 시점에서 해야 효과를 볼 수 있다.
- 리팩토링 하는 것 보다 새로 작성하는 게 쉬울 때는 하지 않는다. 이건 판단하기 어렵다.
리팩토링 시 고려할 문제
새 기능 개발 속도 저하
리팩토링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것.
코드베이스가 건강하면 기존 코드를 새로운 방식으로 조합하기 쉬워서 복잡한 새 기능을 더 빨리 추가 할 수 있다. 지금 당장 리팩토링이 개발 속도를 저하시키는 시간낭비로 보일 수 있으나 궁극적으로 개발 속도가 빨라진다.
리팩터링의 본질은 코드를 이쁘게 꾸미는 데 있지 않다. 경제적인 이유로 하는 것이다.
기능 추가 시간을 줄이고, 버그 수정 시간을 줄여주고 개발 기간을 단축시켜준다. 리팩토링하도록 이끄는 동력은 어디까지나 경제적인 효과에 있다. 이를 명확히 이해해야 소프트웨어 개발 진행 그래프에서 ‘좋은 설계’ 곡선을 더 많이 볼 수 있다.
코드 소유권
리팩토링 하다 보면 모듈의 내부뿐 아니라 시스템의 다른 부분과 연동하는 방식에도 영향을 주는 경우가 많다. 함수를 호출하는 코드의 소유자가 다른 팀이라서 나에게 쓰기 권한이 없을 수도 있다.
또는 고객에게 api로 제공된 것이라면 얼마나 사용되고 있는지, 쓰이고 있는 지도 파악이 안될 수 있다. 이런 경우 코드가 복잡해지지만 기존 함수를 유지하되 함수 본문에서 새 함수를 호출하도록 할 수 있다.
그래서 코드 소유권을 팀에 두고 팀원 모두가 팀이 소유한 코드를 수정할 수 있는 방식을 선호한다.
흡사 오픈소스 개발 모델을 권장한다. 이렇게 하면 함수의 클라이언트도 바꿀 수 있다. 이런 방식은 코드 소유권을 엄격하게 통제하는 방식과 완전히 풀어서 변경을 통제하기 어려운 방식의 절충안으로 대규모 시스템 개발 시 잘 어울린다.
브랜치
현재 흔히 볼 수 있는 팀 단위 작업 방식은 버전 관리 시스템을 사용해서 팀원마다 코드베이스의 브랜치를 하나씩 맡아서 작업하다가, 결과물이 어느정도 쌓이면 마스터 브랜치에 통합해서 다른 팀원들과 공유하는 것이다.
이러한 방식은 작업이 끝나지 않은 코드가 마스터에 섞이지 않고, 기능이 추가될 때마다 버전을 명확히 나눌 수 있고, 기능에 문제가 생기면 이전 상태로 쉽게 되돌릴 수 있는 게 장점이지만, 독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터로 통합하기 어려워진다.
이를 해결하기위해 마스터를 개인 브랜치로 수시로 리베이스 혹은 머지 하지만 여러 기능 브랜치에서 동시에 작업이 진행될 때는 이런 식으로 해결할 수 없다.
마스터를 브랜치로 ‘머지’ 하는 방법은 ‘단방향’ 이다. 브랜치만 바뀌고 마스터는 그대로다.
‘통합’ 은 마스터를 개인 브랜치로 가져와서(pull) 작업한 결과를 다시 마스터에 올리는(push) ‘양방향’ 이다. 그래서 마스터와 브랜치가 모두 변경된다.
누군가 개인 브랜치를 마스터에 통합하기 전까지는 다른 사람이 그 내용을 볼 수가 없고 달라진 내용을 적용하기에 상당한 노력이 필요한 경우가 많이 생긴다.
이런 문제는 기능별 브랜치들이 독립적으로 개발되는 기간이 길어질 수록 기하급수적으로 늘어난다. 4주간 작업한 브랜치를 통합하는 노력이 2주간 작업한 브랜치를 통합하는 노력보다 훨씬 크다.
이때문에 기능별 브랜치의 통합 주기를 2~3일 단위로 짧게 관리해야 한다는 주장들이 많다 .
이 방식을 지속적 통합(Continuous Integration, CI), 또는 트렁크 기반 개발(Trunk-Based Development, TBD) 라고 한다.
CI에 따르면 모든 팀원이 하루에 최소 한번은 마스터와 통합을 한다. 이렇게 하면 다른 브랜치들과 차이가 크게 벌어지는 브랜치가 없어져서 머지의 복합도를 상당히 낮출 수 있다. 하지만 CI를 적용하기 위해서는 마스터를 건강하게 유지하고, 거대한 기능을 잘게 쪼개는 법을 배우고, 각 기능을 끌 수 있는 기능 토글(feature toggle (기능 플래그 feature fleg))을 적용하여 완료되지 않은 기능이 시스템 전체를 망치지 않도록 해야한다.
이러한 CI는 리팩토링과 궁합이 아주 좋다.
테스팅
리팩토링안 단계별 변경 폭이 작아서 도중에 발생한 오류의 원인이 될만한 코드 범위가 넓지 않다. 원인을 못 찾더라도 버전 관리 시스템을 이용하여 가장 최근에 정상 작동하던 상태로 되돌리면 된다. 여기서 핵심은 오류를 빨리 찾는 것에 있다. 이렇게 하려면 코드의 다양한 측면을 검사하는 테스트 스위트(test suite)거 필요하다. 그리고 이를 빠르게 실행할 수 있어야 수시로 테스트를 하는 데 부담이 없고 ,이는 자가 테스트 코드(self-testing code)를 마련해야 한다는 뜻이다. 테스트 코드에 어느 정도 노력을 기울여야 하지만 효과가 상당하다.
자가 테스트 코드는 리팩토링을 할 수 있게 해줄 뿐만 아니라, 새 기능 추가도 훨씬 안전하게 진행할 수 있도록 도와준다. 테스트가 실패한다면 가장 최근에 통과한 버전이 무엇인지 살펴보고 테스트 주기를 짧게 하여 단 몇줄만 비교하여 문제가 일으킨 부분을 쉽게 찾아낼 수 있다.
자가 테스트 코드는 통합 과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와 연결된다.
레거시 코드 legacy code
레거시 코드란 사전적 의미로 보면 유산, 산물이 된 코드로 누가 떠나면서 남겨놓은 코드를 말한다.
레거시 시스템을 파악할때 리팩토링이 굉장히 도움이 된다.
리팩토링와 성능
‘직관적인 설계 vs 성능’ 은 중요한 주제이다.
리팩토링 하면 소프트웨어가 느려질 수도 있지만 , 그와 동시에 성능을 튜닝하기는 더 쉬워진다.
성능에 대한 흥미로운 사실은, 대부분 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비한다는 것이다. 코드 전체를 최적화 한다면 그중 90%는 효과가 거의 없고 10%에서 효과를 보는 것이다.
성능 개선을 위한 세번째 방법은 이 ‘90%의 시간은 낭비’라는 통계에서 착안한 것인데 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경 쓰지 않고 코드를 다루기 쉽게 만드는 데 집중하고 성능 최적화단계가 되면 다음의 구체적인 절차에 따라 프로그램을 튜닝한다.
- 먼저 프로파일러로 프로그램을 분석하여 시간과 공간을 많이 잡아먹는 지점을 알아낸다.
- 성능에 큰 영향을 주는 작은 부분을 찾을 수 있는 데 그부분을 리팩토링할 때처럼 개선한다.
리팩토링이 잘 되어 있으면
- 성능 튜닝에 투입할 시간을 벌 수 있다.
성능을 더 세밀하게 분석할 수 있다.
위 내용은 ‘리팩터링 2판 - 마틴 파울러’를 읽고 나중에 또 다시 보기 위해 정리한 내용입니다.
Discussion and feedback