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 크기의 광고 코드만 넣을 수 있습니다.
최근 진행했던 두 번의 프로젝트는 내게 무척 새로운 프로젝트였습니다. 왜냐하면 프로젝트의 목적이 특정 기능의 개발이 아닌 기존 코드의 리팩토링이었기 때문입니다. 첫 프로젝트를 진행할 때 저의 최초 관심은 '개선'이었습니다. 어떻게 하면 소스를 좀 더 유지보수하기 좋게 만들 수 있을까? 하지만 실제로 프로젝트를 시작하고 전반적인 작업의 진행을 시작하자 저의 관심은 개선에서 점차 '검증'쪽으로 향하게 되었습니다. 왜냐하면 개선을 하며 자연스레 발생하는 영향력이 어마어마하다는 것을 깨달았기 때문입니다. 좋은 예로써 핵심 클래스의 인터페이스를 수정해야 하는 경우가 있었습니다. 그런데 그 수정으로 인해 영향 받는 부분을 살펴보자 영향 받는 하위 프로젝트만도 10개[1]가 넘었습니다. 따라서 개선도 개선이지만 검증을 성공적으로 하는 것이 매우 중요하게 느껴졌습니다.

어떻게 하면 검증을 성공적으로 할 수 있을까? 가장 먼저 생각난 것은 QA(Quality Assurance)였습니다. 제가 일하는 환경에는 능력이 매우 뛰어난 QA부서가 있었고, 사내 표준 프로세스상 배포해야 하는 모든 제품은 반드시 QA를 거쳐야 했습니다. 따라서 열심히 리팩토링을 한 후 QA에게 검증을 요구하는 것은 매우 자연스러운 일이었습니다. 하지만 곰곰이 생각해볼 때 몇 가지 면에서 문제를 느꼈습니다.

첫째는 위에서 얘기했던 것처럼 영향 받는 범위가 매우 크다는 점이었습니다. 제가 일하는 환경의 QA는 보통 블랙박스 관점으로 기능 중심의 검증을 수행했습니다. 따라서 보통의 경우 개발자가 특정 부분을 수정하면 수정한 부분으로 인해 영향 받는 기능을 QA에게 이야기 했습니다. 이 경우 QA는 매우 기본적이며 핵심적인 주요 기능에 대한 검증을 진행하는 것과 더불어 수정으로 인해 영향 받은 기능을 추가적으로 검증했습니다. 하지만 제가 진행했던 리팩토링의 경우 위와 같은 방법으로 수정범위를 얘기하려고 하면 거의 모든 기능을 검증 해달라고 말해야 했습니다. 왜냐하면 위에서 얘기했듯이 영향력이 어마어마했기 때문입니다. 더욱이 당시 QA 자원이 많이 부족한 상태였는데 거의 모든 기능에 대해 검증해달라고 말하는 것은 바람직하지 못해 보였습니다.

둘째는 문제 발견 시점에 따른 비용증가 때문이었습니다. FEDEX에는 1-1-100원칙이 있습니다. 이 원칙은 제품을 출고하기 전에 문제를 발견하면 비용이 1만 필요하지만 출고 후에는 10의 비용이 필요하며 고객에게 항의가 들어오면 100의 비용이 필요하다는 원칙입니다. 이 원칙은 소프트웨어에도 그대로 적용된다고 봅니다. 아래 표는 실제 내가 일하는 환경에서 발생하는 현상을 바탕으로 작성했습니다.

 문제 발견 시점  관련 자원(문제 처리 비용)
 개발자가 개발 중  담당 개발자
 QA 중  BTS(Bug Tracking System)
 외부와 의사소통 담당하는 개발자
 담당 개발자
 배포 후 서비스 중
 고객
 문제/장애 알림 시스템
 서비스 기획자
 서비스 운영 책임 개발자
 BTS(Bug Tracking System)
 담당 개발자
 QA
 배포 담당자(재배포가 필요하기 때문에)

각 환경에 따라 다소 차이는 있겠지만 큰 맥락에서 위 표와 크게 다르지는 않을 것이라 생각합니다. 위 표를 보면 FEDEX 원칙과 유사하게 문제가 늦게 발견되면 될수록 처리하는 데 필요한 관련 자원이 증가함을 볼 수 있습니다. 따라서 특수한 환경이 아니라면 QA 단계에서 문제를 발견하는 것 보다는 개발자가 문제를 발견하는 것이 조직 전체 비용 관점에서는 유리합니다.

마지막으로 QA가 탐지하기 어려운 영향력이 있다는 점 때문입니다. 위에서 잠시 애기했듯이 QA는 블랙박스 관점으로 기능중심의 검증[2]을 수행합니다. 이 경우 화면으로 즉시 보여지는 부분에 대해서는 매우 높은 수준의 검증이 가능합니다. 예를 들어 화면상의 특정 부분이 일그러져 보이거나, 확연히 구분할 수 있게 문자 등으로 오류가 보이는 등의 문제점입니다. 하지만 월말이 되어 통계 화면에서 보여지는 데이터의 입력이 누락되거나 하는 문제는 발견하기 어렵습니다. 이러한 문제는 외부로 즉시 드러나지 않기 때문입니다.

따라서 저는 QA에게 모든 검증을 의존하는 것은 좋지 못한 선택이라고 생각합니다. 그것보다는 개발자가 최전방에서 적극적으로 보다 많은 검증을 수행해야 한다고 봅니다. 물론 QA를 배제할 생각은 없습니다. 다만 일차적으로 개발자가 나름의 방법으로 검증을 먼저 진행한 후 이차로 QA가 검증하는 이중검증이 더 좋은 결과를 만들 것이라 생각합니다. 이중검증은 동일한 문제를 전혀 다른 방법으로 검증하여 검증의 정확도를 더욱 높이는 방법입니다. 일반적으로 개발자와 QA의 시각은 많이 다른 편이기 때문에 각자가 생각하는 최선의 방법으로 이중검증[3]을 하면 분명히 긍정적 효과가 있으리라 봅니다.

참고 설명

[1] 제가 리팩토링을 진행했던 프로젝트는 공통 라이브러리 성격의 프로젝트였습니다.
[2] 제가 일하는 환경에는 QA가 블랙박스 테스트만 하지만, 개발자 출신의 QA가 코드검토와 같은 화이트박스 테스트를 진행하는 곳도 있다고 합니다.
[3] 켄트벡의 익스트림 프로그래밍이라는 책에 보면 재확인(Double Checking) 원칙이 소개됩니다. 이 원칙은 문제를 해결할 때 전혀 다른 방법 두 가지로 각각 문제를 해결한 후에 결과를 비교해 보아 결과의 정확성을 보장하는 방법입니다. 마찬가지로 개발자는 개발자의 관점에서 QA는 QA의 관점에서 각기 다르게 검증을 수행한다면 좀더 정확한 검증을 할 수 있을 것이라 생각합니다.

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

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

,