티스토리 뷰

이번 장에서는 시야를 넓혀 리팩터링 전반에 적용되는 원칙 몇 가지를 이야기하는 시간을 가져보자.

 

2.1 리팩터링 정의

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

수많은 사람이 코드를 정리하는 작업을 모조리 리팩터링이라고 표현하고 있는데, 앞에서 제시한 정의한 방식에 따라 코드를 정리하는 것만이 리팩터링이다. 리팩터링을 동작을 보존하는 작은 단계들을 거쳐 코드를 수정하고, 이러한 단계 들을 순차적으로 연결하여 큰 변화를 만들어내는 일이다. 개별 리팩터링은 아주 작을 수도 있고, 작은 단계 여러 개가 합쳐진 모습일 수도 있다. 따라서 리팩터링 하는 동안에는 코드가 항상 정상 작동하기 때문에 전체 작업이 끝나지 않았더라도 언제든 멈출 수 있다.

누군가 "리팩터링 하다가 코드가 깨져서 며칠이나 고생했다" 라고 한다면, 십중팔구 리팩터링한 것이 아니다.

한 번에 바꿀 수 있는 작업을 수많은 단계로 잘게 나눠서 작업하는 모습을 처음 접하면 리팩터링하는 것이 오히려 비효율적이라고 생각하기 쉽다. 하지만 이렇게 나눔으로써 오히려 더 빨리 작업할 수 있다. 단계들이 체계적으로 구성되어 있고, 무엇보다 디버깅하는 데 시간을 뺏기지 않기 때문이다.

리팩터링은 성능 최적화와 비슷하다. 둘 다 코드를 변경하지만 프로그램의 전반적인 기능은 그대로 유지한다. 단지 목적이 다를 뿐이다.
리팩터링의 목적은 코드를 이해하고 수정하기 쉽게 만드는 것이다. 프로그램 성능은 좋아질 수도, 나빠질 수도 있다. 반면 성능 최적화는 오로지 속도 개선에만 신경 쓴다. 그래서 목표 성능에 반드시 도달해야 한다면 코드는 다루기에 더 어렵게 바뀔 수도 있음을 각오해야 한다. 

 

2.2 두 개의 모자

나는 소프트웨어를 개발할 때 목적이 '기능 추가'냐, 아니면 '리팩터링'이냐를 명확히 구분해 작업한다.
켄트 벡은 이를 두 개의 모자에 비유했다. 기능을 추가할 때는 '기능 추가' 모자를 쓴 다음 기존 코드는 절대 건드리지 않고 새 기능을 추가하기만 한다. 진척도는 테스트를 추가해서 통과하는지 확인하는 방식으로 측정한다. 반면 리팩터링 할 때는 '리팩터링' 모자를 쓴 다음 기능 추가는 하지 않기도 다짐한 뒤 오로지 코드 재구성에만 전념한다. 테스트도 새로 만들지 않는다. 부득이 인터페이스를 변경해야 할 때만 기존 테스트를 수정한다.

나는 소프트웨어를 개발하는 동안 두 모자를 자주 바꿔 쓴다. 새 기능을 추가하다 보면 코드 구조를 바꿔야 작업하기 쉽겠다는 생각이 들거나 코드가 이해하기 어렵게 짜인 경우 모자를 바꿔 쓰고 리팩터링한 후에 어느 정도 개선되면 다시 모자를 바꿔 쓰고 기능 추가를 이어간다.

전체 작업 시간이 10분 정도로 짧다 해도, 항상 내가 쓰고 있는 모자가 무엇인지와 그에 따른 미묘한 작업 방식의 차이를 분명하게 인식해야 한다.

 

2.3 리팩터링하는 이유

리팩터링이 모든 문제를 해결하는 만병통치약은 절대 아니다. 하지만 코드를 건강하게 유지하는데 도와주는 약임은 분명하다. 리팩터링은 다양한 용도로 활용할 수 있고, 반드시 그래야 하는 도구다.

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

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

설계가 나쁘면 코드가 길어지기 십상이다. 같은 일을 하는 코드가 여러 곳에 나타날 수 있기 때문이다. 그래서 중복 코드 제거는 설계 개선 작업의 중요한 한 축을 차지한다. 코드량을 줄인다고 시스템이 빨라지는 것은 아니다. 프로그램의 용량이 속도에 영향을 주는 경우는 별로 없다. 하지만 코드량이 줄면 수정하는데 드는 노력은 크게 달라진다.

코드가 길수록 실수 없이 수정하기 어려워진다. 이해해야 할 코드량도 늘어난다. 비슷한 일을 하는 코드가 산재해 있다면 한 부분만 살짝 바꿔서는 시스템이 예상대로 작동하지 않을 수 있다. 반면 중복 코드를 제거하면 모든 코드가 언제나 고유한 일을 수행함을 보장할 수 있으며 이는 바람직한 설계의 핵심이다.

 

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

