336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
어떤 부분을 수정했는데 잘못된 부분이 있습니다. 이때 잘못되었다는 피드백을 즉시 받을 수 있다면 가장 이상적인 환경일 것입니다. 지금 까지 얘기했던 테스트 환경과 CI서버를 잘 구축해놓으면 이런 이상적인 환경을 구축할 수 있고 현실화 시킬 수도 있습니다.

하지만 실전에서는 또 다른 어려운 점이 많이 생깁니다. 개인적인 경험을 되돌아보면 어떤 부분을 리팩토링 하려고 했더니 그 부분이 핵심 기능 중 하나라서 리팩토링에 따른 영향이 무척 컸던 경우가 있습니다. 영향 받는 부분을 검토하고 검증 방법이 있나 살펴보았습니다. 그런데 테스트도 없고 그 외 검증할 방법이 마땅치 않았습니다. 리팩토링에 투자할 수 있는 시간은 한정되어 있고 고민에 빠져들었습니다. 이런 때는 어떻게 해야 할까요?

이럴 때 바로 영향력 제어가 필요합니다. 영향력 제어는 리팩토링 즉 변경에 따라 발생하는 영향력을 현재 상황에서 다룰 수 있을 정도로 축소시키려는 노력입니다. 이를 위한 여러 가지 방법이 있겠지만 이어지는 글에서 본인이 실무에서 사용하여 그 효과를 느낀 방법 두 가지를 소개하려고 합니다.

1. 위임을 이용한 영향력 제거

지난 단위 테스트에 대한 글에서 인터페이스가 변경되지 않는 경우 영향력을  제거하는 방법을 소개했습니다. 하지만 인터페이스가 변경되는 경우에는 이 방법을 사용할 수 없습니다. 결국 영향 받는 부분 모두를 검증해야 하는데 현실적인 여건상 쉽지 않을 때가 많습니다. 이럴 때는 다소의 레거시(Legacy)를 인정하며 영향력 제거를 꾀하는 '위임'을 이용하여 리팩토링을 진행할 수 있습니다.

예전 리팩토링 프로젝트를 할 당시 프로젝트 전역에 걸쳐 사용되는 핵심 도메인 모델의 한 메서드를 리팩토링 해야 하는 경우가 있었습니다. 코드 내부뿐 아니라 메서드의 인터페이스도 변경해야 했습니다. 그런데 인터페이스를 수정하려고 보니 수많은 JSP에서 해당 인터페이스를 사용하고 있었습니다. 안타깝게도 당시 JSP에 대한 자동화 된 검증방법을 준비하지 못한 상황이었습니다. 그래서 수정을 하더라도 어마어마한 범위의 확인을 어떻게 할지에 대한 대책이 필요했습니다. 어떻게 하면 시간을 아끼면서 리팩토링도 할 수 있을까? 저는 많은 고민을 했고 결국 위임을 사용했습니다. 세부적인 방법은 아래와 같습니다.
 
1) 리팩토링 했을 때 최종적으로 만들고 싶은 형태의 새로운 메서드를 개발하고 그에 따른 단위 테스트를 작성한다. 이때 주의할 점은 예전 메서드의 기능을 100% 지원할 수 있는 형태로 만들어야 한다는 점이다. 즉 하위 호환성을 완벽하게 제공해야 한다. 또한 메서드 인터페이스가 기존과는 달라야 한다는 점에 유의해야 한다. 만약 인터페이스가 같다면 단위 테스트를 이용하여 수정 전/후의 동일성을 보장하여 영향력을 제어할 수 있기 때문에 굳이 위임을 사용할 필요가 없다.

2) 예전 메서드에 대한 단위 테스트를 작성한다. 이후 수정을 할 때 해당 테스트를 통해 수정 전후의 동일성이 보장될 수 있게 한다.

3) 예전 메서드의 내부 코드를 모두 지운다. 그리고 예전에 하던 것처럼 동작을 직접 구현하는 것이 아니라 1번에서 만든 새로운 메서드에게 구현을 위임하는 방법으로 코드를 작성한다. 이것이 가능한 이유는 1번에서 하위 호환성을 제공했기 때문이다.

