(Working Effectively with Legacy Code) 002. Working with Feedback

Working with Feedback

시스템을 변경하기 위한 방법은 크게 두 가지로 나눌 수 있다.

1. Edit and Pray

Edit and Pray는 직역하면 편집 후 기도하기 가 되겠다.

업계에서 흔히들 말하는 기도메타이다.

코드 변경 계획을 먼저 수립하고 변경 대상 코드에 대한 이해를 바탕으로 변경 작업을 수행한 뒤,

시스템을 실행하여 제대로 동작하는 지 확인하는 방법이다.

위의 텍스트만 보면 매우 신중하고 전문적인 방법으로 보일 수 있지만, 아무리 신중하더라도 결국 사람은 실수를 할 가능성이 존재한다.

쉽게 말해 코드 수정 후 리그레션 테스트간에 영향성이 없길 기도해야하는 방식이다.

2. Cover and Modify

똑같이 직역해보면 보호 후 수정하기 가 되겠다.

이 방식은 기본적으로 변경사항에 대한 안전 장치를 깔아두고 변경하는 것을 의미한다.

여기서 안전 장치란, 변경으로 인한 문제점이 발생하더라도 전파되는 범위를 최소화하는 것을 의미한다.

즉 테스트 코드를 작성하여 코드를 안전하게 보호하는 것이다.

적절하게 테스트 코드가 작성되어있는 경우, 변경 전 후의 수행 결과가 정상적인지 쉽게 확인할 수 있고

문제가 발생하더라도 빠르게 탐지해낼 수 있다.

편집 후 기도하기 처럼 신중하게 하는 것은 동일하지만, 이 신중함을 테스트 코드가 보조해주기때문에 좀 더 용이하게 변경을 수행할 수 있다.

Regression Test

전통적으로 테스트 루틴은 개발이 완료된 후에 작성하고 실행하는 것이 일반적이다.

개발자가 코드를 작성하고나면, QA 팀이 코드가 요구사항에 맞게 작성되었는지 확인하는 방식으로, 피드백을 받을수는 있지만 신속하게 받을 수 있다는 보장이 없다는 문제가 있다.

이런 방식의 테스트는 작업 결과의 정확성을 보여주기 위한 테스트 로 좋은 테스트 방법인 것은 맞다.

하지만 다른 목적을 가진 테스트를 수행할 수 있는데, 변경된 부분을 발견하기 위한 테스트 이다.

이 테스트를 회귀 테스트(Regression Test) 라고 부르며, 주기적으로 수행하며 정상적인 동작 여부를 확인하고

기존에 동작들이 여전히 잘 동작하는 지 확인하는 것을 의미한다.

회귀 테스트가 매우 좋아보이는 개념임에도 자주 사용되지않는 이유는 일반적으로 애플리케이션의 인터페이스를 통해 수행되기 때문이다.

이를 개선하기위해 단위 테스트(Unit Test) 개념을 도입해야 한다.

변경할 코드에 대응하는 단위 테스트를 먼저 작성하고, 변경 후에 테스트를 수행하여 정상적으로 동작하는 지 확인하는 방식으로

즉각적인 피드백을 얻을 수 있다.

What Is Unit Testing?

단위 테스트란 용어는 매우 오래된 역사를 가지고 있다.

단위 테스트의 기본 개념은 독립된 개별 소프트웨어 컴포넌트를 테스트하는 것이다.

컴포넌트의 정의는 다양하지만, 시스템의 가장 원자적인 동작 단위를 의마한다.

절차지향 프로그래밍에서는 보통 함수를 의미하며, 객체지향 프로그래밍에서는 클래스를 의미한다.

함수의 호출 구조나 클래스 간의 관계를 생각해보면 단 하나의 함수나 클래스를 테스트하는 것은 매우 어렵다는 것을 유추할 수 있다.

하지만 반대로 함수나 클래스또한 독자적으로 존재하는 경우가 거의 없다는 것을 생각해보면 원자적인 테스트를 수행해나가는 것의 의미가 매우 중요함을 깨달을 수 있다.

그렇다면 단위 테스트 대비 넓은 범위를 가진 대규모 테스트가 더 중요할까?

이는 맞는 말이지만 대규모 테스트가 가진 몇 가지 문제점이 있다.

1. 오류 위치 파악

테스트 루틴이 테스트 대상으로부터 멀어지면 멀어질수록, 테스트 실패의 의미를 파악하는 것이 힘들어진다.

테스트의 입력값을 확인하고 오류의 내용을 확인한 뒤, 테스트 범위내에서 오류 원인을 추적해나가야 한다.

이는 단위 테스트도 마찬가지긴 하지만 파악 범위가 매우 작기때문에 그렇게 어려운 일이 아니다.

2. 실행 시간

테스트 루틴의 길이가 길어질수록 실행하는 데 오랜 시간이 걸린다.

지나치게 오래 걸리는 테스트는 결국 잦은 테스트의 실행을 지양하게 만든다.

3. 커버리지
각 코드 조각들과 조각들의 연결관계는 파악하기 어려운 부분이다.

새로운 코드 조각이 추가된 경우, 그 코드를 실행하기 위한 상위 단계의 테스트 루틴을 작성해야하는 등 많은 작업을 요구하는 경우가 잦다.

단위 테스트의 장점

대규모 테스트의 단점을 보완하는 단위 테스트의 장점을 알아보자.

단위 테스트는 코드 조각들을 독립적으로 테스트할 수 있으며, 테스트를 그룹화하여 서로 다른 조건하에 테스트를 실행할 수 있다.

이는 테스트 실패가 발생할 경우 파악 범위를 작게 만드는 효과가 있다.