프로그래밍은 여러 면에서 컴퓨터와 대화하는 것과 같다. 컴퓨터에게 시킬 일을 표현하는 코드를 작성하면, 컴퓨터는 정확히 시킨 대로 반응한다. 그런데 내 소스 코드를 컴퓨터만 사용하는 게 아니다. 예컨대 몇 달이 지나 누군가 내 코드를 수정하고자 읽게 될 수 있다. 사실 프로그래밍에서는 사람이 가장 중요하지만 소홀하기 쉽다.
코드를 컴파일하는데 시간이 살짝 더 걸린다고 누가 뭐라 하겠는가? 하지만 다른 프로그래머가 내 코드를 제대로 이해했다면 한 시간에 끝낼 수정을 일주일이나 걸린다면 사정이 달라진다.

문제는 프로그램을 동작시키는 데만 신경 쓰다 보면 나중에 그 코드를 다룰 개발자를 배려하지 못한다는데 있다. 잘 작동하지만 이상적인 구조는 아닌 코드가 있다면, 잠깐 시간을 내서 리팩터링해보자. 그러면 코드를 작성한 의도를 더 명확하게 전달하도록 개선할 수 있다.

단지 다른 사람을 배려하기 위해서는 아니다. 사실 그 다른 사람이 바로 나 자신일 때가 많다. 그래서 더더욱 리팩터링이 중요하다. 난 굉장히 게으른 프로그래머다. 단적인 예로 내가 작성한 코드를 전혀 머리에 담아두지 않는다. 코드를 보면 알 수 있는 것들은 의도적으로 기억하지 않는다. 내 기억 용량을 초과할까 봐 두렵기 때문이다. 그래서 기억할 필요가 있는 것들은 최대한 코드에 담으려고 한다.

 

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

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

이 사실은 켄트 벡의 말을 떠올리게 해준다. "난 뛰어난 프로그래머가 아니에요. 단지 뛰어난 습관을 지닌 괜찮은 프로그래머일 뿐이에요." 리팩터링은 견고한 코드를 작성하는 데 무척 효과적이다.

 

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

지금까지 내용을 정리하면 다음과 같다. 리팩터링하면 코드 개발 속도를 높일 수 있다.

얼핏 그 반대로 생각할 수 있다. 내가 사람들에게 리팩터링에 대해 설명하면 품질을 높일 수 있다는 점에는 대부분 쉽게 수긍한다. 내부 설계와 가독성이 개선되고 버그가 줄어든다는 점은 모두 품질 향상에 직결된다. 하지만 리팩터링 하는데 시간이 드니 전체 개발 속도는 떨어질까 봐 걱정할 수도 있다.

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

이 과정을 그래프로 표현하면 대략 다음과 같다.

<그래프 용어 설명>
- cumulative functionality : 기능의 누적
- time : 구현 속도
- high internal quality : 좋은 설계
- low internal quality : 나쁜 설계

 

그런데 어떤 팀은 이와 전혀 다른 양상을 보인다. 이들은 기존에 작성한 코드를 최대한 활용할 수 있어서 새 기능을 더 빨리 추가한다.

 

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

나는 이 효과를 설계 지구력 가설(Design Stamina Hypothesis)이라고 부른다. 내부 설계에 심혈을 기울이면 소프트웨어의 지구력이 높아져서 빠르게 개발할 수 있는 상태를 더 오래 지속할 수 있다. 정말 그런지는 증명할 수 없어서 '가설'이라고 표현했다. 하지만 수많은 뛰어난 프로그래머들의 경험이 이를 뒷받침한다.

20년 전만 해도 설계를 잘하려면 코딩을 시작하기 전에 설계부터 완벽히 마쳐야 한다는 것이 정설이었다. 코딩 단계에 한번 들어서면 코드가 부패할 일만 남았기 때문이다. 한변 리팩터링을 하면 기존 코드의 설계를 얼마든지 개선할 수 있으므로, 설령 프로그램 요구사항이 바뀌더라도 설계를 지속해서 개선할 수 있다. 처음부터 좋은 설계를 마련하기란 매우 어렵다. 그래서 빠른 개발이라는 숭고한 목표를 달성하려면 리팩터링이 반드시 필요하다.

 

2.4 언제 리팩터링해야 할까?

나는 프로그래밍할 때 거의 한 시간 간격으로 리팩터링한다. 그러다 보니 내 작업 흐름에 리팩터링을 녹이는 방법이 여러 가지임을 알게 됐다.

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

리팩터링하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다. 이 시점에 코드를 살펴보면서, 구조를 살짝 바꾸면 다른 작업을 하기 훨씬 쉬워질 만한 부분을 찾는다. 버그를 잡을 때도 마찬가지다. 오류를 일으키는 코드가 세 곳에 복제되어 퍼져 있다면, 우선 한 곳으로 합치는 편이 작업하기에 훨씬 편하다. 또는 질의 코드에 섞여 있는 갱신 로직을 분리하면 두 작업이 꼬여서 생기는 오류를 줄일 수 있다.

