////
Search
Duplicate
🚔

Chapter 6. 고칠 것은 많고 시간은 없고

리팩토링은 개발자의 시간을 많이 빼앗는다. 그러나 대부분의 경우, 결국은 개발 시간과 시행착오를 줄여준다.
그런데 ‘결국’은 대체 언제를 가리키는 것일까? 이것은 프로젝트에 따라 다르다.
변경하고자 하는 코드의 테스트 루틴을 작성하는데 2시간이 걸렸고, 코드 변경에 15분이 걸렸다면 2시간을 낭비했다고 생각할 지 모른다.
하지만 테스트 루틴이 준비되어있었기 때문에 코드 변경에 15분 밖에 걸리지 않은 것이다.
결론적으로 테스트 루틴의 사용은 개발 작업의 속도를 높이므로 대부분의 개발팀에게 중요하다.
데드라인이 정해진 상황에서 테스트 루틴 작성 여부 판단에 가장 걸림돌이 되는 것은 기능 구현에 걸리는 시간을 알 수 없다는 점이다.
이럴 때 사용할 수 있는 몇 가지 추정 기법은 16장에서 다룬다.
때문에, 가장 빠른 방법을 사용해 해당 기능을 구현하려는 유혹에 빠지기 쉽다. 테스트와 리팩토링은 뒤로 미뤄두고 말이다.
문제는 뒤로 미뤄둔 테스트와 리팩토링을 하지 않는다는 점이다. 이는 의지의 문제이기도 하다.
당장 변경해야 할 클래스가 있다면 우선 테스트 하네스 내에서 해당 클래스의 인스턴스 생성을 시도한다.
생성이 불가하다면 9장과 10장을 참고하자.
9장과 10장을 참고해도 의존 관계 제거가 힘들고 테스트 루틴을 준비하는 것이 불가능하다고 판단된다면, 현재 시도 중인 코드 변경을 조사하자.
완전히 새로운 코드를 작성함으로써 많은 경우에 문제를 해결할 수 있다.
이번 장은 완전히 새로운 코드를 작성함으로써 코드 변경에 대처하는 몇 가지 기법들을 설명한다.

발아 메소드

시스템에 새로운 기능을 추가할 때, 이를 완전히 새로운 코드로 표현할 수 있다면 새로운 메소드로서 이 기능을 구현하고 이 메소드를 필요한 위치에서 호출하도록 사용할 수 있다.
호출을 수행하는 코드를 테스트 루틴으로 보호하기는 어려울 수 있으나 최소한 새로운 코드에 대한 테스트 루틴 작성은 용이할 것이다.
더하기와 빼기만 지원하던 계산기 코드에 곱하기 기능을 추가한다고 생각해보자.
이를 기존 계산기 코드에 추가하면 책임이 늘고 클래스의 길이가 길어져 가독성이 저하될 것이다.
이런 코드를 새로운 클래스에 책임을 할당하거나 메소드를 분리하여 빼내라는 것이다.
기존 calculate라는 메소드의 테스트 루틴을 작성하기엔 어려울 수 있으나 새로운 multiply 메소드에 대한 테스트 루틴 작성은 용이할 것이다.
이런 메소드들을 발아 메소드의 예다. 발아 메소드의 작성 순서는 다음과 같다.
1.
어느 부분에 코드 변경이 필요한지 식별한다.
2.
메소드 내의 특정 위치에서 일련의 명령문으로서 구현될 수 있는 변경이라면, 필요한 처리를 수행하는 신규 메소드를 호출하는 코드를 작성한 후 주석 처리해둔다.
3.
호출되는 메소드가 필요로하는 지역 변수를 확인하고 이 변수들을 매개변수로 전달한다.
4.
호출하는 메소드에 값을 반환해야 하는지 여부를 결정한다. 값을 반환해야 한다면 반환 값을 사용하도록 호출 코드를 변경한다.
5.
새롭게 추가되는 메소드를 테스트 주도 개발 방법을 사용해 작성한다.
6.
앞서 주석 처리했던 신규 메소드 호출 코드의 주석을 제거한다.
독립된 한 개의 기능으로서 코드를 추가하게 되는 경우나 변경이 발생하는 메소드의 테스트 루틴이 아직 준비되지 않은 경우, 발아 메소드의 사용을 권장한다.
코드를 인라인 형태로 추가하는 것보다 더 낫기 때문이다.

