ILoveCoffee, ILoveJava

코드의 웃음을 빼앗아가는 리펑토링(Refuctoring)

jeeyong 2007. 12. 14. 14:48

 우선 아래글은 마소 잡지의 컬럼에 올라왔던 글을 인용한것이라는 것을 우선 밝힌다.

 내가 생각하는 좋은 코딩은 과연 다른사람이 내가 짜놓은 소스를 보고 이해가 쉽게 되는가 이다. 소스라는 것이 스파게티의 면처럼 복잡할 수 밖에 없겠지만 그렇다고 엉킨 면을 더욱 엉켜놓을 필요는 없지 않은가....

 특히 프레임웍까지 사용하면서 이런 리펑토링 코드까지 감당해야 한다면.. 간단한 커스터 마이징을 위해 소스의 분석을 하다가 하루가 다지나가고 결국 야근을 야기시키는 개발자들 사이에선 없어져야할 부분이 아닌가싶다. 밑의 고맨의 6가지 리펑토링을 가지고 구체적인 예를 들어 설명하고 있다. 사실 6가지 리펑토링 중 필자가 가장 와닿는 리펑토링은 ‘보물찾기(Treasure Hunt)’ 이다. 사실 이런 코딩 기법을 기교 마냥 자랑스럽게 어디서든 사용하는 분들이 많을 것으로 생각된다. 리펑토링을 모르는 어제까지 나 조차도 이런식의 코딩이 잘못된 코딩이라는 확신을 가지지 못했기 때문이다.


"고맨의 6가지 리펑토링 당신은 아직도 자랑스럽게 사용하고 있는가?"

고맨의 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마리 원숭이가 더)


원문 : http://www.waterfall2006.com/gorman.html

마소 기사 : http://imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=29942