리팩터링: [명사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법 (ex : 함수추출하기, 조건부로직을 다형성으로 바꾸기)
리팩터링(하다): [동사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러 가지 리팩터링 기법을 적용해서 소프트웨어를 재구성하다.
누군가 "리팩터링하다가 코드가 깨져서 며칠이나 고생했다"라고 한다면, 십중팔구 리팩터링 한 것이 아니다.
코드베이스를 정리하거나 구조를 바꾸는 모든 작업을 '재구성(restructuring)'이라는 포괄적인 용어로 표현한다면, 리팩터링은 재구성 중 특수한 한 형태에 속한다.
리팩터링의 전후는 사전적 정의에서 보듯이 사용자 관점에서는 달라지는 점이 없어야 한다.
리팩터링은 성능 최적화와 비슷하다. 둘 다 코드를 변경하지만 프로그램의 전반적인 기능은 그대로 유지한다.
하지만 성능최적화와 달리 리팩터링의 키 포인트는 코드를 이해하고 수정하기 쉽게 만드는 것이다. 그래서 리팩터링 후 프로그램 성능은 좋아질 수도, 나빠질 수도 있다.
소프트웨어 개발할 때의 목적이 '기능추가'냐, '리팩터링'이냐를 명확히 구분해서 작업하는게 좋다.
'기능추가'를 할 때는 기존 코드를 절대 건드리지 않고 새 기능을 추가하기만 한다. 반면 리팩터링 할 때는 기능 추가는 절대 하지않고 오로지 코드 재구성에만 전념한다.
리팩터링하지 않으면 소프트웨어의 내부 설계(아키텍처)가 썩기 쉽다. 아키텍처를 충분히 이해하지 못한 채 단기 목표만을 위해 코드를 수정하다 보면 기반 구조가 무너지기 쉽다. 코드만으로 설계를 파악하기 어려워질수록 설계를 유지하기 어려워지고, 설계가 부패하는 속도는 더욱 빨라진다. 반면 규칙적인 리팩터링은 코드의 구조를 지탱해 줄 것이다.
리팩터링을 전혀안함
설계가 나쁨 -> 코드가 길어짐 -> 코드가 길수록 실수 없이 수정하기 어려워짐 -> BAD! 🤢
꾸준한 리팩터링
리팩터링 진행 -> 중복코드 제거 -> 이해해야 할 코드량이 적어지고, 모든 코드가 언제나 고유한 일을 수행함을 보장 -> 바람직한 설계 -> GOOD! 🤗
프로그래밍은 결국 내가 원하는 바를 정확히 표현하는 길이다.
내 소스코드는 컴퓨터만 사용하는게 아니다. 누군가가 내 코드를 수정하고자 읽게 될 수 있다. 사실 사람이 가장 중요하지만 소홀하기 쉽다. 코드를 컴파일하는데 시간이 살짝 더 걸린다고 누가 뭐라 하겠는가? 하지만 다른 프로그래머가 내 코드를 제대로 이해했다면 한 시간에 끝낼 수정을 일주일이나 걸린다면 사정이 달라진다.
리팩터링하면 코드가 하는 일을 깊이 파악하게 되면서 새로 깨달은 것을 곧바로 코드에 반영하게 된다. 프로그램의 구조를 명확하게 다듬으면, 버그를 지나칠래야 지나칠 수 없을 정도까지 명확해진다.
초기엔 진척이 빨랐지만 새 기능을 하나 추가하는데 시간이 점점 길어지는 경우가 있고, 기존에 작성한 코드를 최대한 활용할 수 있어 새 기능을 더 빨리 추가할 수 있는 경우가 있다. 이렇게 차이나는 원인은 소프트웨어의 내부 품질에 있다. 내부 설계가 잘 된 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있다. 모듈화가 잘 되어 있으면 전체 코드베이스 중 작은 일부만 이해하면 된다. 코드가 명확하면 버그를 만들 가능성도 줄고, 버그를 만들더라도 디버깅하기가 훨씬 쉽다. 내부 품질이 뛰어난 코드베이스는 새 기능 구축을 돕는 견고한 토대가 된다.
리팩터링하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다.이 시점에 현재 코드를 살펴보면서, 구조를 살짝 바꾸면 다른 작업을 하기 훨씬 쉬워질 만한 부분을 찾는다.
버그를 잡을 때도 맟나가지다. 오류를 일으키는 코드가 세 곳에 복제되어 퍼져 있다면, 우선 한 곳으로 합치는 편이 작업하기에 훨씬 편하다. 질의 코드에 섞여 있는 갱신 로직을 분리하면 두 작업이 꼬여서 생기는 오률르 크게 줄일 수 있다. 이처럼 준비를 위한 리팩터링으로 상황을 개선해놓으면 버그가 수정된 상태가 오래 지속될 가능성을 높이는 동시에, 같은 곳에서 다른 버그가 발생할 가능성을 줄일 수도 있다.
코드를 파악할 때마다 그 코드의 의도가 더 명확하게 드러나도록 리팩터링할 여지는 없는지 찾아봐야 한다. 족건부 로직의 구조가 이상하지 않는지 살펴보기도 하고, 함수 이름을 잘 못 정해서 실제로 하는 일을 파악하는 데 시간이 오래 걸리지는 않는지도 살펴봐야 한다.
코드를 파악하던 중 로직이 쓸데 없이 복잡하거나, 매개변수화한 함수 하나면 될 일을 거의 똑같은 함수 여러 개로 작성해놨을 수 있다. 이때 약간 절충을 해야한다. 원래 하던 작업과 관련 없는 일일 지라도 쓰레기가 나뒹굴게 방치하는것은 좋지않다. 간단히 수정할 수 있는 것은 즉시 고치고, 시간이 좀 걸리는 일은 짧은 메모만 남긴 다음, 하던일을 끝내고 나서 처리한다.
앞선 리팩터링들은 모두 기회가 될 때만 진행한다. 리팩터링은 프로그래밍과 구분되는 별개의 활동이 아니다. 보기싫은 코드를 발견하면 리팩터링 하자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야 한다.
무언가 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고, 그런다음 쉽게 수정하자. 뛰어난 개발자는 새 기능을 추가하기 쉽도록 코드를 '수정'하는 것이 그 기능을 가장 빠르게 추가하는 길일 수 있음을 안다.
리팩터링 커밋과 기능추가 커밋을 분리하는 것은 팀에 적합한 방식에 맞춰 결정하는게 좋다.
(저자의 경우 분리하지 않은 것을 선호한다)
리팩터링은 대부분 몇 분 안에 끝난다. 길어야 몇 시간 정도다. 하지만 팀 전체가 달려들어도 몇 주는 걸리는 대규모 리팩터링도 있다. 그러나 이런 상황에 처하더라도 팀 전체가 리팩터링에 매달리는 데는 회의적이다. 그보다는 주어진 문제를 몇 주에 걸쳐 조금씩 해결해가는 편이 효과적일 때가 많다.
라이브러리를 교체할 때는 기존 것과 새 것 모두를 포용하는 추상 인터페이스부터 마련한다. 기존 코드가 이 추상 인터페이스를 호출하도록 만들고나면 라이브러리를 훨씬 쉽게 교체할 수 있다.
(이 전략을 추상화로 갈아타기라 한다.)
코드 작성자가 참여하는 코드리뷰가 리팩터링 활용하기에 좋다. 가장 좋은 방법은 작 성자와 나란히 앉아서 코드를 훝어가면서 리팩터링하는 일종의 짝 프로그래밍(PairProgramming)이다.
일정을 최우선으로 여기는 관리자는 최대한 빨리 끝내는 방향으로 진행하기를 원한다. 그리고 구체적인 방법은 개발자가 판단해야 한다. 프로 개발자에게 주어진 임무는 새로운 기능을 빠르게 구현하는 것이고, 가장 빠른 방법은 리팩터링이다. 그래서 리팩터링부터 해야한다.
지저분한 코드를 발견해도 굳이 수정할 필요가 없다면 리팩터링 하지않는다. 외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 둔다. 내부 동작을 이해해야 할 시점에 리팩터링해야 효과를 제대로 볼 수 있다.
리팩터링하는 것보다 처음부터 새로 작성하는게 쉬울 때도 리팩터링하지 않는다.
많은 사람들이 리팩터링 때문에 새 기능을 개발하는 속도가 느려진다고 여기지만, 리팩터링의 궁극적인 목적은 개발속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것이다.
코드베이스가 건강하면 기존 코드를 새로운 방식으로 조합하기 쉬워서 복잡한 새 기능을 더 빨리 추가할 수 있다. 그러나 항상 위험한 오류를 조심해야한다. 리팩터링은 '클린코드'가 아니다. 리팩터링의 본질은 코드베이스를 예쁘게 꾸미는 데 있지 않다. 오로지 경제적인 이유로 하는 것이다.
프로그래머마다 각자가 책임지는 영역이 있을 수는 있다.이 말은 자신이 맡은 영역의 변경 사항을 관리하라는 뜻이지. 다른 사람이 수정하지 못하게 막으라는 뜻이 아니다.
대규모 개발환경에서 리팩터링 시 PR을 통해 컨펌 받는 식으로 변경을 어느정도 통제하면서 느슨하게 풀어둘 수 있게 활용할 수 있다.
어떤 기능 전체를 한 브랜치에만 구현해놓고, 프로덕션 버전으로 릴리스 할 때가 돼서야 마스터에 통합하는 경우가 많다. 이 방식을 선호하는 이들은 작업이 끝나지 않은 코드가 마스터에 섞이지 않고, 기능 추가 될 때마다 버전을 명확히 나눌 수 있고, 기능에 문제가 생기면 이전 상태로 쉽게 되돌릴 수 있어서 좋다고 한다.
하지만 이런 기능 브랜치 방식은 독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터로 통합하기가 어려워진다. 이 고통을 줄이고자 많은 이들이 마스터를 개인 브랜치에 수시로 리베이스 하거나 머지한다. 그러나 머지와 통합은 명확히 구분되어야 한다.
'머지'는 단방향이다 브랜치만 바뀌고 마스터는 그대로다. 반면 '통합'은 마스터를 개인브랜치로 가져와서 작업한 결과를 다시 마스터에 올리는 양방향 처리를 뜻한다. 그래서 마스터와 브랜치가 모두 변경된다.
머지가 복잡해지는 문제는 기능별 브랜치들이 독립적으로 개발되는 기간이 길어질 수록 기하급수적으로 늘어난다. 이ㄷ 때문에 기능별 브랜치의 통합주기를 2~3일 단위로 짧게 관리해야 한다고 주장하는 사람이 많다. 이 방식을 지속적 통합(CI) 혹은 트렁크 기반 개발(TBD)이라 한다. CI에 따르면 모든 팀원이 하루에 최소 한 번은 마스터와 통합한다. 이렇게 하면 다른 브랜치들과 차이가 크게 벌어지는 브랜치가 없어져서 머지의 복잡도를 상당히 낮출 수 있다. 하지만 CI를 적용하기 위해서는 치러야할 대가가 있다. 마스터를 건강하게 유지하고, 거대한 기능을 잘게 쪼개는 법을 배우고, 각 기능을 끌 수 있는 기능 토글(=기능 플래그)을 적용하여 완료되지 않는 기능이 시스템 전체를 망치지 않도록 해야 한다.
절차를 지켜 제대로 리팩터링하면 동작이 깨지지 않아야 한다. 하지만 실수를 저지른다면? 리팩터링은 단계별 변경 폭이 작아서 도중에 발생한 오류의 원인이 될만한 코드 범위가 넓지 않다. 원인을 못 찾더라도 버전 관리 시스템을 이용하여 가장 최근에 정상 작동하던 상태로 되돌리면 된다.
핵심은 오류를 재빨리 잡아야하고, 이렇게하려면 대부분의 경우 자가 테스트코드를 마련해야 한다.
자가 테스트코드는 리팩터링을 할 수 있게 해줄 뿐만 아니라, 새 기능 추가도 훨씬 안전하게 진행할 수 있도록 도와준다. 실수로 만든 버그를 빠르게 찾아서 제거할 수 있기 때문이다. 또한 리팩터링 과정에서 버그가 생길 위험이 크다는 불안감을 해소할 수 있다.
물론 자동 리팩터링 기능을 제공하는 환경이라면 굳이 테스트하지 않아도 오류가 생기지 않는다고 확신할 수 있다. 따라서 안전한 자동 리팩터링만을 활용한다면 테스트 없이 리팩터링해도 좋다. 물론 활용할 수 있는 리팩터링 기법 수는 제한되겠지만, 자동 리팩터링들만으로도 의미 있는 효과를 보기에 충분하다.
자가 테스트 코드는 통합과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와도 밀접하게 연관된다. CI에 통합된 테스트는 XP의 권장사항이자 지속적 배포(CD)의 핵심이기도 하다.
대규모 레거시 시스템을 테스트 코드 없이 명료하게 리팩터링하기는 어렵다. 이 문제의 정답은 당연히 테스트 보강이다. 쉽게 해결되는 문제는 아니다. 그나마 해줄 수 있는 조언은 '레거시 코드 활용전략'에 나온 지침을 충실히 따르는 것이다.
프로그램에서 테스트를 추가할 틈새를 찾아서 시스템을 테스트해야 한다
처음부터 자가 테스트코드를 작성해야 하는 이유가 여기에 있다. 물론 테스트 코드를 갖추고 있더라도 복잡하게 얽힌 레거시 코드를 아름다운 코드로 단번에 리팩터링하기란 쉬운일이 아니다. 그러므로 서로 관련된 부분끼리 나눠서 하나씩 공략해야한다. 코드의 한 부분을 훑고 넘어갈 때마다 예전보다 조금이라도 개선하려고 노력한다. 레거시 시스템의 규모가 크다면 자주 보는 부분을 더 많이 리팩터링 한다. 코드를 훑게 되는 횟수가 많다는 말은 그 부분을 이해하기 쉽게 개선했을 때 얻는 효과도 그만큼 크다는 뜻이니 당연히 이렇게 해야 한다.
데이터베이스 리팩터링은 프로덕션 환경에 여러 단계로 나눠서 릴리스하는 것이 대체로 좋다는 점에서 다른 리팩터링과 다르다. 이렇게 하면 프로덕션 환경에서 문제가 생겼을 때 변경을 되돌리기 쉽다.
리팩터링이 아키텍처에 미치는 실질적인 효과는 요구사항 변화에 자연스럽게 대응하도록 코드베이스를 잘 설계해준다는 데 있다. 코딩 전에 아키텍처를 확정지으려 할 때의 대표적인 문제는 소프트웨어 요구사항을 사전에 모두 파악해야 한다는 것이다. 하지만 우리는 소프트웨어를 실제로 사용해보고 업무에 미치는 영향을 직접 확인하고 나서야 정말로 원하는 바를 알게 되는 경우가 허다하다.
한 가지 방법은 '유연성 메커니즘'을 소프트웨어에 심어두는 것이다. 가령 함수를 정의하다 보면 범용적으로 사용할 수 있겠다라는 생각이 들 때가 있다. 그래서 다양한 예상 시나리오에 대응하기 위한 매개변수들을 추가한다. 이런 매개변수가 바로 유연성 메커니즘이다. 물론 메커니즘들이 대개 그렇듯 치러야 할 비용이 있다. 매개변수를 생각나는 대로 추가하다 보면 함수가 너무 복잡해진다. 또한 깜박 잊은 매개변수가 있다면 앞서 추가해둔 매개변수들 때문에 새로 추가하기가 더 어려워진다. 간혹 설계한 메커니즘 자체가 결함이 있을 때도 존재한다. 이 모든 상황을 고려하다보면 유연성 메커니즘이 오히려 변화에 대응하는 능력을 떨어뜨릴 때가 대부분이다.
리팩터링을 활용하면 다르게 접근할 수 있다. 앞으로 어느 부분에 유연성이 필요하고 어떻게 해야 그 변화에 가장 잘 대응할 수 있을지 추측하지 않고, 그저 현재까지 파악한 요구사항만을 해결하는 소프트웨어를 구축한다. 진행하면서 사용자의 요구사항을 더 잘 이해하게 되면 아키텍처도 그에 맞게 리팩터링해서 바꾼다. 그 과정에서 소프트웨어의 복잡도에 지장을 주지 않는 메커니즘은 마음껏 추가하지만, 복잡도를 높일 수 있는 유연성 메커니즘은 반드시 검증을 거친 후에 추가한다. 매개변수를 추가해야 할 시점이 오면 간단한 리팩터링 기법인 함수 매개변수화하기로 해결한다. 예상되는 변경을 미리 반영하는 리팩터링을 미루면 얼마나 어려워질지를 가늠해보면 판단에 도움이 될 때가 많다. 리팩터링을 미루면 훨씬 힘들어진다는 확신이 들 때만 유연성 메커니즘을 미리 추가한다.
이런식으로 설계하는 방식을 간결한 설계, 점진적 설계, YAGNI(You Aren't Going to Need It) 등으로 부른다
YAGNI는 아키텍처를 전혀 고려하지 말라는 뜻은 아니다. YAGNI는 아키텍처와 설계를 개발 프로세스에 녹이는 또 다른 방식이며, 리팩터링의 뒷받침 없이는 효과를 볼 수 없다. 결론적으로 나중에 문제를 더 깊이 이해하게 됐을 때 처리하는 쪽이 훨씬 낫다고 생각한다면 '진화형 아키텍처' 원칙을 생각해보는게 좋다.
실제로 리팩터링이 퍼지기 시작한 것은 익스트림 프로그래밍(XP)에 도입됐던게 크다. XP의 두드러진 특징은 지속적 통합, 자가 테스트코드, 리팩터링 등의 개성이 강하면서 상호 의존하는 기법들을 하나로 묶은 프로세스라는 점이다. 참고로 자가 테스트 코드와 리팩터링을 묶어서 테스트 주도 개발(TDD)라 한다
리팩터링의 첫 번째 토대는 자가 테스트 코드다. 다시 말해 프로그래밍 도중 발생한 오류를 확실히 걸러내는 테스트를 자동으로 수행할 수 있어야 한다. 또한 팀으로 개발하면서 리팩터링을 하기 위해선 지속적 통합을 적용해서 팀원 각자가 수행한 리팩터링의 결과를 빠르게 동료와 공유할 수 있다. 이렇듯 자가 테스트코드, 지속적 통합, 리팩터링이라는 세 기법은 서로 강력한 상승효과를 발휘한다.
이상의 세 실천법을 적용한다면 앞 절에서 설명한 YAGNI 설계 방식으로 개발을 진행 할 수 있다.
XP (리팩터링 + 자가테스트코드 + CI = 강력한 상승효과) ==> YAGNI 설계방식 ==> CD (지속적배포)
리팩터링하면 소프트웨어가 느려질 수도 있는 건 사실이다. 하지만 그와 동시에 성능을 튜닝하기는 더 쉬워진다. 소프트웨어를 빠르게 만드는 비결은, 먼저 튜닝하기 쉽게 만들고 나서 원하는 속도가 나게끔 튜닝하는 것이다.
빠른 소프트웨어를 작성하는 방법에는 세 가지가 있다. 그 중 가장 엄격한 방법은 시간예산분배방식으로, 하드 리얼타임 시스템에서 많이 사용한다. 이 방식에 따르면 설계를 여러 컴포넌트로 나눠서 컴포넌트마다 자원(시간과 공간) 예산을 할당한다. 컴포넌트는 할당된 자원 예산을 초과할 수 없다.단 주어진 자원을 서로 주고 받는 메커니즘을 제공할 수는 있다. 시간 예산 분배방식은 엄격한 시간엄수를 강조한다. 심장 박동 조율기처럼 데이터가 늦게 도착하면 안되는 시스템에서는 이러한 점이 매우 중요하다.
두 번째 방법은 끊임없이 관심을 기울이는 것이다. 프로그래머라면 누구나 높은 성능을 유지하기 위해 무슨 일이든 한다. 흔히 사용하는 방식이지만 실제 효과는 변변치 않다. 성능을 개선하기 위해 코드를 수정하다보면 프로그램은 다루기 어려운 형태로 변하기 쉽고, 결국 개발이 더뎌진다. 이 방식에서는 성능을 개선하기 위한 최적화가 프로그램 전반에 퍼지게 되는데, 각각의 개선은 프로그램의 특정 동작에만 관련될 뿐, 정작 컴파일러와 런타임과 하드웨어의 동작을 제대로 이해하지 못한 채 작성될 때도 많다.
성능에 대해 흥미로운 사실은, 대부분 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비한다는 것이다. 그래서 코드 전체를 고르게 최적화한다면 그중 90%는 효과가 거의 없기 때문에 시간 낭비인 셈이다.
성능 개선을 위한 세 번째 방법은 이 '90% 시간은 낭비'라는 통계에서 착안한 것이다. 즉, 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경을 쓰지 않고 코드를 다루기 쉽게 만드는데 집중한다. 그러다 성능 최적화 단계가 되면 다음의 구체적인 절차를 따라 프로그램을 튜닝한다.
먼저 프로파일러로 프로그램을 분석하여 리소스를 많이 잡아먹는 지점을 알아낸다. 그런다음 그 부분을 개선한다. 이렇게 하면 성능에 큰 영향을 주는 부분만 집중적으로 최적화하기 때문에 적은 노력으로 훨씬 큰 효과를 볼 수 있다. 이때도 물론 신중하게 작업해야 한다. 리팩터링 할때처럼 최적화를 위한 수정도 작은 단계로 나눠서 진행한다. 각 단계마다 컴파일과 테스트를 거치고 프로파일러를 다시 실행해본다. 성능이 개선되지 않았다면 수정내용을 되돌린다. 이를 반복한다.
프로그램을 잘 리팩터링해두면 이런 식의 최적화에 두 가지 면에서 도움이 된다.첫째. 성능 튜닝에 투입할 시간을 벌 수 있다. 리팩터링이 잘 되어 있다면 기능 추가가 빨리 끝나서 성능에 집중할 시간을 더 벌 수 있다. 둘째. 리팩터링이 잘 되어있는 프로그램은 성능을 더 세밀하게 분석할 수 있다. 프로파일러가 지적해주는 코드의 범위가 더 좁아질 것이고, 그래서 튜닝하기 쉬워진다. 코드가 깔끔하면 개선안들이 더 잘 떠오를 것이고, 그 중 어떤 튜닝의 효과가 좋을지 파악하기 쉽다.
별로 안중요한 내용이라 skip
IDE는 구문 트리를 분석해서 리팩터링하기 때문에 단순한 텍스트 에디터와는 비교할 수 없을 만큼 유리하다.
- 리팩터링 연습을 더하고싶다면?
리팩터링 워크북(인사이트, 2006)- 소프트웨어 패턴과 리팩터링을 접목시키고 싶다면?
패턴을 활용한 리팩터링(인사이트, 2011)- 특정분야에 특화된 리팩터링을 알고싶다면?
리팩토링 데이터베이스(위키북스, 2007)과 리팩토링 HTML(에이콘 출판사, 2009)- 오래된 코드베이스를 리팩터링 하고싶다면?
레거시 코드 활용 전략(에이콘출판사, 2018)