장점과 단점

발아 메소드의 단점은 이 메소드를 사용하게 되는 경우, 원래의 메소드와 그 클래스들을 잠시 포기하는 것과 같다.
원래의 메소드를 테스트 루틴으로 보호하는 것도 아니고 개선하는 것도 아니다. 그저 신규 메소드로 새로운 기능을 추가하는 것이다.
이는 적어도 원래의 클래스를 나중에 테스트 루틴으로 보호할 때, 추가적인 작업을 해야 한다는 것을 의미한다.
장점은, 발아 메소드를 사용하면 기존 코드와 새로운 코드의 영역을 확실히 구분지을 수 있다는 점이다.
기존 코드를 테스트 루틴으로 즉시 보호할 수 없더라도 최소한 변경 부분을 개별적으로 이해할 수 있고 새 코드와 기존 코드 사이의 인터페이스도 명확해진다.
또한 영향을 받는 변수를 명시해두었기 때문에 메소드의 결과도 정확하게 파악할 수 있다.

발아 클래스

발아 메소드도 충분히 강력한 기법이지만 의존 관계가 복잡하게 얽혀 있다면 충분히 효과적이지 않다.
어떤 클래스를 변경해야 하는데, 적절한 시간 내에 이 클래스의 객체를 테스트 하네스 내에서 생성할 방법이 없는 경우, 이 클래스에 대해 발아 메소드를 적용해서 테스트를 작성할 수도 없다.
객체 생성과 관련된 의존 관계가 많아 인스턴스 생성이 어려울 수도 있고 많은 수의 의존 관계가 숨겨져 있을 수도 있다.
이런 문제를 해결하려면 광범위하게 리팩토링을 수행해, 의존 관계를 제거함으로써 테스트 하네스 내에서 인스턴스를 생성할 수 있게끔 만들어야 한다.
이런 경우, 변경에 필요한 기능을 별도의 클래스로서 추출한 후, 이 클래스를 기존 클래스에서 이용하는 방법을 사용할 수 있다.
즉, 변경하려는 객체의 테스트 컴파일이 가능하다면 발아 메소드, 불가하다면 발아 클래스를 이용해 테스트 루틴을 작성할 수 있는 변경 영역을 제한하는 것이 핵심인 것 같다.

장점과 단점

발아 클래스의 장점은 최소한 이미 좋지 않은 상황을 악화시키지 않는다는 것이다. 시간이 조금 지난 후, 기존 클래스를 재검토하여 테스트 루틴에 집어 넣을 수 있을 것이다.
단점은 메커니즘이 복잡하다는 것이다. 이러니 저러니해도 결국, 새로운 클래스에 대한 의존 관계가 생기기 때문에 전체적으로 봤을 때는 복잡해질 수밖에 없다.

포장 메소드