4) 2번에서 작성한 테스트 코드를 이용하여 동일성을 보장한다. 만약 테스트가 잘 동작한다면 예전과 같게 동작하는 것이 보장된 것이다.

5) 추천하지 않는다는 의미의 @deprecated 주석을 예전 메서드에 달아 사용자들이 예전 메서드를 더는 사용하지 않도록 안내한다. 예전 메서드는 참조가 많아 부득불 하게 유지하는 것이며 가능한 한 빨리 제거하는 것이 좋다.

6) 예전 메서드에 @see 주석을 달아 대신 사용 가능한 새로운 메서드를 알려준다. 추가로 주석 부분에 새롭게 리팩토링 한 메서드가 필요한 배경을 설명하고 하위 호환성을 제공한다는 점을 밝혀두면 이후 개발자들이 더욱 신뢰성을 가지고 새롭게 리팩토링 한 메서드를 사용할 수 있기 때문에 좋다.


그림#1. 위임의 예

저는 위와 같이 작업하여 제가 리팩토링 프로젝트에서 만났던 비슷한 유형의 문제를 모두 해결할 수 있었습니다. JSP에서 해당 인터페이스를 호출하던 부분은 예전 메서드가 그대로 존재하며 '예전과 동일한 일'을 해주기 때문에 아무런 영향을 받지 않게 되었습니다. 새로운 메서드가 추가된 것 빼고는 예전과 모든 것이 동일했기 때문에 더 이상 검증에 대해 신경 쓰지 않아도 됐고 따라서 특별히 QA를 받지 않아도 됐습니다. 또한 @deprecated와 @see를 이용한 안내로 말미암아 새롭게 코드를 작성할 때는 리팩토링 된 메서드에 대한 안내를 좀 더 편하게 받을 수 있었습니다.

위임은 현실과 이상의 괴리를 채워주는 훌륭한 방법이라고 생각합니다. 즉 리팩토링을 하고 싶지만 완벽하게 할 수는 없을 때 한편으로 이상적인 코드를 추구하면서 잠시 예전의 코드도 인정하는 방법입니다. 이렇게 되면 인터페이스 변경에 따른 전반적인 코드수정을 하지 않아도 되며 리팩토링에 따른 영향력을 거의 무의미하게 만들어 검증 또한 쉬워집니다.

2. 인터페이스 변경에 따른 영향력 확산 고립

때로 인터페이스를 변경했을 때 그에 따른 영향의 파급이 계속해서 커지는 경우가 있습니다. 예를 들어 아래 그림은 DAO의 반환 타입을 기존 Map에서 모델로 바꾸었는데 BO(Business Object)에서는 DAO에서 받은 Map을 그대로 반환하는 예입니다. 그림에서 볼 수 있듯이 문제는 해당 BO가 가지고 있는 영향력이 넓다는 점입니다. 따라서 BO의 인터페이스를 수정하게 되면 많은 부분에 영향이 미치고 검증의 부담으로 다가옵니다.


그림#2. 영향력 고립의 예

이럴 때는 영향력의 확산을 막기 위해 위 그림의 빨간 선 부분에서 영향력을 고립시킬 수 있습니다. 즉 DAO는 리팩토링을 해서 Map대신 Model을 반환하게 하지만 BO에서는 예전과는 달리 DAO에서 Model을 받더라도 예전처럼 Map으로 바꿔 반환함으로써 외부에서는 변화를 모르게 하는 방법입니다.

여기서 중요한 점은 BO에 대해서 단위 테스트에 얘기했던 단위 테스트를 이용한 동일성 보장을 해주어야 한다는 점입니다. 기존에 쓴 글에서 필요한 부분을 일부 발췌했습니다.

이어 단위 테스트는 인터페이스 변경 없이 내부코드만 변경이 있을 경우 ... 내부코드 변경의 경우는 영향 받는 곳의 코드변경은 없다는 점 ... 이 점을 통해 알 수 있는 사실은 만약 우리가 변경되는 내부코드의 변경 전/후의 동일성을 확실히 보장한다면 내부코드 수정으로 말미암아 영향 받는 부분에 대해서는 더는 신경 쓰지 않아도 된다는 것 (생략)