비유하면 지금 위치에서 동쪽으로 100km를 이동하려는데 그 사이를 숲이 가로막고 있다면,
좀 둘러가더라도 20km 북쪽에 있는 고속도로를 타는 편이 세 배나 빠를 수 있다.
다들 "직진!"을 외치더라도, 떄로는 "잠깐, 지도를 보고 가장 빠른 경로를 찾아보자"라고 말할 줄 알아야 한다.
준비를 위한 리팩터링이 바로 이런 역할을 한다.

 

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

코드를 수정하려면 먼저 그 코드가 하는 일을 파악해야 한다. 그 사람은 자신일 수도 있고 다른 사람일 수도 있다. 나는 코드를 파악할 때마다 그 코드의 의도가 더 명확하게 드러나도록 리팩터링할 여지가 없는지 찾아본다. 조건부 로직의 구조가 이상하지 않은지 살펴보기도 하고, 함수 이름을 잘못 정해서 실제로 하는 일을 파악하는 데 시간이 오래 걸리지는 않는지도 살펴본다. 

이렇게 하면 나중은 물론 지금 당장 효과를 볼 떄도 많다. 어떤 역할을 하는지 이해된 변수는 적절한 이름으로 바꿔주고, 긴 함수를 잘게 나누기도 한다. 그러면 코드가 깔끔하게 정리되어 전에는 보이지 않던 설계가 눈에 들어오기 시작한다. 코드를 분석할 때 리팩터링을 해보면, 그렇지 않았더라면 도달하지 못했을 더 깊은 수준까지 이해하게 된다. 이해를 위한 리팩터링을 의미 없이 코드를 만지작거리는 것이라고 무시하는 이들은 코드 아래 숨어 있는 다양한 기회를 결코 발견할 수 없다.

 

쓰레기 줍기 리팩터링

코드를 파악하던 중에 비효율적으로 처리하는 모습을 발견할 때가 있다. 이때 약간 절충을 해야 한다. 원래 하려던 작업과 관련 없는 일에 너무 많은 시간을 빼앗기긴 싫을 것이다. 그렇다고 쓰레기가 나뒹굴게 방치해서 나중에 일을 방해하도록 내버려 두는 것도 좋지 않다. 조금이나마 개선해두는 것이 좋다. 캠핑 규칙이 제안하듯, 항상 처음 봤을 때보다 깔끔하게 정리하고 떠나자. 코드를 훑어볼 때마다 조금씩 개선하다 보면 결국 문제가 해결될 것이다.

 

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

리팩터링은 과거에 저지른 실수를 바로잡거나 보기 싫은 코드를 정리하는 작업이라고 오해하기 쉽다. 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야 한다. 어제는 적합했던 기준이 오늘 하는 다른 작업에는 맞지 않을 수 있다. 상황이 변해 기준을 변경해야 할 때 코드가 이미 깔끔하다면 리팩터링하기가 더 쉽다.

뛰어난 개발자는 새 기능을 추가하기 쉽도록 코드를 '수정'하는 것이 그 기능을 가장 빠르게 추가하는 길일 수 있음을 안다. 소프트웨어 개발을 끝이 있는 작업으로 보면 안 된다. 새 기능이 필요할 떄마다 소프트웨어는 이를 반영하기 위해 수정된다. 이때 새로 작성해 넣는 코드보다 기존 코드의 수정량이 큰 경우가 대체로 많다.

그동안 리팩터링에 소홀했다면, 따로 시간을 내서 새 기능을 추가하기 쉽도록 코드베이스를 개선할 필요가 있다. 하지만 계획된 리팩터링을 하게 되는 일은 최소한으로 줄여야 한다. 리팩터링 작업 대부분은 드러나지 않게, 기회가 될 때마다 해야 한다.

 

오래 걸리는 리팩터링

리팩터링은 대부분 몇 분 안에 끝난다. 하지만 팀 전체가 달려들어도 몇 주는 걸리는 대규모 리팩터링도 있다. 라이브러리를 새것으로 교체하거나 골치 아픈 의존성을 정리하는 작업일 수도 있다.

이런 상황에 처하더라도 팀 전체가 리팩터링에 매달리는 데는 회의적이다. 그보다는 주어진 문제를 몇 주에 걸쳐 조금씩 해결해가는 편이 효과적일 때가 많다. 누구든지 리팩터링해야 할 코드와 관련한 작업을 하게 될 때마다 원하는 방향으로 조금씩 개선하는 식이다. 리팩터링이 코드를 깨트리지 않는다는 장점을 활용하는 것이다. 일부를 변경해도 모든 기능이 항상 올바르게 동작한다. 예컨대 라이브러리를 교체할 때는 기존 것과 새것 모두를 포용하는 추상 인터페이스부터 마련한다. (이 전략을 추상화 갈아타기라 한다)

 

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