기존 메소드에 동작을 추가하는 것이 옳지 않은 접근법일 때가 자주 있다.
보통 처음 메소드가 작성될 때의 의도는 하나의 동작만을 제공하기 위한 것이 대부분인데, 이후 추가되는 코드들은 처음 작성될 때의 의도와 부합하지 않는 경우가 많기 때문이다.
개발자가 이런 코드를 추가하는 이유는 단지 추가되는 코드가 기존 코드와 동기적으로 동작하기 때문일 때가 있다.
이는 과거에 일시적 결합이라 불리던 현상으로 과도하게 사용되면 코드의 품질을 저하시킨다.
단지 동시에 실행된다는 이유로 동일 메소드 내에 들어있는 코드 간의 연관성이 그리 크지 않은 경우가 많다.
따라서 동작이 추가될 때, 그리 복잡하지 않은 기법을 사용할 필요가 있으며, 이때 매우 유용한 방법 중 하나가 포장 메소드다.
포장 메소드의 한 형태는 기존 메소드와 이름이 같은 메소드를 새로 생성하고 기존 코드에 처리를 위임한다.
이 포장 메소드는 기존 메소드의 전후에 새로운 동작을 추가하고 싶을 때, 용이하다.
사용자가 로그인 시도를 할 때마다 기록을 남기는 경우, 유용할 것이다.
이는 다음과 같은 단계로 작성된다.
1.
변경해야 할 메소드를 식별한다.
2.
변경이 메소드의 특정 위치에서 일련의 명령문으로 구현된다면 메소드 이름을 바꾸고 기존 메소드와 동일한 이름과 시그니처를 갖는 메소드를 새로 작성한다. 이때 시그니처를 그대로 유지해야 한다.
3.
새로운 메소드에서 기존 메소드를 호출하도록 한다.
4.
새로운 기능을 위한 메소드를 테스트 주도 개발로 작성하고 이 메소드를 신규 메소드에서 호출한다.
또한 아직 호출된 적이 없는 새로운 메소드를 추가하고자 할 때 사용할 수 있다.
기존 메소드와 신규 기능을 함께 포장하는 새로운 메소드를 추가함으로써 기존 메소드를 봉합 지점으로 만들어 대체 가능하게 만드는 것이다.
이는 다음과 같은 단계로 작성된다.
1.
변경하고자 하는 메소드를 식별한다.
2.
변경이 메소드 내의 특정 위치에서 일련의 명령문으로 구현된다면 변경을 구현할 메소드를 테스트 주도 개발로 새로 작성한다.
3.
새 메소드와 기존 메소드를 호출하는 별도의 메소드를 작성한다.
이 기법에도 단점은 존재한다. 새로 추가하려는 기능의 로직이 기존 기능의 로직과 통합될 수 없다는 점이다.
새로운 기능은 기존 기능의 이전, 이후에 실행되어야 한다. 하지만 이는 항상 부정적인 것은 아니며 가능하다면 이 기법을 적용하는 것이 좋다.

장점과 단점

호출을 수행하는 코드에 대한 테스트 루틴을 작성하기 쉽지 않을 때, 포장 메소드는 테스트가 끝난 신규 기능을 애플리케이션에 추가할 수 있는 좋은 기법이다.
발아 메소드, 클래스는 기존 메소드에 코드가 추가되기 때문에 코드의 길이가 늘지만, 포장 메소드는 기존 메소드의 길이가 변경되지 않는다.
또한 기존 기능과 분명히 독립적으로 만들어진다는 것도 장점이다. 하나의 목적을 가진 코드가 그 외의 목적을 가진 코드와 섞이지 않는다.
가장 큰 단점은 적절하지 못한 이름이 붙을 가능성이 높다는 것이다.

포장 클래스