이 방법을 적용했을 때 BO의 내부코드는 변경 되었지만 동작은 예전과 동일하다라는 것이 보장되기 때문에 해당 BO의 변화에 영향 받는 넓은 범위의 동작을 보장할 수 있게 됩니다. 영향력 고립시키기는 부분적으로 리팩토링을 할 때 원치 않게 영향력이 확산되는 것을 막아줍니다.

WRITTEN BY
차민창
르세상스 엔지니어가 사회를 이끌어나가는 상상을 하며!

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
인터페이스를 수정하는 경우 제게 있어 가장 인상 깊었던 부분은 컴파일러가 영향력 검토에 있어 매우 중요한 역할을 한다는 것이었습니다. 예를 들어 특정 메서드의 인자를 변경해야 한다고 가정해봅시다. 해당 메서드의 인자를 변경하고 컴파일을 수행합니다. 이때 만약 컴파일 오류가 발생한다면 해당 메서드를 사용하고 있는 곳이 있다는 뜻이며 변경에 따라 영향을 받는 곳이 있다는 증거입니다. 실제로 컴파일은 이클립스와 같은 IDE를 통해 실시간으로 이뤄지기 때문에 개발자는 매우 빠르고 정확하게 영향력 검토를 할 수 있습니다.

하지만 이 같은 컴파일러의 지원에도 영향력 검토가 쉽지 않은 경우가 있습니다. 대표적인 경우 중 하나는 클래스 경로 정보를 갖고 있는 설정 파일입니다. 최근 많이 사용되는 여러 프레임웍에서는 설정 파일에 클래스 경로를 적게 하고 실행 시에 그 정보를 이용하여 클래스를 동적으로 생성합니다. 이런 경우 해당 클래스의 이름이나 위치가 변경되었을 때 컴파일러를 통해서는 영향력을 확인할 수 없게 됩니다. 또 한 가지 대표적인 경우는 JSP의 표현식(Expression Language)에서 특정 클래스의 메서드를 참조하는 경우입니다. 이 경우에도 역시 컴파일러를 통해서는 영향력을 확인할 수 없습니다. 따라서 이런 부분에 대해 자동으로 영향력 검토를 해주는 도구가 없는 경우, 설정 파일이나 JSP의 표현식과 같은 영역에 대해서는 개발자가 직접 검색을 하는 등의 방법을 통해 영향력 검토를 수행해야 합니다. 이런 경우 컴파일러만을 이용하여 영향력 검토를 할 경우보다 훨씬 더 큰 비용이 필요해지며 사람이 개입함으로 인해 실수가 생길 확률도 높아지게 됩니다. 따라서 컴파일러를 이용한 영향력 검토는 한계가 있다고 볼 수 있습니다.

한편 컴파일러를 검증방법으로써도 활용할 수 있습니다. 마틴 파울러의 리팩토링을 보면 메서드의 이름을 이해하기 좋게 바꾸거나 세부적인 코드의 변경 없이 특정 메서드를 다른 클래스로 옮기거나 하는 것도 리팩토링으로 분류하고 있습니다. 저 역시 지난 프로젝트에서 이러한 유형의 리팩토링을 많이 했습니다. 특히 첫 번째 리팩토링 프로젝트에서 코드를 수정하기 보다는 클래스나 메서드를 옮기고 이름을 바꾸는 등의 일을 많이 했습니다. 당시 저는 리팩토링 후 코드가 전과 같이 문제 없는 상태로 있다는 것을 어떻게 확신 할 수 있는지에 대해 부단히 고민했습니다. 그리고 위와 같이 클래스를 옮기는 등에 리팩토링을 가만히 살펴보았더니 이들에 대한 검증은 컴파일러가 맡고 있었습니다. 예전에는 컴파일러는 자바코드를 JVM이 실행 가능한 바이트 코드(Byte Code)로 변경해주는 도구라고만 생각했었습니다. 그러나 위 경험을 통해 컴파일러의 검증기능에 주목하기 시작했고 이어 컴파일러의 검증범위에 대해 생각해보게 되었습니다. 당시 작업을 하며 가장 어려울 때는 전에 얘기한 설정파일이나 JSP 표현식이였습니다. 이 부분은 컴파일러에서 검증하지 못했습니다. 결국 컴파일러의 검증범위 밖에 있는 것들이 문제였고 이런 부분들도 컴파일러에서 검증할 수 있으면 얼마나 좋을까라는 생각을 했습니다. 즉 컴파일러의 검증범위가 좀더 넓어졌으면 좋겠다고 생각한 것입니다.