코드 작성자가 참석해야 맥락을 설명해줄 수 있고 작성자도 리뷰어의 변경 의도를 제대로 이해할 수 있으므로, 이왕이면 참석자가 참석하는 방식이 좋다. 가장 좋은 방법은 작성자와 나란히 앉아서 코드를 훑어가면서 리팩터링하는 것이다. 이렇게 하면 자연스럽게 짝 프로그래밍이 된다.

 

관리자에게는 뭐라고 말해야 할까?

내가 가장 많이 받는 질문 중 하나는 "관리자에게 리팩터링에 대해 어떻게 말해야 하나요?다. 리팩터링만을 위한 일정을 몇 주씩 잡는 개발팀을 보면 오해는 더욱 커진다. 설상가상으로 실제로는 리팩터링이 아닌, 어설픈 재구성 작업을 하면서 코드베이스를 오히려 망가뜨리는 모습을 보면 불신이 증폭된다.

관리자가 기술에 정통하고 설계 지구력 가설도 잘 이해하고 있다면 오히려 관리자가 리팩터링을 권장할 뿐만 아니라 팀이 리팩터링을 충분히 하고 있는지 살펴보기도 한다. 그러면 팀이 수행하는 리팩터링이 과도할 수는 있어도, 부족할 가능성은 거의 없다.

물론 기술을 모르는 상당수의 관리자와 고객은 코드베이스의 건강 상태가 생산성에 미치는 영향을 모른다. 이런 상황에 있는 이들에게는 "리팩터링한다고 말하지 말라"고 조언하겠다.

하극상일까? 그렇진 않다. 소프트웨어 개발자는 프로다. 프로 개발자의 역할은 효과적인 소프트웨어를 최대한 빨리 만드는 것이다. 내 경험상 리팩터링하면 소프트웨어를 빨리 만드는데 아주 효과적이다. 일정을 최우선으로 여기는 관리자는 최대한 빨리 끝내는 방향으로 진행하기를 원한다. 그리고 구체적인 방법은 개발자가 판단해야 한다. 프로 개발자에게 주어진 임무는 새로운 기능을 빠르게 구현하는 것이고, 가장 빠른 방법은 리팩터링이다. 그래서 리팩터링부터 한다.

 

리팩터링하지 말아야 할 때

지금까지의 이야기가 무조건 리팩터링을 권장한다고 들릴 수 있는데, 리팩터링하면 안 되는 상황도 있다.
지저분한 코드를 발견해도 굳이 수정할 필요가 없다면 리팩터링하지 않는다. 외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 둔다. 내부 동작을 이해해야 할 시점에 리팩터링해야 효과를 제대로 볼 수 있다.

리팩터링하는 것보다 새로 작성하는 게 쉬울 때도 리팩터링하지 않는다. 이런 결정을 내리기는 쉽지 않다. 직접 리팩터링해보기 전에는 어느 쪽이 쉬운지 확실히 알 수 없을 때도 많기 때문이다. 리팩터링할지 새로 작성할지를 잘 결정하려면 뛰어난 판단력과 경험이 뒷받침돼야 한다.

2.5 리팩터링 시 고려할 문제

새 기능 개발 속도 저하

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

그렇더라도 상황에 맞게 조율해야 한다. 예컨대 대대적인 리팩터링이 필요해 보이지만, 추가하려는 새 기능이 아주 작아서 기능 추가부터 하고 싶을 수 있다. 이럴 때는 프로 개발자로서 가진 경험을 잘 발휘해서 결정한다.

 

코드 소유권

리팩터링하다 보면 모듈의 내부뿐 아니라 시스템의 다른 부분과 연동하는 방식에도 영향을 주는 경우가 많다. 함수를 호출하는 코드의 소유자가 다른 팀이라서 나에게는 쓰기 권한이 없을 수 있다. 또는 바꾸려는 함수가 고객에게 API로 제공되는 것이라면 누가 얼마나 쓰고 있는지, 실제로 쓰이기나 하는지조차 모를 수 있다. 이런 공개된 인터페이스는 사용하여 기존 함수를 그대로 유지하되 함수 본문에서 새 함수를 호출하도록 수정하는 전략이 필요하다. (deprecated)

이처럼 복잡해지기 때문에 나는 코드 소유권을 작은 단위로 나눠 엄격히 관리하는 데 반대하는 입장이다. 내가 선호하는 방식은 코드의 소유권을 팀에 두는 것이다. 그래서 팀원이라면 누구나 팀이 소유한 코드를 수정할 수 있게 한다. 프로그래머마다 각자가 책임지는 영역이 있을 수는 있다. 이 말은 자신이 맡은 영역의 변경 사항을 관리하라는 뜻이지, 다른 사람이 수정하지 못하게 막으라는 뜻이 아니다.

