내가 일하고 있는 팀에서 동료끼리 서로의 코드를 검토하는 코드리뷰(code review)는 필수적인 항목이다. 코드를 제출(submit)하기 위해서는 누구라도 최소한 한 명 이상의 검토를 받아야 한다. 그렇게 하지 않으면 경우에 따라 문책을 받기도 한다. 특히 새로 입사했거나 경험이 많지 않은 프로그래머는 시스템 이해가 부족한 탓에 코드리뷰를 통해서 검토와 수정을 반복하는 일이 적지 않다. 이러한 맥락에서 나는 얼마 전에 새로 입사한 프로그래머가 작성한 코드를 검토하였다.
임백준
baekjun.lim@gmail.com·삼성SDS와 루슨트테크놀러지사에서 근무하기도 했다. 지금은 월스트리트의 한 금융회사에서 일하고 있다. 저서로는 <행복한 프로그래밍>, <누워서 읽는 알고리즘>, <임백준의 소프트웨어 산책> 등이 있다. 현재 뉴저지주에서 아내 그리고 예쁜 두 딸과 살고 있다.
코드를 작성한 사람은 다양한 회사를 거치며 일을 해온 경험이 많은 프로그래머였다. 그래서 그의 코드를 검토할 때 내가 중점을 둔 부분은 ‘기본기’가 아니라, 시스템의 다른 부분과 유기적인 융합을 이루고 있는가 여부였다. 하지만 그의 코드는 화면에 띄우는 순간 강력한 ‘냄새’를 풍겼다. 뭔가 이건 아니라는 느낌이었다. 그는 이미 존재하는 클래스 내부의 스무 줄 가량의 메소드를 전체적으로 복사해 바로 아래에 붙여 넣었다.
그리고 메소드가 받아들이는 인수(parameter)에 불리언 값을 하나 추가한 다음, 메소드가 담고 있는 알고리즘에서 두 줄을 수정했다. 전형적인 중복 코드의 사례이다.
너무나 노골적인 중복을 보고 참을 수 없었던 나는 문제를 제기했다. 하지만 그는 잘 납득이 가지 않는 반론을 제시하며 코드를 변호했다. 그와 약간의 논쟁을 벌이던 나는 그의 감정을 상하게 하고 싶지 않았기에, 그리고 그의 코드가 최소한 제대로 동작을 하고 있었기에 적당한 지점에서 뒤로 물러섰다. 그가 프로젝트에 합류한 지는 불과 두세 달 밖에 지나지 않았으므로 시간이 더 필요할 것이라고 생각했다.
코드의 응집력 저하와 오작동을 야기
얼마 뒤에 그가 작성한 코드에서 버그가 발견되었다. 화면에 입력된 숫자의 소수점을 특정한 규칙에 따라서 포맷(format)하는 알고리즘이었다. 이번에도 내가 그의 코드를 검토하게 되었다. 이전의 경험 때문에 혹시나 하고 열어 본 그의 코드는 역시나 불필요한 군살이 잔뜩 끼어있었다. 관련된 요구사항은 매우 간단한데도 그의 코드가 수행하는 일은 이해하기 어려운 복잡한 계산으로 가득 차 있었다.
이번에는 이것이 단순한 스타일이 아니라 버그의 문제였기 때문에 나는 그의 기분과 상관없이 해결책을 제시하는 것을 마다하지 않았다. 그는 자존심을 내세우며 자신의 코드를 변호했지만, 실제로 존재하는 버그 때문에 결국 코드를 재작성하는 일에 동의했다.
그의 코드가 시스템의 엔트로피를 상승시키는 사례는 이뿐만이 아니었다. 예컨대 그가 새로운 소프트웨어 버전에 포함되는 기능을 구현하기 위해 이미 존재하는 객체의 이곳저곳을 수정해놓은 것을 보고 나를 포함한 몇몇 고참 프로그래머는 매우 당황했다.
그는 어떤 객체 내부에 그 객체와 상관이 없는 변수와 메소드를 도입함으로써 코드의 응집력(coherence)을 떨어뜨리고, 순수한 데이터 객체 내부에 어떤 동작(operation)을 수행하는 메소드를 삽입시킴으로써 객체가 사용되는 범위(scope)와 성격을 흔들어놓았다. 특정한 상태를 유지하지 않는 중립적인(stateless) 객체에게 인스턴스(instance) 변수를 도입함으로써 기존 코드의 동작을 불안하게 만들었다. 한마디로 그의 코드는 기존 시스템이 설계되어 있는 방식에 아랑곳 하지 않고 자기가 있고 싶은 장소에 마음대로 존재하는 것이다.
이 글을 읽으면서 나는 아니야, 라고 생각하는 사람이 있을지 모르지만, 사실 프로그램을 이와 같은 방식으로 작성하는 사람은 의외로 많다. 프로그래밍에 대한 이해가 깊고 성격이 신중한 소수의 사람을 제외하면 대부분의 프로그래머는(나 자신을 포함하여) 사실상 매일 소프트웨어의 엔트로피를 감소시키기보다 증가시킨다. 다만 우리는 필요한 기능을 구현하기 위해서 필요한 엔트로피의 양을 최소한으로 만들기 위해서 노력할 뿐이다.
프로그래밍 세계에서 흔히 목격되는 이러한 문제를 지적하려고 지난 2006년 4월 1일 뉴욕 나이아가라 폭포에서 열린 ‘워터폴 2006(Water fall)’ 컨퍼런스에서 제이슨 고맨(Jason Gorman)은 ‘리펑토링(refunctoring)’이라는 제목의 연설을 수행했다.
고맨의 6가지 리펑토링
고맨이 말하길 소프트웨어 코드는 자신을 웃게 만드는 몇 가지 요소를 가지고 있다고 한다. 그가 ‘스마일’이라고 이름붙인 요소들에는 변수, 메소드, 객체의 이름을 상식에 근거해 붙이기, 자체로는 응집력이 있지만 외부적으로는 느슨하게 결합된 모듈, 우아한 추상화(elegant abstra ctions), 중복의 부재(lack of redundancy), 애플리케이션 도메인과의 밀접한 관련성 등이 포함된다. 스마일이란 좋은 프로그램을 만들기 위한 바람직한 스타일을 의미하는 것이다.
하지만 리펑토링은 스마일과 정반대의 의미를 갖는다. 그것은 “잘 설계된 코드에 작고 가역적인(rever sible) 변화를 연속적으로 도입해서 자기 자신을 제외한 어느 누구도 그것을 관리할 수 없도록 만드는 과정”을 의미한다. 고맨이 소개한 리펑토링의 사례는 여러 가지가 있는데 그 중에서 사례 1번은 ‘피그 라틴(Pig Latin)’이라고 불리는 일종의 제 멋대로 이름붙이기이다. 예를 들어서 다음 코드를 보도록 하자.
class Account {
private float balance = 0;
void deposit(float amount) {
balance += amount;
}
public void withdraw(float amount) {
balance -= amount;
}
public float getBalance() {
return balance;
}
}
위의 코드가 수행하는 일은 누가 보아도 그 의미가 명백하게 드러난다. 변수와 메소드의 이름이 상식에 기초하고 있기 때문이다. 하지만 다음 코드를 보자.
class Accountway {
private oatflay alancebay = 0;
public void epositday(oatflay amountw ay) {
alancebay += amountw ay;
}
public void ithdrawway(oatflay amountw ay) {
alancebay -= amountw ay;
}
public oatfflay etBalancegay() {
return alancebay;
}
}
이 예는 클래스, 메소드, 변수의 이름을 일부러 엉터리로 만들어 놓았지만 실제로 이에 못지않게 터무니없는 변수 이름을 사용하는 코드를 발견하는 것은 어려운 일이 아니다. 객체, 메소드, 변수의 이름을 상식에 맞게 붙이는 것과 그렇지 않은 것은 코드의 관리라는 측면에서 상상외로 엄청난 차이를 낳는다. 하지만 프로그래밍을 할 때 변수의 이름을 상식에 맞게 붙이는 것은 충분조건이 아니다. 그것은 최소한의 필요조건이다.
이름이 누구나 쉽게 생각할 수 있는 구체적인 대상을 지칭할 때에는 상식을 따르는 것이 쉽다. 하지만 프로그래밍에서는 이름이 고도의 추상성을 요구하는 경우도 많다. 예컨대 객체의 이름 하나를 정하기 위해서 몇날 며칠을 고민해야 할 때도 있다. 그런 고민을 경험한 적이 없다면 아마 리펑토링을 저지르고 있는 사람일 가능성이 높다.
고맨이 이야기하는 리펑토링의 사례 2번은 ‘보물찾기(Treasure Hunt)’이다. 이것은 코드가 간단하고 자체적으로 완결된 방식으로 구성되는 것이 아니라, 주로 다른 코드에 대한 참조로 이루어지는 것을 의미한다. 객체지향적인 설계를 수행하여 상속(inheritance), 위임(delegation), 프록시(proxy)와 같은 개념을 적용하다보면 어느 객체가 수행하는 간단한 업무가 실제로 어디에서 이루어지는지 알기 어려운 때가 있다. 보물찾기는 그런 복잡한 상황이 불필요하게 연출되는 경우를 지적한다. 두 줄로 이루어진 다음 메소드를 보자.
public void executeTransaction() {
payer.withdraw(amount);
payee.deposit(amount);
}
리펑토링에 일가견이 있는 프로그래머는 이렇게 간단하고 명료한 코드조차 다음과 같이 복잡한 미로 안으로 밀어 넣는다. 혹시라도 버그가 발생했을 때 도대체 누가 무슨 일을 하고 있는 건지 알아채기도 쉽지 않다.
public void executeTransaction() {
this.callExecuteTransaction();
}
private void callExecuteTransaction() {
helper.doExecute();
}
.........
class TransactionExecutionHelper extends ExecutionHelper {
public void doExecute() {
base.doExecute(this);
}
public void execute() {
ExecuteTxCommand command = CommandFactory.createCommand();
command.execute();
// and so on...
}
}
...........
abstract class ExecutionHelper {
protected void doExecute(ExecutionHelper helper) {
helper.execute();
}
public void execute();
}
}
리펑토링의 실력자는 코드를 이런 식으로 작성해놓고 멋진 객체지향 기법이라고 강변한다. 이 코드가 추상 클래스(abstract class)나 도우미(Helper) 클래스와 관련해서 객체지향 기법을 사용하고 있는 것은 사실이다. 하지만 왜 객체지향인가? 객체지향의 근본사상은 코드의 관리를 쉽게 하자는 것이지 추상적인 계층을 도입해서 코드의 이해를 어렵게 하자는 것이 아니다. 객체지향은 민감한 수술과 같아서 잘 하면 큰 병을 낫게 하지만 잘못하면 오히려 몸을 망치기 십상이다. 간단하게 두 줄로 구현할 수 있고, 그 자체로 아무 문제가 없는 코드를 객체지향이라는 명목으로 복잡하게 꼬아놓는 것은 현명한 태도가 아니다.
고맨이 이야기하는 리펑토링의 세 번째 사례는 ‘자기만의 모델링 언어(Unique Modeling Language)’를 사용하는 것이다. 이것은 객체의 계층구조나 사용자 동작을 표현하기 위해서 UML이라는 보편적인 도구가 존재함에도 불구하고, 마이크로소프트 워드나 파워포인트의 그림그리기 기능으로 자기 마음대로 만들어낸 도형을 이용하는 사람이나 상황을 의미한다. 그렇게 임의로 작성된 도형이 당장은 어떻게 설명이 될지 몰라도, 시간이 조금만 흐르면 자기 자신을 포함한 어느 누구도 이해할 수 없는 이집트 상형문자가 되고 만다. 있으나마나 한 문서가 되는 것이다.
리펑토링의 네 번째 사례는 ‘너무나 명백한 사실을 상세히 설명하기(Stating Bleeding Obvious)’이다. 다음과 같은 세 줄짜리 코드가 존재한다고 하자. 프로그래밍에 입문한지 한 시간이 지난 사람이라도 쉽게 이해할 수 있는 간단한 코드이다.
for (int number = 1; number <= 12; number++) {
System. out.println(number + " squared is " + (number * number));
}
아무렇지도 않게 리펑토링을 수행하는 프로그래머는 이 코드를 다음과 같이 길쭉한 코드로 변형시킨다.
/*
Author: 제이슨 고맨(Jason Gorman)
Date & Time: 13/9/05 12:32:06
Revision History:
13/9/05 12:33:14 실수로 한 줄을 지웠다가 그것을 다시 복구함
Comment Body:
number라고 불리는 정수형 변수를 선언하고 초기값 1을 설정함. 그리고
number의 값을 1씩 증가시키면서 똑같은 코드 블록을 12번 수행한다.
*/
for (int number = 1; number <= 12; number++) {
/*
number의 제곱이 무엇인지 나타내는 다음 텍스트를 출력하기 위해서
System의 아웃풋 스트림을 얻는다:
number + " squared is " + (number * number)
*/
System.out.println(number + " squared is " + (number * number));
// 컴파일러에게 루프가 끝나는 지점을 알려주기 위해서 { 괄호를 사용하라.
}
믿기 힘들겠지만 나는 프로그램을 이런 식으로 작성하는 사람을 실제로 본 적이 있다. 그것도 여러 번을 보았다. 정식 직원이 아니라 1년 단위로 계약을 하는 컨설턴트 중에서 이런 사람을 본 적이 많은데, 그들이 작성한 프로그램은 겉으로 보기에 매우 깔끔하다. 그런데 나중에 요구사항이 변경돼 코드를 조금만 수정하면 사방이 삐꺽거리며 무너져 내리는 특징을 가지고 있다.
그들은 요구사항에 대한 깊은 이해와 신중한 설계가 아니라 프로그램의 겉모습을 치장하는데 많은 시간을 들였기 때문이다. 내용보다는 포장을, 프로그램이 수행하는 일보다는 소스 코드가 정렬되는 방식에 심혈을 기울임으로써 높은 보수를 받으며 리펑토링을 수행한 것이다.
리펑토링의 다섯 번째 사례는 ‘비오는 날을 위한 시나리오(Rainy Day Scenario)’이다. 코드의 핵심 알고리즘을 한 번 더 살펴보거나 테스트하면 좋을 시간에 일어나지도 않을 가상의 상황을 위해서 코드를 작성하는 것이다. 필요하지도 않은 코드를 열심히 작성하는 것은 리펑토링을 즐겨 수행하는 프로그래머들이 공통적인 특징이기도 하다.
class SpareCode {
private int spareInteger;
private String luckyString;
private bool youNeverKnow ;
public void spareLogic() {
spareInteger = 1;
if( youNeverKnow ) {
spareInteger++;
}
System.out.println(luckyString);
}
}
처음에 예로 들었던 사례가 정확히 이 경우에 해당한다. 일정한 규칙에 따라 소수점을 포맷(format)하는 알고리즘이 불필요하게 복잡했던 이유는 바로 그가 매우 드물게 발생하는(내가 보기에는 영원히 일어나지 않을) 상황을 처리하기 위한 코드를 포함시켰기 때문이다. 그 자신은 내심 남들이 생각하지 못한 상황까지 처리하는 ‘꼼꼼한’ 코드를 작성한다고 착각했는지 모르지만, 그것은 전혀 필요하지 않을 뿐만 아니라 알고리즘 전체를 복잡하게 만들어 결국 버그까지 유발하였다. 하지만 애초부터 불필요한 상황을 염려하지 않았더라면 코드는 훨씬 간단하게 작성되었을 것이고 버그의 가능성도 크게 줄었을 것이었다.
리펑토링과 관련한 사례의 여섯 번째는 ‘모듈의 중력장(Module Gravity Well)’이다. 끝에 있는 ‘well’을 ‘우물’로 번역해야 하는지 모르겠지만 아무튼 고맨이 이 사례를 놓고 말하려는 것은 오만가지 잡동사니를 전부 한 곳으로 끌어당기는 엉터리 같은 객체의 존재이다.
내가 보기에 리펑토링의 사례 중에서 가장 주목해야할 부분은 바로 이 여섯 번째 ‘모듈의 중력장’이다. 이러한 리펑토링은 프로그래머들이 가장 흔히 저지르는 실수에 속하기 때문이다. 코드의 응집력이란 사람의 면역성과 같아서 응집력이 높은 코드일수록 버그를 양산하는 일이 드물다. 이 여섯 번째 리펑토링은 코드의 응집력을 결정적으로 떨어뜨리기 때문에 코드를 가장 아프게 만드는 사례이다.
남이 아닌 우리 자신의 이야기
리펑토링에 대한 고맨의 글과 프레젠테이션 파일은 워터폴 2006의 홈페이지(
http://www.waterfall2006.com/ gorman.html)에서 볼 수 있다. 리펑토링에 대해서 더 자세한 내용을 보고 싶은 사람은 직접 살펴볼 것을 권한다.
다음은 고맨이 ‘모듈의 중력장’을 설명하면서 예로 든 어느 객체에 포함된 메소드이다. 응집력 제로의 수준을 보여주기 위해서 실제적인 사례가 아니라 우스운 내용을 나열하고 있다. 하지만 오늘 자신이 설계하는 객체에 포함된 메소드를 다시 한 번 살펴보기 바란다. 어쩌면 이 예에 포함시켜도 이상하지 않을 정도로 엉뚱한 메소드를 만드는 리펑토링을 수행하고 있음을 깨닫게 될 지도 모르는 일이다. 리펑토링은 남 이야기가 아니라 우리 자신의 이야기이다.
doStuff() (뭔가 해라)
openWindows() (창문을 열어)
connectToDatabase() (데이터베이스에 연결)
blah() (어쩌고)
etc() (기타 등등)
kitchenSink() (부엌 싱크대)
everyMan() (모든 남자)
andHisDog() (그리고 그의 개)
inForAPenny() (전부 1원에 팔아요)
anyPortInAStorm() (모든 항구에 폭풍우가)
makeHayWhileTheSunShines() (기회를 놓치지 말라고)
aRollingStoneGathersNoMoss() (구르는 돌에는 이끼가 끼지 않아)
didYouSeeDoctorWhoLastNight() (어제 밤에 닥터 후를 보았나요)
iAmTheWalrus() (나는 바다코끼리라고)
areWeThereYet() (다 왔니)
method() (메쏘드)
madness() (광기)
wakaJawaka() (와카자와카)
aHooliHayliHah() (어훌리할리하)
andAPartridgeInAPairTree() (배나무 안에 메추리가)
twelveMonkeys() (12마리 원숭이)
twelveMoreMonkeys() (12마리 원숭이가 더)