이 때문인지 몰라도 Effective Java 2판의 저자인 Joshua Bloch는 그의 책을 통해 무엇인가 잘못 되었을 때 컴파일러가 알아차릴 수 있게 코드를 작성하는 것을 강조합니다. 저는 컴파일러가 검증방법으로써 이점이 있지만 한계 또한 있다는 점을 이미 언급했습니다. 하지만 컴파일러 검증을 이용하면 테스트 보다 더 이른 시점에 검증이 가능하다는 점과 단위 테스트와 검증부분이 중복 된다 하더라도 이중검증을 할 수 있다라는 점 때문에 Effective Java 2판의 내용과 같이 컴파일러의 검증범위를 넓히려는 노력은 매우 합리적이라 봅니다.

아래는 Effective Java 2판에서 저자가 강조했던 내용 중 실무에서 자주 발생하는 사례를 정리한 것으로써 컴파일러의 검증범위를 넓히기 위해 활용될 수 있습니다.

1. Generic 활용

Generic을 사용하지 않는 코드
List myIntList = new LinkedList();
myIntList.add(new Integer(0));
String x = (String) myIntList.iterator().next();

위 예제는 Generic을 사용하지 않고 List를 사용하는 예제입니다. 위 코드는 정상적으로 컴파일이 되지만 실행시점에 ClassCastingException이 발생합니다. 왜냐하면 두 번째 줄에서 Integer객체를 생성해 넣었는데, 세 번째 줄에서는 해당 객체를 String 사용하려고 했기 때문입니다. 여기서 주목해야 할 점은 오류를 컴파일 시에는 발견할 수 없었다는 점입니다. 하지만 자바 1.5에서 도입 된 Generic로 말미암아 컴파일 시점에 위 오류를 발견할 수 있게 되었습니다.

Generic을 활용한 코드
List<Integer> myIntList = new LinkedList()<Integer>;
myIntList.add(new Integer(0));
String x = myIntList.iterator().next();

위 예제는 Generic을 사용하여 바뀐 코드입니다. 위 예제는 첫 번째 예제와는 달리 세 번째 줄에서 컴파일 오류가 발생합니다. 왜냐하면 해당 List는 Integer로 사용하기로 결정되었기 때문에 String으로 사용할 수 없기 때문입니다. 위와 같이 Generic을 이용하면 컴파일러에서 더 많은 검증을 할 수 있습니다.

2. Enum 활용

Enum을 사용하지 않는 코드
public boolean isModifable(String osType) {
... // 유효한 osType은 Linux, Window이다.
}

위 코드에서 볼 수 있듯이 예전에는 누군가가 실수하여 type에 "Computer"라는 문자열이 잘못 들어와도 컴파일러가 검증할 방법이 없었습니다. 예를 들어 허용되지 않는 문자열인 Computer가 데이터베이스에 저장되어도 그것을 특별히 검사하는 코드가 없다면 문제를 발견하기 어려웠습니다. 하지만 Enum에 도입으로 컴파일 시점에 오류를 발견할 수 있게 되었습니다.

Enum을 활용한 코드
public boolean isModifable(OSType osType) {
...
}

public enum OSType {
LINUX, WINDOW
}
        
위와 같이 Enum에 사용으로 OSType에 미리 정의되지 않은 값은 넘길 수 없게 되었습니다. 만약 LINUX나 WINDOW가 아닌 다른 타입을 넘기려고 하면 컴파일 오류가 발생할 것입니다.  

3. Switch 문을 Constant-specific methods 로 변경

Constant-specific methods를 사용하지 않는 코드
public enum Operation {
  PLUS, MINUS, TIMES, DIVIDE, POWER;