이렇게 코드 소유권을 느긋하게 정하는 방식은 여러 팀으로 구성된 조직에도 적용할 수 있다. 다른 팀 코드의 브랜치를 따서 수정하고 커밋을 요청하는 흡사 오픈소스 개발 모델을 권장하기도 한다.

 

브랜치

기능 브랜치 방식에는 단점이 있다. 독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터로 통합하기 어려워진다. 머지가 복잡해지는 문제는 기능별 브랜치들이 독립적으로 개발되는 기간이 길어질수록 기하급수적으로 늘어난다. 이 때문에 기능별 브랜치의 통합 주기를 2~3일 단위로 짧게 관리해야 한다고 주장하는 사람이 많다. 한편 나와 같은 사람들은 더 짧아야 한다고 주장한다. 이 방식을 지속적 통합(CI, Continuous Integration), 또는 트렁크 기반 개발(TBD, Trunk-Based Development)이라 한다. CI에 따르면 모든 팀원이 하루에 최소 한 번은 마스터와 통합한다. 하지만 CI를 적용하기 위해서는 마스터를 건강하게 유지하고, 거대한 기능을 잘게 쪼개는 법을 배우고, 각 기능을 끌 수 있는 기능 토글(feature toggle(flag))를 적용하여 완료되지 않은 기능이 시스템 전체를 망치지 않도록 해야 한다.

머지의 복잡도를 줄일 수 있어서 CI를 선호하기도 하지만, 가장 큰 이유는 리팩터링과 궁합이 좋기 때문이다. 켄트 벡이 CI와 리팩터링을 합쳐서 익스트림 프로그래밍(XP)을 만든 이유도 바로 두 기법이 궁합이 잘 맞기 때문이다.

기능별 브랜치를 사용하면 안 된다는 말을 아니다. 브랜치를 자주 통합할 수만 있다면 문제가 발생할 가능성을 크게 줄일 수 있다.

 

테스팅

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

자가 테스트 코드는 리팩터링을 할 수 있게 해 줄 뿐만 아니라, 새 기능 추가도 훨씬 안전하게 진행할 수 있도록 도와준다. 실수로 만든 버그를 빠르게 찾아서 제거할 수 있기 때문이다.  이때 핵심은 테스트가 실패한다면 가장 최근에 통과한 버전에서 무엇이 달라졌는지 살펴볼 수 있다는 데 있다. 테스트 주기가 짧다면 단 몇 줄만 비교하면 버그를 훨씬 쉽게 찾을 수 있다.

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

 

레거시 코드

레거시 시스템을 파악할 때 리팩터링이 굉장히 도움된다. 제 기능과 맞지 않은 함수 이름을 바로 잡고 어설픈 프로그램 구문을 매끄럽게 다듬어서 거친 원석 같던 프로그램을 반짝이는 보석으로 만들 수 있다. 하지만 이러한 희망찬 스토리에 테스트가 없다는 사실이 찬물을 끼얹는 때가 많다. 대규모 레거시 시스템을 테스트 코드 없이 명료하게 리팩터링하기는 어렵다.

이 문제의 정답은 당연히 테스트 보강이다. 하지만, 막상 해보면 생각보다 훨씬 까다롭다. 테스트를 염두에 두고 설계한 시스템만 쉽게 테스트할 수 있기 때문이다. 물론 그런 시스템이라며 테스트를 갖추고 있을 것이라서 애초에 이런 걱정을 할 일이 없다.

쉽게 해결할 방법은 없다. 그나마 레거시 코드 활용 전략의 조언에 따르면 '프로그램에서 테스트를 추가할 틈새를 찾아서 시스템을 테스트해야 한다'는 것이다.

 

데이터베이스

데이터베이스 리팩터링은 프로덕션 환경에 여러 단계로 나눠서 릴리즈하는 것이 대체로 좋다. 이렇게 하면 프로덕션 환경에서 문제가 생겼을 때 변경을 되돌리기 쉽다.

이를테면 필드 이름을 바꿀 때 첫 번째 커밋에서는 새로운 필드를 추가만 하고 사용하지 않는다. 그런 다음 기존 필드와 새 필드를 동시에 업데이트하도록 설정한다. 그다음에는 데이터베이스를 읽는 클라이언트들을 새 필드를 사용하는 버전으로 조금씩 교체한다. 이 과정에서 발생하는 버그도 해결하면서 클라이언트 교체 작업을 모두 끝냈다면, 더는 필요가 없어진 예전 필드를 삭제한다.

필드 이름 바꾸기 순서

  1. 데이터베이스에 새로운 필드를 추가만하고 사용하지 않는다.
  2. 기존 필드와 새 필드를 동시에 업데이트 하도록 변경한다.
  3. 데이터베이스를 읽는 클라이언트들을 새 필드를 사용하는 버전으로 조금씩 교체한다.
  4. 교체 작업을 모두 끝냈다면 더는 필요 없어진 예전 필드를 삭제한다.

