(Working Effectively with Legacy Code) 007. It Takes Forever to Make a Change

It Takes Forever to Make a Change

코드 변경시 시간이 얼마나 걸릴까?

코드의 파악 혹은 이해도가 떨어지는 상태에서 작업하는 경우 당연히 많은 시간이 필요할 것이다.

반면 명확하게 코드를 이해하고 있는 경우엔 상대적으로 매우 빠르게 처리할 수 있을 것이다.

1. Understanding

프로젝트가 비대해질수록 코드 전체를 이해하는 데 많은 시간이 필요한 것은 당연한 일이다.

비대한 프로젝트는 곧 변경 포인트를 찾아내는 데 필요한 시간의 증가로 이어진다.

사실 이러한 현상은 프로젝트를 진행함에 따라 불가피한 현상이다.

기존 시스템에 코드를 추가하는 경우는 크게 두 가지로 볼 수 있다.

  1. 기존에 존재하는 클래스 / 메서드에 코드를 추가하는 경우
  2. 새로운 클래스 / 메서드를 작성하는 경우

레거시 시스템의 경우 상황을 제대로 이해하고 있지 않으면 변경 방법을 파악하는 데 매우 오래 걸리기 때문에

개발자는 맡은 시스템을 꾸준히 관리하여 적절한 크기를 유지하고, 네이밍을 부팅고, 이해하기 쉽도록 모듈화해야한다.

2. Lag Time

코드 변경 작업은 지연 시간(Lag Time) 이라는 흔한 이유로 오래 걸리는 경우가 많다.

지연 시간은 변경을 수행한 시점과 그 변경에 대한 실질적인 피드백을 받을 때까지의 시간을 말한다.

참고 쉽게 생각하면 개발 후 QA를 완료할때까지의 시간이다.

비효율적인데? 라는 생각이 들지만, 코드를 변경하고 빌드 후 결과를 확인하는 일반적인 소프트웨어 개발 환경이라는 것을 곧 깨달을 수 있을 것이다.

비효율을 효율로 바꾸는 조건은 시스템 내의 모든 클래스 및 모듈을 다른 클래스 및 모듈과는 독립적으로 별도의 테스트 하네스에서 컴파일할 수 있게 교체하는 것이다.

이 조건을 달성하면 매우 빠르게 피드백을 얻을 수 있기에 지연 시간을 최소화할 수 있다.

3. Breaking Dependencies

이전 포스팅부터 꾸준히 의존 관계를 끊어내는 것에 대해 설명해왔다.

일반적으로 변경한 클래스를 테스트 하네스 내에서 실행할 수 있다면 edit - compile - link - test 에 투입하는 시간을 크게 줄일 수 있다.

꾸준히 관리되어온 시스템이라면 상관없겠지만, 레거시 시스템의 경우엔 테스트 하네스에서 클래스를 참조하는 것부터 난관일 수 있다.

좀 더 자세한 내용은 추후에 깊게 다루도록 하고,

본 포스팅에서는 좀 더 편리한 빌드를 위하여 코드의 구조를 바꾸는 방법에 대해서 알아본다.

3.1. Build Dependencies

객체지향 시스템의 경우, 가장 먼저 해야할 일은 어떤 종류의 의존 관계가 테스트에 걸림돌인지 파악하는 것이다.

방법은 간단하다.

테스트 하네스 안에서 변경 대상인 클래스들을 접근해서 사용해보면 된다.

문제가 발생하면 십중팔구는 의존 관계에 의한 것이며, 이를 해소하더라도 실질적인 의존 관계는 남아있기때문에 빌드 타임에 이득을 보지 못한다.

즉 관건은 재컴파일 대상이 되는 클래스들을 최대한 줄여나가는 것이다.

이 문제를 해결하려면 클래스 집합 외부의 클래스가 사용하는 클래스집합 내부의 클래스에 대한 인터페이스를 추출해내야 한다.

혹은

클래스 집합을 먼저 테스트 루틴으로 보호 조치한 뒤, 물리적 구조를 변경하여 빌드 타임의 이득을 확보하는 방법이 있다.

다소 복잡한 빌드 구성이 되더라도, 의존 관계를 끊어낼수록 개별 빌드에서 이득이 되면 된다.

참고 복잡한 빌드 구성은 전체 시스템의 빌드 타임을 증가시킬 수 있다. 하지만 우리가 원하는 것은 변경 대상에 대해 개별 빌드를 빠르게 수행하여 빠른 피드백을 얻어내는 것이다.