포장 메소드를 클래스 수준으로 확장한 것이 포장 클래스다. 이는 메소드와 거의 같은 개념을 사용한다.
시스템에 동작을 추가해야 할 때, 그 동작을 기존 메소드에 추가할 수도 있지만 그 메소드를 사용하는 클래스에 추가할 수도 있다.
포장 클래스의 한 예는 데코레이터 패턴으로 소개되기도 한다. 다른 클래스를 포장하는 객체들을 생성한 후, 이 객체들을 차례대로 전달하는 것이다.
이때의 포장 클래스는 내부에 포장되는 클래스와 동일한 인터페이스를 가져야한다. 대신해야하기 때문이다.
⇒ 유사한 패턴인 프록시가 어떤 목적을 가졌는지 기억이 잘 나지 않는다. 패턴을 의도에 따라 분류하기도 하므로 다시 공부해둬야겠다.
다른 포장 기법도 존재하는데, 새로운 기능을 데코레이터로서 포장하는 대신, 별도의 클래스로 두고 기존 메소드의 객체를 전달받아 새로운 기능을 수행하는 것이다.
이렇듯 포장 클래스의 핵심은 신규 동작을 기존 클래스에 추가하지 않으면서 시스템에 추가하는 것이다.
기존 코드에 대한 기존 호출이 많은 경우, 데코레이터 패턴이 효과적이며 많지 않을 경우, 포장 클래스가 효과적이다.
포장 클래스를 적용하는 순서는 다음과 같다.
1.
어느 부분의 코드를 변경해야 하는지 식별한다.
2.
변경이 특정 위치에서 일련의 명령문으로 구현될 수 있다면 포장 대상 클래스를 인수로 받는 클래스를 작성한다. 포장 클래스를 테스트 하네스에서 객체로 생성하기 힘들 경우, 구현체 추출, 인터페이스 추출 기법을 사용한다.
3.
테스트 주도 개발 방법으로 포장 클래스에 새로운 처리를 수행하는 메소드를 작성한다. 또한 기존 클래스의 메소드를 호출하는 메소드를 하나 더 만들어, 기존 클래스의 메소드 명을 가진 메소드에 둘을 함께 위치시킨다.
4.
새로운 동작이 수행될 위치에서 포장 클래스의 인스턴스를 생성한다.
기존 메소드로부터 신규 메소드를 호출하고 싶을 때는 발아 메소드를 사용하며, 기존 메소드의 이름을 변경하고 이 메소드의 호출과 새로운 처리를 하는 신규 메소드를 호출하고 싶을 땐 포장 메소드를 사용한다.
저자는 기존 메소드의 알고리즘이 분명히 이해될 때는 발아 메소드를 사용하고 추가될 신규 기능이 기존 기능과 동등한 수준으로 중요할 때는 포장 메소드를 사용한다.
다음은 저자가 포장 클래스의 사용을 고려하는 기준이다.
1.
추가하려는 동작이 완전히 독립적이며 구현에 의존적인 동작이나 관련 없는 동작으로 기존 클래스를 오염시키고 싶지 않은 경우
2.
클래스가 비대해져서 더 이상 키우고 싶지 않은 경우, 향후 변경을 대비하기 위한 목적으로만 클래스를 포장한다.
이는 작업을 수행하기도 익숙해지기도 매우 어렵다. 매우 많은 수의 책임을 가진 클래스일 경우, 사소한 기능을 추가하기 위해 클래스를 포장하는 것이 어색해보일 수 있기 때문이다.
이런 포장 작업의 정당성을 제대로 설명하려면, 다음을 참고하라.
대규모 코드베이스를 개선할 때 가장 큰 걸림돌은 기존 코드다.
기존 코드를 점진적으로 개선해두는 것은 추후 좋은 설계로 인한 코드 변경의 시간을 절약하는데 매우 도움이 된다.
적어도 처음에는 미련해보일지 모른다. 하지만 포장 클래스로 봉합 지점을 만들어 책임을 시야에서 보이지 않게 만든다면 인지 능력이 과부화될 일이 적어지고 포장하기를 잘했다고 생각하게 될 것이다.

요약

이번 장에서는 기본 클래스의 테스트 루틴이 없을 때, 테스트 루틴을 작성하기 힘든 제약(시간, 의존 관계 복잡) 등이 있을 경우, 변경을 수행할 수 있는 몇 가지 기법을 설명했다.
즉 발아, 포장 기법은 나중에 시간을 투자하는 기법으로 데드라인 추정, 의존 관계 제거 등의 기법들로 해결이 어려울 때, 완전히 새로운 코드를 추가해 코드 변경에 대처하는 방식이다.
설계 관점에서는 이해하기 어려운 부분도 있지만 많은 경우에 이런 기법들을 사용함으로써 봉합 지점을 만들어 새로운 책임과 기존 책임을 분리할 수 있다. 다시 말해, 더 나은 설계에 가까워진다.
그러나 이것이 반복되면 오래되고 거대한 클래스 주변에 포장 클래스 여럿이 중첩된 형태로 널려있는 결과로 이어진다. 너무 남발하지 말자.
테스트로 보호받지 않으며 오래된 거대한 클래스를 반복적으로 조사하다보면 언젠가는 용기있게 리팩토링에 도전할만큼 충분한 정보를 얻게 될 것이다.
⇒ 용기있게 도전하자. 언젠가는 누군가가 치울 쓰레기다. 치우면 성장하는 쓰레기다. 내가 치운다면 내가 성장하게 된다.
지금 시간을 투자할 것인지, 아니면 나중에 시간을 투자할 것인지 내게 주어진 상황과 자원에 기반하여 잘 선택하자.