이렇게 데이터베이스를 변경하는 방식은 병렬 수정(parallel change) 또는 팽창-수축(expend-contract)의 일반적인 예다.

 

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

내가 프로그래밍을 시작하던 시절에는 코딩을 시작하기 전에 소프트웨어 설계와 아키텍처를 거의 완료해야 한다고 배웠다. 일단 코드로 작성된 뒤로는 아키텍처를 바꿀 수 없고 부주의로 인해 부패할 일만 남았다고 여기곤 했다.

코딩 전에 아키텍처를 확정지으려 할 때의 대표적인 문제는 소프트웨어 요구사항을 사전에 모두 파악해야 한다는 것이다. 하지만 막상 우리는 소프트웨어를 실제로 사용해보고 업무에 미치는 영향을 직접 확인하고 나서야 정말로 원하는 바를 알게 되는 경우가 허다하다.

한 가지 방법은 향후 변경에 유연하게 대처할 수 있는 유연성 메커니즘을 소프트웨어에 심어두는 것이다. 물론 메커니즘들이 대개 그렇듯 치러야 할 비용이 있다. 당장의 쓰임에 비해 함수가 너무 복잡해지며 간혹 유연성 메커니즘을 잘못 구현할 때도 있다. 요구사항이 당초 예상과 다르게 바뀌기 때문일 수도 있고, 설계한 메커니즘 자체에 결함이 있어서일 때도 있다. 이 모든 상황을 고려하다 보면 유연성 메커니즘이 오히려 변화에 대응하는 능력을 떨어뜨릴 때가 대부분이다.

리팩터링을 활용하면 그저 현재까지 파악한 요구사항만을 해결하는 소프트웨어를 구축한다. 단, 이 요구를 멋지게 해결하도록 설계한다. 진행하면서 사용자의 요구사항을 더 잘 이해하게 되면 아키텍처도 그에 맞게 리팩터링해서 바꾼다. 그 과정에서 복잡도를 높일 수 있는 유연성 메커니즘은 반드시 검증을 거친 후에 추가한다. 예상되는 변경을 미리 반영하는 리팩터링을 미루면 나중에 얼마나 어려워질지를 가늠해보면 판단에 도움 될 때가 많다. 리팩터링을 미루면 훨씬 힘들어진다는 확신이 들 때만 유연성 메커니즘을 미리 추가한다.