특정 코드에 오류가 있다고 추정된다면, 단위 테스트를 사용하여 정말 오류가 있느지 빠르게 검증할 수 있다.

좋은 단위 테스트의 조건은 아래와 같다.

1. 실행 속도가 빠르다.

2. 오류 위치 파악에 도움이 된다.

소프트웨어 업계에서는 어떤 테스트가 단위 테스트인지 많은 의견이 오가곤 한다.

단위 테스트라고 해도 여러 개의 클래스를 사용하는 비교적 큰 테스트가 존재할 수 있으며, 이는 소규모 통합 테스트로 보일 수도 있다.

Higher-Level Testing

단위 테스트가 중요함을 계속해서 언급했지만, 그렇다고해서 상위 수준의 테스트가 중요하지 않은 것은 아니다.

상위 수준의 테스트 역시 필요할 때가 많으며, 이 테스트를 통해 여러 클래스의 동작을 한 번에 확인할 수 있다.

상위 테스트를 통해 개별 테스트에 대한 테스트 루틴 작성이 용이해지기도 한다.

Test Coverings

테스트 코드로 기존 코드를 보호할 수도 있다.

만약 어떠한 레거시 프로젝트를 변경한다고 가정해보자.

가장 먼저 해야할 일은 어떤 코드부터 변경할지 계획을 세우는 게 아니라, 기존에 존재하는 레거시 코드에 대응하는 테스트 루틴을 배치하는 것이다.

이를 통해 안전성을 확보한 상태로 변경을 가할 수 있으며, 휴먼 에러로 인한 오류를 쉽게 잡아낼 수 있다.

예제를 통해 한 번 알아보자.

위의 그림에서 몇 개의 클래스를 보여주고 있다.

InvoiceUpdateResponder 클래스의 getResponseText() 메서드와 Invoice 클래스의 getValue() 메서드를 변경하고 싶다고 가정해보자.

이때 두 메서드가 변경 지점이며 이 변경 지점에 대한 테스트 루틴을 작성함으로서 두 메서드를 보호할 수 있게 된다.

보호를 위해 테스트 루틴을 작성하려고 하면 InvoiceUpdateResponder 클래스와 Invoice 클래스의 객체를 테스트 코드 내에서 생성해야 하는데 여기서 문제가 발생한다.

Invoice 클래스의 객체는 아무런 인자없이 생성할 수 있기때문에 문제가 안되지만 InvoiceUpdateResponder 클래스를 실제 DB와의 연결을 인자로 넘겨주어야만 한다.

테스트를 위해 실제로 DB를 생성할 수도 있겠지만, 이는 많은 작업이 요구되며 테스트 시간도 증가하게 될 것이다.

일단 Invoice 클래스의 테스트 루틴을 작성하는 데 집중해서 처리하고, InvoiceUpdateResponder를 다시 보자.

InvoiceUpdateServlet 객체까지 인자로 넘겨주어야 하는데 이 객체를 쉽게 생성할 수 있을까?

아예 인자로 받지않도록 수정하는 것도 방법이다.

InvoiceUpdateServlet 객체대신 해당 객체에서 필요한 정보만 넘겨서 생성하도록 코드를 변경하는 것이다.

그럼 기존에 존재하는 레거시 코드를 보호하지않게되는 것이 되고, 변경 전 작업이 맞는지 의구심이 들게 된다.

이 클래스들간의 의존 관계로 인해 테스트 코드를 작성하기 힘든 경우가 많으며 의존 관계를 끊어내기 위한 리팩토링도 빈번하다.

어찌되었든 아래와 같이 코드를 수정했다고 가정해보자.

InvoideUpdateServlet 클래스에서 꼭 필요한 것은 Invoid ID값이므로 이 값을 따로 전달하여 의존 관계를 끊어버린다.

DBConnection 클래스는 IDBConnection이라는 인터페이스를 작성하고 이를 사용하도록 변경하여 끊어냈음을 확인할 수 있다.

이러한 리팩토링 방식을 기본 타입 매개변수(primitive parameter)인터페이스 추출(extract interface) 라고 부른다.

이 방법론은 추후 포스팅에서 알아보자.

The Legacy Code Change Algorithm

레거시 코드를 변경하는 순서에 대해서 알아보자.

1. 변경 지점을 식별한다. (Identify change points.)

코드 변경을 수행할 지점은 소프트웨어 아키텍쳐와 밀접하게 연관되어 있다.

어떠한 부분을 변경하는 것이 좋은지 확신을 가지려면 설계 구조를 잘 파악하고 있어야 한다.

소프트웨어 설계는 추후 포스팅에서 다루도록 하겠다.

2. 테스트 루틴을 작성할 위치를 찾는다. (Find test points.)

레거시 코드의 경우 테스트 루틴을 작성할 곳을 파악하기 어렵다.

추후 테스트 루틴 작성 위치를 판단하기 위한 기법들에 대해서도 알아보자.

3. 의존 관계를 제거한다. (Break dependencies.)

예제로 경험했듯, 테스트를 실행할때 의존 관계는 매우 큰 장애물이다.

의존 관계가 문제가 되는 가장 대표적인 두 가지는 테스트 내부에서 객체를 생성하거나 메서드를 실행하는 경우이다.

의존 관계를 제거하기 위해 수행한 작업으로 다른 문제가 발생하지않도록 신중하게 작업해야 한다.

이 방법도 추후 포스팅으로 자세히 다루도록 하겠다.

4. 테스트 루틴을 작성한다. (Write tests.)

5. 변경 및 리팩토링을 수행한다. (Make changes and refactor.)

레거시 코드에 기능을 추가할 때는 TDD(Test-Driven Development) 방법론이 권장된다.