  double eval(double x, double y) {
    switch(this) {
      case PLUS:   return x + y;
      case MINUS:  return x - y;
      case TIMES:  return x * y;
      case DIVIDE: return x / y;
    }
    throw new AssertionError("Unknown op: " + this);
  }
}

위 예는 SUN의 공식 문서에서 가져온 예입니다. 위 예는 컴파일도 잘 되고 동작도 잘 됩니다. 하지만 자세히 보면 POWER 타입이 추가 되었는데 실수로 switch 문에 새로운 타입에 대한 코드를 넣지 않았음을 볼 수 있습니다. 이 때 컴파일은 잘 되지만 실행 중 오류가 발생합니다.

Constant-specific methods를 사용하지 않는 코드에 Type 추가 시
public enum Operation {
  PLUS   { double eval(double x, double y) { return x + y; } },
  MINUS  { double eval(double x, double y) { return x - y; } },
  TIMES  { double eval(double x, double y) { return x * y; } },
  DIVIDE { double eval(double x, double y) { return x / y; } };

  abstract double eval(double x, double y);
}

그래서 코드를 위와 같이 작성하면 eval 메서드를 사용하는 측에서는 코드 변경이 전혀 없으면서도, 타입이 추가 되었을 때 eval 메서드를 구현하지 않으면 컴파일 오류가 발생하게 됩니다.

저는 컴파일러를 이용한 영향력 검토와 검증방법으로써 사용이 가능하지만 한계가 있다는 점을 설명했습니다. 또한 Effective 2판의 조언에 따라 컴파일러 검증범위의 확장이 가능한 점 또한 설명했습니다. 하지만 여전히 컴파일러의 검증범위는 미비하며 컴파일러를 아무리 잘 활용해도 검증하지 못하는 범위가 많음을 알 수 있습니다. 따라서 컴파일러를 적극적으로 활용해야 할 필요는 있지만 한편으로는 컴파일러가 하지 못하는 부분에 대한 검증을 시도해야 할 필요가 있습니다. 이 점이 바로 단위 테스트, 인수 테스트, End-To-End 테스트와 같은 테스트가 필요한 이유입니다.

수정 이력
2010/05/07 : 이 주제가 여러 편에 걸쳐 게시되었기 때문에 내용이 중복되지 않고 읽는이가 부드러운 흐름을 타게하기 위해 내용을 다소 수정합니다.

WRITTEN BY
차민창
르세상스 엔지니어가 사회를 이끌어나가는 상상을 하며!

,
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.
마틴 파울러는 그의 저서인 ‘리팩토링’에서 리팩토링을 다음과 같이 정의했습니다.

“리팩토링은 외부의 동작을 바꾸지 않고 내부의 구조를 개선하는 것”

검증의 시각에서 이 정의를 볼 때 우리가 리팩토링을 하며 개선 외에 추구해야 할 또 다른 목표를 알 수 있습니다. 바로 리팩토링을 하더라도 외부의 동작을 바꾸지는 않는 것입니다. 다시 말해 리팩토링을 어떻게 하든 예전의 동작은 그대로 유지되어야 합니다. 지난 글에서 이 부분을 ‘검증’이라 불렀습니다. 검증을 하려 하다 보면 항상 반복적으로 고려하는 요소가 있음을 알게 됩니다. 바로 영향력 검토, 검증방법 결정, 검증시점입니다.

영향력 검토는 내가 수행하는 리팩토링이 어디에 영향을 미치는지 확인하는 단계입니다. 예를 들어 사용자가 남자이면 True를 반환하는 메서드가 있고 이 메서드는 프로젝트 전반에 걸쳐 광범위하게 사용되고 있다고 가정해봅시다. 이때 리팩토링 중 실수로 남자임에도 False가 반환되게 되었습니다. 이때 프로젝트에는 어떤 영향이 미칠까요? 아마 이 메서드를 사용하는 모든 곳의 기능이 정상적으로 동작하지 않을 것입니다. 이것이 바로 해당 메서드가 가지고 있는 영향력이며, 우리는 리팩토링 전에 이 영향력을 반드시 알아내야 합니다. 때로 리팩토링 시 내가 변경하는 곳으로 인해 생기는 영향력에 대해 정확히 파악하기가 어려운 경우가 있습니다. 이런 경우는 정확한 검증을 할 수 없다는 뜻과 동일하기 때문에 영향력을 파악하기 전에는 리팩토링 진행을 멈추는 것이 좋습니다,