예제를 통해 파악해보자.

Figure 7.1

위의 UML은 하나의 패키지 내에서 서로 연관된 클래스를 보여준다.

이때 AddOpportunityFormHandler 클래스를 변경하면서 빌드 속도를 올릴 수 있는 방법이 있을지 고민해보자.

가장 먼저 해야할 일은 AddOpportunityFormHandler 클래스의 객체를 생성해보는 것이다.

헌데 AddOpportunityFormHandler 클래스가 의존중인 클래스들이 전부 실체를 가진 클래스일 경우 문제가 된다.

AddOpportunityFormHandler 클래스는 ConsultantSchedulerDB 클래스와 AddOpportunityXMLGenerator 클래스를 필요로 하고 있다.

물론 UML에 표시되지않는 다른 위치에 존재하는 클래스에 의존 관계를 가지고 있을 수도 있기에 AddOpportunityFormHandler 객체를 생성하기 위해 얼마나 많은 의존 관계가 있는지 알 수 없다.

따라서, 의존 관계를 하나씩 제거해나가는 작업이 필요하다.

먼저 ConsultantSchedulerDB 클래스의 의존 관계를 보자.

AddOpportunityFormHandler 객체 생성시 ConsultantSchedulerDB 객체를 인자로 전달해야하는데, ConsultantSchedulerDB는 데이터베이스에 연결되므로 까다로운 클래스이다.

ConsultantSchedulerDB 클래스는 구현체 추출 기법을 이용해 의존 관계를 아래와 같이 끊어낼 수 있다.

Figure 7.2

구현체 추출 후 ConsultantSchedulerDB 클래스는 인터페이스가 되었으므로 위장 객체를 사용해 AddOpportunityFormHandler 객체를 생성할 수 있다.

의존 관계를 끊어내는 경우 빌드 속도가 빨라지는 경우가 종종 생기는 데, ConsultantSchedulerDBImpl을 변경하더라도 의존 관계가 없기때문에 재컴파일 대상에서 제외되기 때문이다.

즉, ConsultantSchedulerDB 인터페이스를 변경하지만 않는다면 ConsultantSchedulerDBImpl을 얼마나 변경하는 지와 관계없이 AddOpportunityFormHandler 는 재컴파일할 필요가 없다.

구조를 한 번 더 변경해보자.

Figure 7.3

OpportunityItem 클래스에 구현체 추출 기법을 적용한 UML이다.

이제 AddOpportunityFormHandler 클래스와 OpportunityItem 클래스는 의존 관계가 끊어져 OpportunityItemImpl 클래스의 변경에 대해 재컴파일이 필요없어졌다.

시스템에 구조를 명시하고 싶다면 아래와 같이 패키지 레벨로 분할한다.

Figure 7.4

OpportunityProcessing 패키지는 데이터베이스와 관련된 어떠한 의존 관계도 가지도 있지 않으므로, 패키지내 테스트 루틴에 대해 빠른 피드백을 얻을 수 있는 구조가 완성되었다.

지금까진 구현체 추출 기법을 통해 AddOpportunityFormHandler 클래스가 의존하는 클래스에 대해 의존 관계를 끊어 빌드 속도를 높이도록 구조를 변경해보았다.

이번엔 반대로 AddOpportunityFormHandler 클래스에 의존하는 코드들의 빌드 속도를 높이도록 변경해보자.

Figure 7.5

위의 UML을 통해 파악해보면 AddOpportunityFormHandler 클래스는 OpportunityProcessing 패키지에서 유일하게 공개된 클래스이다.

따라서 AddOpportunityFormHandler 클래스에 변경 사항이 발생한다면, AddOpportunityFormHandler 클래스를 의존하고 있는 모든 코드가 재컴파일 대상이 된다.

여태까지 해왔던 것처럼 AddOpportunityFormHandler 클래스에 구현체 추출 기법 혹은 인터페이스 추출 기법을 적용해 동일한 효과를 얻을 수 있을 것이다.

의존 관계를 제거하고 클래스를 다수의 패키지로 분산시키는 등 빌드 시간을 줄이는 것은 매우 큰 의미가 있다.

테스트 루틴의 재빌드 및 실행 속도를 높이는 것은 곧 빠른 피드백의 획득을 보장하며 결과적으로 더 적은 오류와 장애 방지를 가져오기 때문이다.