이런 식으로 설계하는 방식을 간결한 설계, 점진적 설계, YAGNI(you aren't going to need it, 필요 없을 거다) 등으로 부른다. YAGNI를 받아들인다고 해서 선제적인 아키텍처에 소홀해도 된다는 뜻은 아니다. 리팩터링으로는 변경하기 어려워서 미리 생각해두면 시간이 절약되는 경우도 얼마든지 있다. 다만 이제는 둘 사이의 균형점이 크게 달라졌다. 나는 나중에 문제를 더 깊이 이해하게 됐을 때 처리하는 쪽이 훨씬 낫다고 생각하는 편이다.

 

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

2.5절을 읽었다면 팀이 따르는 실천법에 따라 리팩터링의 효과가 크게 달라진다. 실제로 리팩터링이 퍼지기 시작한 것도 익스트림 프로그래밍에 도입됐기 때문이다. XP의 특징은 지속적 통합, 자가 테스트 코드, 리팩터링 등의 개성이 강하면서 상호 의존하는 기법들을 하나로 묶은 프로세스라는 점이다.
참고로 자가 테스트 코드와 리팩터링을 묶어서 테스트 주도 개발(TDD)이라 한다.

XP는 수년에 걸쳐 애자일의 부흥을 이끌었다. 지금은 상당수의 프로젝트에서 애자일을 적용하고 있어서 애자일 사고가 주류로 자리 잡았다. 하지만 현재 '애자일'을 내세우는 프로젝트 중에는 이름만 애자일인 경우가 대부분이다. 애자일을 제대로 적용하려면 리팩터링에 대한 팀의 역량과 열정이 뒷받침되어 프로세스 전반에 리팩터링이 자연스럽게 스며들도록 해야 한다.

리팩터링의 첫 번째 토대는 자가 테스트 코드다. 다시 말해 프로그래밍 도중 발생한 오류를 확실히 걸러내는 테스트를 자동으로 수행할 수 있어야 한다.

팀으로 개발하면서 리팩터링을 하려면 각 팀원이 다른 사람의 작업을 방해하지 않으면서 언제든지 리팩터링할 수 있어야 한다. 지속적 통합을 적극 권장하는 이유도 바로 이 때문이다. 지속적 통합을 적용하면 각자가 수행한 리팩터링 결과를 빠르게 동료와 공유할 수 있다. 리팩터링한 결과가 다른 팀원의 작업에 문제를 일으키면 즉시 알아낼 수 있다. 자가 테스트 코드 역시 지속적 통합의 핵심 요소다. 따라서 자가 테스트 코드, 지속적 통합, 리팩터링이라는 세 기법은 서로 강력한 상승효과를 발휘한다.

이상의 세 실천법을 적용한다면 앞 절에서 설명한 YAGNI 설계 방식으로 개발을 진행할 수 있따. 리팩터링과 YAGNI는 서로 긍정적인 영향을 준다. 리팩터링이 YAGNI의 토대인 동시에, YAGNI로 인해 리팩터링을 더욱 쉽게 할 수 있다. 추측에 근거한 수많은 유연성 메커니즘을 갖춘 시스템보다는 단순한 시스템이 변경하기가 훨씬 쉽기 때문이다. 

지금까지의 설명이 다소 간단해 보일 수 있지만 실무에 적용하기는 만만치 않다. 어떤 방법으로 하든 소프트웨어 개발을 여러 사람과 기계가 복잡하게 엮어 상호작용하는 까다로운 일이다. 지금까지 소개한 접근법은 이 복잡도를 다루는 데 효과적이라고 검증된 것이다. 물론 어떠한 접근법이든지 충분한 연습과 뒷받침돼야 한다.

 

2.8 리팩터링과 성능

'직관적인 설계 vs 성능'은 중요한 주제다. 리팩터링하면 소프트웨어가 느려질 수도 있는 건 사실이다. 하지만 그와 동시에 성능을 튜닝하기는 더 쉬워진다. 하드 리얼타임 시스템을 제외한 소프트웨어를 빠르게 만드는 비결은, 먼저 튜닝하기 쉽게 만들고 나서 원하는 속도가 나게끔 튜닝하는 것이다.

나는 빠른 소프트웨어를 작성하는 방법 세 가지를 경험했다. 그중 가장 엄격한 방법은 시간 예산 분배(time budgeting) 방식으로, 하드 리얼타임 시스템에서 많이 사용한다. 설계를 여러 컴포넌트로 나눠서 컴포넌트마다 자원(시간과 공간) 예산을 할당한다. 심장 박동 조율기처럼 데이터가 늦게 도착하면 안 되는 시스템에서는 이러한 점이 굉장히 중요하다. 반면, 사내 정보 시스템과 같은 부류에는 맞지 않는 기법이다.

두 번째 방법은 끊임없이 관심을 기울이는 것이다. 프로그래머라면 누구나 높은 성능을 유지하기 위해 무슨 일이든 한다. 직관적이어서 흔히 사용하는 방식이지만 실제 효과는 변변치 않다. 성능을 개선하기 위해 코드를 수정하다 보면 프로그램은 다루기 어려운 형태로 변하기 쉽고, 결국 개발이 더뎌진다. 결과적으로 소프트웨어가 더 빨라지면 충분한 보상을 얻겠지만 실제로 그런 경우는 별로 없다. 이 방식에서는 성능을 개선하기 위한  최적화가 프로그램 전반에 퍼지게 되는데, 각각의 개선은 프로그램의 특정 동작에만 관련될 뿐, 정작 컴파일러와 런타임과 하드웨어의 동작을 제대로 이해하지 못한 채 작성할 때도 많다.

성능에 대한 흥미로운 사실은, 대부분 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비한다는 것이다. 그래서 코드 전체를 고르게 최적화한다면 그중 90%는 효과가 거의 없기 때문에 시간 낭비인 셈이다. 속도를 높이기 위해 투자한 시간(다른 관점에서 보자면 코드를 덜 명료하게 바꾸느라 투자한 시간)을 모두 날리는 행위다.

성능 개선을 위한 세 번째 방법은 이 '90%의 시간은 낭비'라는 통계에서 착안한 것이다. 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경 쓰지 않고 코드를 다루기 쉽게 만드는데 집중한다. 그러다 성능 최적화 단계가 되면 다음의 구체적인 절차를 따라 프로그램을 튜닝한다.
먼저 프로파일러로 프로그램을 분석하여 시간과 공간을 많이 잡아먹는 지점을 알아낸다. 그러면 성능에 큰 영향을 주는 작은 부분들을 찾을 수 있다. 그런 다음 그 부분들을 개선한다. 이렇게 하면 성능에 큰 영향을 주는 부분만 집중해서 최적화하기 때문에 적은 노력으로 훨씬 큰 효과를 볼 수 있다. 이때도 물론 신중하게 작업해야 한다. 리팩터링할 때처럼 최적화를 위한 수정도 작은 단계로 나눠서 진행한다. 각 단계마다 컴파일과 테스트를 거치고 프로파일러를 다시 실행해본다. 성능이 개선되지 않았다면 수정 내용을 되돌린다. 이런 식으로 만족하는 성능에 도달할 때 까지 최적화 대상을 찾아서 제거하는 일을 계속한다.

프로그램을 잘 리팩터링해두면 이런 식의 최적화에 두 가지 면에서 도움이 된다.
첫째, 성능 튜닝에 투입할 시간을 벌 수 있다. 리팩터링이 잘 되어 있다면 기능 추가가 빨리 끝나서 성능에 집중할 시간을 더 벌 수 있다.
둘째, 리팩터링이 잘 되어 있는 프로그램은 성능을 더 세밀하게 분석할 수 있다. 프로파일러가 지적해주는 코드의 범위가 더 좁아질 것이고, 그래서 튜닝하기 쉬워진다. 코드가 깔끔하면 개선안들이 더 잘 떠오를 것이고, 그중 어떤 튜닝이 효과가 좋을지 파악하기 쉽다.

리팩터링은 성능 좋은 소프트웨어를 만드는 데 기여한다. 단기적으로 보면 리팩터링 단계에서는 성능이 느려질 수도 있다. 하지만 최적화 단계에서 코드를 튜닝하기 훨씬 쉬워지기 때문에 결국 더 빠른 소프트웨어를 얻게 된다.

 

2.9 리팩터링의 유래

'리팩터링'이란 용어의 정확한 유래는 찾을 수 없었다. 실력 있는 프로그래머는 항상 자신의 코드를 정리하는 데 어느 정도의 시간을 할애해왔다. 복잡하고 지저분한 코드보다는 깔끔한 코드가 수정하기 쉽고, 처음부터 깔끔하게 작성하는 경우는 거의 없다는 것을 경험을 통해 알기 때문이다.

켄트와 함께 프로젝트를 진행하면서 그가 리팩터링하는 모습을 지켜볼 수 있었고, 그래서 리팩터링할 때와 안 할 때의 생산성과 품질의 차이를 확실히 경험할 수 있었다. 하지만 아쉽게도 당시에는 실무 프로그래머가 읽을 책이 하나도 없었고, 리팩터링 전문가 중에서 그런 책을 쓰겠다는 사람도 없었다. 그래서 내가 그들의 도움을 받아서 이 책의 초판을 쓰게 된 것이다.

다행히 리팩터링이란 개념을 업계에서 잘 받아들였다. 이 책도 꽤 팔렸고 거의 모든 프로그래머가 리팩터링이란 용어를 쓰게 되었다. 하지만 이처럼 대중화되면서 코드를 재구성하는 모든 작업을 가리키는 느슨한 의미로 사용하는 사람이 많아졌다. 어쨌든 리팩터링은 주류 개발 기법으로 자리 잡았다.

 

2.10 리팩터링 자동화

리팩터링과 관련하여 지난 수십 년 사이에 일어난 가장 큰 변화는 자동 리팩터링을 지원하는 도구가 등장한 것이다. 예를 들어 인텔리제이 IDEA, 이클립스에서 메서드 이름을 바꾸는 작업을 메뉴에서 클릭하는 것만으로 처리할 수 있다. 실제 리팩터링은 나 대신 개발 도구가 처리해주며, 따로 테스트할 필요가 없을 정도로 안정적이다.

대부분의 자동 리팩터링이 믿을만하더라도 중간에 꼬인 부분이 없는지 이따금 테스트로 확인하는 것이 바람직하다. 나는 주로 자동 리팩터링과 수동 리팩터링을 함께 사용한다. 그래서 테스트도 충분히 거친다.

 

2.11 더 알고 싶다면

이 책은 특정 리팩터링 기법이 궁금할 때 찾아볼 수 있는 레퍼런스를 제공하는 데 주력했다. 리팩터링 연습에 주력한 책을 원한다면 윌리엄 웨이크가 쓴 '리팩터링 워크북'을 추천한다.

리팩터링 개척자 중 많은 이가 소프트웨어 패턴 커뮤니티에서도 활발히 활동하고 있다. 조슈야 케리에프스카는 '패턴을 활용한 리팩터링' 을 통해 두 분야를 잘 접목하고 있다. 이 책은 소프트웨어 패턴 분야에 지대한 영향을 준 '디자인 패턴' 책에서 가장 핵심적인 패턴을 골라, 코드를 패턴대로 재구성하기 위해 리팩터링하는 방법을 다룬다.

특정 분야에 특화된 리팩터링 책
- 리팩토링 데이터베이스 (스캇 엠블러, 프라모드 사달게)
- 리팩토링 HTML (엘리엇 러스티 해롤드)

특정 언어에 특화된 리팩터링 책
- Refactoring: Ruby Edition (제이 필즈, 셰인 하비)

최신 자료를 보고 싶다면 이 책의 깃허브 지원 페이지와 리팩터링 웹사이트를 참고하기 바란다.
- https://github.com/WegraLee/Refactoring
- https://refactoring.com 

댓글