그 다음으로는 검증방법을 결정해야 합니다. 특정 부분을 리팩토링 한 후에 이 부분의 동작이 예전과 정확히 동일한지를 실제로 검사하는 단계입니다. 예를 들어봅시다. F를 리팩토링 할 예정입니다. 이 부분은 기능 A와 B에 영향을 미칩니다. 따라서 리팩토링 후 A와 B에 대한 기능을 테스트 인력이 검토하기로 했습니다. 이 예는 영향력 검토를 하고 검증방법을 결정하는 예를 보여줍니다. “A와 B에 대한 기능을 테스트 인력이 검토하기로 했다” 부분이 검증방법을 결정한 것이라 볼 수 있습니다. 예에서처럼 테스트 인력이 검증할 수도 있지만 자동화 된 단위 테스트 등을 이용할 수 있습니다.

마지막으로 검증시점에 대한 고려가 필요합니다. 검증시점은 말 그대로 검증을 수행하는 시점을 뜻합니다. 앞선 내용에서 이미 언급했지만 보다 빠르게 검증하는 것은 비용 면에서 많은 유익을 줍니다. 지난 글에서는 표를 이용하여 개발 중, QA 중, 서비스 중으로 나눠서 설명을 했었는데 이 것은 서비스 흐름 관점의 구분이라 볼 수 있습니다. 좀 더 기술적으로는 컴파일 시점과 실행 시점으로 분류해볼 수 있습니다. 이 경우 컴파일 시점(Compile Time)이 실행 시점 보다 이르며 따라서 문제는 컴파일 시점(Runtime)에 발견되는 것이 좋습니다. 이 점은 컴파일러 검증의 필요성을 나타냅니다. 또한 실행 시점 중에서도 고객에게 서비스를 하고 있는 실행 시점보다는 테스트를 수행하고 있는 실행 시점에 문제가 발견되는 것이 더 좋습니다. 이 점은 단위 테스트와 CI서버 도입의 필요성을 나타냅니다.

위 세 가지 점에 대해 고려한 후에는 리팩토링을 하기 위해 필요한 비용과 리팩토링 후 검증을 하기 위해 필요한 비용, 즉 리팩토링에 대한 투자비용을 생각해볼 수 있습니다. 투자비용을 생각해보는 이유는 리팩토링 또한 일종의 투자이기 때문입니다. 리팩토링은 현재 어떤 노력을 기울여 이후의 유지보수 등의 노력을 적게 들이려는 노력의 일환입니다. 그렇기 때문에 리팩토링에 대한 투자비용을 따져보고 우리가 투자를 할 때 의례 하듯이 이 투자로 인해 얻어질 거라 생각하는 이익보다 투자비용이 크다고 판단한다면 리팩토링을 진행할지에 대해 다시 한번 생각해보는 것이 좋습니다. 또한 투자의 관점에서 리팩토링을 보면 적게 투자해서 많이 얻는 것도 중요합니다. 주식투자에서 이것을 가능하게 가능 것이 저 평가 된 우량주를 발견하는 것이라면, 리팩토링에서는 아래 그림과 같이 영향력과 검증방법에 따른 비용을 줄이고 검증시점을 앞당기는 것이 적게 투자해서 많이 얻는 열쇠라 할 수 있습니다.


그림#1. 영향력, 검증비용을 감소시키고 검증시점을 앞당기는 것

수정 이력
2010/05/07 : 이 주제가 여러 편에 걸쳐 게시되었기 때문에 내용이 중복되지 않고 읽는이가 부드러운 흐름을 타게하기 위해 내용을 다소 수정합니다.

WRITTEN BY
차민창
르세상스 엔지니어가 사회를 이끌어나가는 상상을 하며!

,