kok202
레거시 코드 활용 전략

2022. 4. 18. 02:42[공부] 독서/레거시 코드 활용 전략

http://www.yes24.com/Product/Goods/64586851

 

레거시 코드 활용 전략 - YES24

시스템 내에 오래된 코드를 다루는 방법을 배울 수 있다. 오래된 코드, 즉 레거시 코드는 그 코드에 익숙한 사람도 없고, 테스트 루틴도 없어 관리하기 어렵다. 저자는 다년간의 현장 경험과 실

www.yes24.com

코드 레벨에 대한 설명이 많아 이는 생략하고 보편적인 가치 위주로 정리합니다.

(😛) 은 사견입니다.

 

레거시 코드

저자가 생각하는 레거시 코드란 단순히 테스트 루틴이 없는 코드다. 다만 이 정의는 다소 불완전하다. 테스트 루틴이 없는 코드는 나쁜코드다. 코드가 얼마나 훌륭하게 작성돼 있는지는 상관없다. 아무리 깔끔하고 객체 지향적이며 캡슐화가 잘돼 있어도 소용없다. 테스트 루틴이 있으면 코드의 동작을 빠르게 검증하며 수정할 수 있다. 테스트 루틴이 없으면 우리가 작성하고 있는 코드가 좋아지고 있는지 나빠지고 있는지 제대로 알 수 없다.

 

변경 

코드를 변경할 때 변경 대상만 집중하면 안 된다. 어떻게 기존의 동작에 영향을 미치지 않고 유지할 수 있을지 고민해야한다. 불행히도 기존의 동작을 그대로 유지한다는 것은 단순히 코드를 그대로 두는 것 이상의 의미를 갖는다.

변경을 회피해선 안된다. 변경을 자주 하지 않으면 개발자의 실력이 녹슬기 쉽다. 거대한 클래스를 분할하는 작업을 일주일에 두세 번 정도 꾸준히 하지 않으면, 나중에는 감당하기 힘든 수준이 된다.

 

변경 방법

시스템을 변경하는 방법은 크게 두 가지로 나눌 수 있다. 첫째는 "편집 후 기도하기" 이며, 둘째는 "보호 후 수정하기"이다. 안타깝게도 편집 후 기도하는 방식은 아무리 신중하게 주의를 기울여도 그에 비례해서 안전성이 높아진다는 보장이 없다. 보호 후 수정하기의 기본 개념은 변경할 때 안전망을 이용하자는 것이다.

 

회귀 테스트

회귀 테스트란 주기적으로 테스트를 실행해 정상적인 동작 여부를 확인하고 지금까지와 마찬가지로 소프트웨어가 동작하는지 조사하는 것을 의미한다. 변경 대상 코드의 주변에 배치된 테스트 루틴들은 소프트웨어 바이스(고정 장치) 역할을 할 수 있다. 소프트웨어의 동작 대부분을 고정시키고, 변경하고자 하는 부분만 변경하고 있음을 확인할 수 있다는 뜻이다. 불행히도 회귀테스트는 애플리케이션 수준의 테스트로서 간주돼왔다.

 

대규모 테스트의 문제

  1. 오류 위치 파악이 어렵다. 테스트 루틴이 테스트 대상과 멀어질 경우 테스트 실패의 의미를 파악하기 힘들어진다. 
  2. 실행시간이 길다. 테스트 루틴이 길이가 길어질 수록 실행하는데 오랜 시간이 걸린다. 
  3. 커버리지 파악이 힘들다. 코드를 실행시키는 값들의 연결 관계 파악이 어렵다.

 

단위 테스트는 빨라야한다.

실행에 0.1초가 걸리는 단위 테스트는 느린 단위 테스트다. 빠르지 않다면 단위 테스트가 아니다. 테스트를 위한 특별한 작업을 해야하거나 데이터베이스, 네트워크 통신, 파일 시스템을 사용한다면, 단위테스트가 아니다. (😛 테스트의 크기에 관한 정의는 구글의 소프트웨어 엔지니어링 책이 좀 더 잘 정의한 듯)

 

테스트를 추가하기 어려운 상황

테스트 루틴을 작성하고 실행하는데 인스턴스를 생성하기 어려운 상황이 있다. 예를 들어 클래스 생성에 DBConnection 이 필요한 경우다. 이 DB 연결을 테스트 루틴에서 어떻게 처리할 수 있을까? 실제로 DB를 생성해야할까? 그러기 위해선 많은 작업이 수반돼야 할 것이다. DB 를 거치는 테스트는 속도가 느려질 것이다. 또 다른 예시로 Servlet 인스턴스를 생성하는 것은 더 어렵다. 이러한 값들을 아예 인수로 받지 않는 방법에 대해서도 고민해보지만, 코드 변경이 올바른지 확인한다는 테스트 목적을 생각해보면,  이렇게 나머지 코드를 함부로 변경해도 되는지 의문이 들 때가 있다. (😛 결국 추상화가 잘안되서 생기는 문제)

여기서 레거시 코드에 테스트코드를 추가할 때 딜레마가 생긴다. 코드 변경을 하려면 테스트 코드를 배치해야한다. 그런데 테스트 코드를 배치하려면 코드 변경이 필요할 때가 많다.

 

테스트 루틴을 배치할 때 의존 관계를 제거하는 이유

  1. 감지:  코드 내에서 계산된 값에 접근할 수 없을 때 이를 감지하기 위해 의존 관계를 제거한다.
  2. 분리: 코드를 테스트에 넣을 수 없을 때 코드를 분리하기 위해 의존 관계를 제거한다.

분리를 위한 다양한 기법은 있으나 감지는 보통 협업 클래스를 위장하는 기법(fake or mock)을 사용한다.

 

가짜 객체가 진짜 테스트를 지원할 수 있는가?

어떤 사람들은 가짜 객체를 이용한 테스트가 진짜 테스트냐고 반문할 수 있다. 우리는 이러한 테스트를 통해 소프트웨어의 오작동을 모두 알아낼 수 있다고 단언할 수 있는 것은 아니다. 그렇다고 해서 이 테스트 마저 진짜가 아니라고 할 수는 없다. 우리는 테스트 루틴을 작성할 때 분할 정복 접근법을 사용해야한다. 가짜 객체를 이용한 테스트가 중요하지 않은 테스트라고는 할 수 없다.

 

봉합

봉합 지점이란 코드를 직접 편집하지 않고도 프로그램의 동작을 변경할 수 있는 위치를 말한다.

(😛 회사에서 많이 쓰는 support - process 패턴, OCP) 

(😛 기존의 코드를 봉합 지점으로 만드는 방법 중 하나는, 추상화를 이용하여 의존성 주입하는 것이다.)

봉합에는 다양한 방식이 있는데, C 나 C++ 에서 사용할 수 있을 법한 링크 봉합, 전처리기 봉합이 있으며 가상 함수를 이용한 객체 봉합 방식이 존재한다. 일반적으로 객체 봉합은 객체지향 언어에서 가장 적합한 방법이다. 전처리 봉학과 링크 봉함은 의존관계가 매우 복잡하거나 다른 대안이 없을 경우에 최후의 카드로서 사용하는 것이 바람직하다.

 

코드 변경을 위한 의존 관계를 제거하고 테스트 루틴을 작성하는 작업은 개발자의 시간을 많이 뺏앗는다. 하지만 대부분 결국 개발 시간과 시행착오를 줄여 시간을 절약해준다. 그런데 결국이란 대체 언제를 가리키는 것일까? 이는 현실 세계의 딜레마다. 지금 시간을 투자할 것인지, 나중에 시간을 투자할 것인지 선택해야 한다. (* 여기서 말하는 시간 절약이란 테스트 루틴이 오류를 포착함으로써 절얀된 시간과 오류 탐색에드는 모든 시간을 포함하여 말하는 것이다.)

 

데코레이터 패턴

abstract class ToolControllerDecorator extends ToolController {
    protected ToolController controller;
    public ToolControllerDecorator(ToolController controller) {
        this.controller = controller;
    }
    public void raise() {
        controller.raise();
    }
    public void lower() {
        controller.lower();
    }
    public void step() {
        controller.step();
    }
    public void on() {
        controller.on();
    }
    public void off() {
        controller.off();
    }
}

 

컴파일 방화벽

추상화, 인터페이스를 이용한 의존 관계를 제거하면 빌드 속도도 빨라진다. 구체 클래스가 변경되어도 협업에 사용되는 클래스는 인터페이스이기 때문에 상위 모듈의 재빌드가 필요없이 때문이다. 어떻게 보면 이는 "컴파일 방화벽"을 설치한 것이라고 말할 수 있다.

 

LSP [SOLID]

LSP 를 준수하는데 도움이 되는 대략적인 규칙은 다음과 같다.

  1. 가급적 추상이 아닌 실체 메소드를 재정의 하지 않는다. 실체 메소드를 자주 재정의하면 코드가 혼란스러워진다.
  2. 불가피하게 실체 메소드를 재정의 하는 경우, 그 메소드 내에서 재정의 대상 메소드를 호출할 수 있는지 확인한다.

 

테스트 코드 관리

테스트 코드에는 최종적으로 출시되는 배포 코드와 동일한 기준이 적용될 필요가 없다. 테스트 코드를 쉽게 작성하기 위해서라면 public 변수를 정의해서 캡슐화를 위반하는 일도 별 문제는 아니다.

 

숨겨진 의존 관계

(😛 생성자에 의존성을 넣는 것보다 매개변수로 의존성을 건내주는 방식 의존성을 더 드러내는 방법이다.) 생성자 매개변수화 기법은 생성자의 의존 관계를 외부화하는 쉬운 방법인데, 많은 사람들이 이 기법을 잊어버리곤 한다.

 

전역 변수

일반적으로 전역 변수는 몇가지 이유에서 바람직하지 않다. 첫째로 투명하지 않다는 점이다. 일반적으로 어떤 코드를 봤을 때 그 코드가 무엇에 영향을 미치는지 알 수 있는 것이 바람직하다. 하지만 전역 변수가 자주 사용될 경우 단순한 메소드의 동작이 어떤 영향을 미치는지 알 수 없게 되는 경우가 많다. (😛 isActivatedAdmin...)

Account account = new Account();
account.deposit(1);

// 전역 변수를 남발하는 시스템이라면 이 값을 1이라고 확신할 수 있을까?
int balance = account.getBalance();

둘째로 클래스가 어느 전역 변수를 사용 중인지, 제대로 이해한 후에 테스트에 적절한 상태로 설정해야한다는 점에서 테스트를 더 어렵게 만든다는 점이다. 설정해야하는 상태가 테스트 케이스마다 다르다면 상태 설정을 하는 작업은 상당히 지루한 일이 될 것이다.

 

싱글톤

많은 사람들이 전역 변수를 갖기 위해 싱글톤을 생성하곤 한다. 변수를 필요한 장소로 전달하는 것이 어렵기 때문이다.

 

Null 전달하기

지금 작성중인 테스트에서 인스턴스를 만들기 위해 생성자를 사용하고 있다 치자. 생성자에게 전달되는 매개변수중 정말로 필요한 것은 무엇인가? 매개변수중 테스트에 불필요한 값은 Null 전달 하는 기법을 사용할 수 있다. 테스트와 직접 의존 관계를 갖는 것만 주입하고 필요하다면 가짜 객체를 생성해 전달할 수 도 있다. (😛 개인적으로 Builder 를 좋아하는 이유.)

 

테스트하기 어려운 메소드

  • private 메소드
    private 메소드는 테스트하지 않는 것이 낫다. private 메소드를 테스트해야 한다면 그 메소드를 public 으로 만들었어야 한다는 것이다. public 으로 만드는 것이 꺼려진다면 이는 곧 클래스가 너무 많은 책임을 갖고 있음을 의미하며, 클래스를 수정해야한다는 뜻이다.  
  • final 메소드
    언어마다 서브 클래스화를 방지하는 구문을 제공한다. 자바에서는 final 예약어가 있다. 이는 보안에 민감한 클래스를 보호하는 역할을 한다. final 메소드때문에 테스트하기는 어렵다면 개발을 뭔가 잘못한 것이다.  제어 범위를 벅어난 라이브러리에 직접 의존하는등의 이유로 스스로 어려움을 초래한 경우일 확률이 높다.

 

CQRS

하나의 메소드는 명령이나 쿼리여야하며, 두 가지 기능을 모두 가져서는 안된다. 명령은 객체의 상태를 변경할 수 있지만, 값을 반환하지 않는다. 쿼리는 값을 반환하지만 객체를 변경하지 않는다.

 

영향 스케치

변경을 했을 때 어디까지 영향이 있을지를 그려보는 것. 변경의 대상이 되는 클래스를 사용하는 클래스들을 모두 파악한다. 특히 슈퍼 클래스나 서브 클래스가 있을 경우, 조사 대상에서 슈퍼 클래스나 서브 클래스를 사용하는 경우가 빠지지 않도록 주의한다. 변경하려는 대상이 메소드인 경우 메소드가 매개변수의 값을 변경하지 않는지도 파악한다. 전역 변수나 정적 변수를 사용하고 있다면 이 값을 변경하지 않는지도 파악한다.

 

조임 지점

영향 스케치 안에 영향이 집중되는 곳이다. 조임 지점을 테스트하면 다수의 메소드를 테스트할 수 있다. 조임 지점은 자연적인 캡슐화의 경계다. 조임 지점을 찾았다면, 코드의 상당 부분과 관련이 있는 모든 영향이 통과하는 곳을 발견한 것과 같다. 그런 의미에서 조임 지점을 테스트하는 것은 어려운 프로그램 변경 작업을 시작하기에 이상적인 방법이다. 하지만 조임 지점만을 테스트하면 단위 테스트가 점점 소규모 통합 테스트로 커질 수 있다. 단위 테스트를 작성할 때 핵심은 가급적 독립적인 클래스로 테스트를 진행해야 한다는 것이다. 단위 테스트의 목적은 객체들이 전체적으로 올바르게 동작하는지 확인하는 것이 아니라 하나의 객체가 어떻게 동작하는지 확인하는 것이다. 따라서 필요하다면 가짜 클래스를 사용할 필요도 있다. 가짜 클래스를 사용함으로 써 단위 테스트를 단순화 할 수 있기 때문이다.

 

상위 수준 테스트만 작성하고 싶어요.

상위 수준에서의 테스트는 리팩토링에 유용하다. 개발자들은 변경 대상 인터페이스에 대해 다수의 소규모 테스트 루틴들이 존재하면 변경이 러여워진다고 생각하기 쉽다. 하지만 실제로 대부분의 변경이 생각만큼 어렵지 않다. 테스트 코드 변경을 점진적이고 안전하게 진행할 수 있기 때문이다. 상위 수준 테스트가 분명 중요하긴 하지만, 단위 테스트를 대체할 수 없다. 상위 수준 테스트는 단위 테스트를 위한 첫 단계 정도로 간주하는 것이 좋다.

 

교차 지점

특정 변경에 의한 영향을 감지할 수 있는 프로그램의 위치를 말한다. 교차 지점과 변경 지점의 거리가 떨어지면 테스트 설정이 어려워지며 피드백이 효과적이지 못하게 된다. (😛 테스트 후 검증을 위해 호출해야하는 메소드...? 정도로 생각하면 좋을 듯.)

 

문서화 테스트

기존 동작 유지에 피룡한 테스트를 문서화 테스트라고 부른다. 문서화 테스트는 코드 실제 동작의 특징을 나타내는 테스트로 시스템의 현재 동작을 있는 그대로 문서화 하는 테스트다. 문서화 테스트의 목적은 버그를 찾는 것이 아니다. 시스템의 현재 동작과의 차이라는 형태로 나타나는 버그(regression)를 나중에 발견하기 위한 목적이다. 따라서 코드의 실제 동작을 기록하는 것이 주가 되어야한다. 만약 문서화 테스트를 작성하다 버그를 발견하면 어떻게 할까? 상황에 따라 다르다. 시스템이 아직 출시되지 않았다면 수정하는 것이 맞고 아니라면 파급 효과를 고려하여 버그를 수정할지 말지 결정해야한다. 저자는 일반적으로 바로 수정하는 방식을 선호한다.

 

라이브러리 호출

라이브러리 내의 클래스를 직접 호출하는 코드를 여러 곳에 분산시키면 안 된다. 현재 사용중인 라이브러리 코드가 나중에 바뀔 일이 결코 없을 것이라고 생각한다면, 단순히 근거없는 예측에 지나지 않는다.

 

API 호출

시스템은 API 호출로 부터 분리할 수 있는 핵심 로직을 갖고 있어야한다. 기본적으로 두가지 접근 방법이 있다. 첫째로 API 를 포장하는 것이고 둘째로 책임을 기반으로 추출하는 것이다.

 

비대한 클래스

클래스가 커지면 혼란을 야기한다. 무엇을 변경해야할지 다른 클래스에 어떤 영향을 미치는지 파악하기 어려워진다. 협업에 있어서도 conflict 가 빈번해진다. 추가로 테스트 코드 작성도 어려워진다. 비대한 클래스는 책임을 파악하고 적절한 분리 방법을 익혀 분리할 수 있어야한다. 비대한 클래스를 분리하는 방식에는 다음과 같은 방식들이 있다.

  1. 메소드를 분류한다. 이름이 비슷한 메소드를 찾고 접근 제한자를 함께 적어봐서 묶을 만한 메소드가 있는지 찾아본다.
  2. 숨겨진 메소드를 조사한다. 클래스 내에 private 메소드가 많다면 별도의 클래스로 추출해야함을 의미한다.
  3. 변경 가능한 결정사항을 찾는다. 예를 들어 updateScreen 메소드가 있다면 이름만으로 얼마나 많은 일을 하는지, 얼마나 많은 책임을 갖고 있는지 알 수 없다. 그렇다면 클래스를 추출하기 전 메소드 추출 리팩토링을 조금 해두는 것이 좋다. 이를 위해 결정사항을 찾는다. 코드에 얼마나 많은 가정이 포함되어 있는가?
  4. 내부 관계를 찾는다. 인스턴스 변수와 메소드 사이의 관계를 찾는다. 어떤 인스턴스 변수가 일부의 메소드에서만 사용되고 있지 않는가? 클래스 안에는 이른바 덩어리라고 부른 넋이 존재한다. 이 덩어리를 찾기 위한 또 다른 기법은 클래스 내부의 관계에 대해 간단한 스케치를 그리는 것인데 이를 기능 스케치라고 부른다.
  5. 주요 책임을 찾는다. 클래스의 책임을 한 개의 문장으로 기술하도록 노력한다.
  6. 스크래치 리팩토링을 수행한다.
  7. 현재 작업에 집중한다.

 

인터페이스 분리 원칙 [SOLID]

거대한 클래스의 경우 모든 클라이언트가 클래스 내의 모든 메소드를 사용하는 것은 거의 볼 수 없다. 대체로 특정 클라이언트마다 주로 사용하는 메소드들이 다르기 때문이다. 따라서 메소드 그룹마다 인터페이스를 작성하고 거대한 클래스에서 인터페이스를 구현하면, 클라이언트들은 특정 인터페이스를 통해 거대한 클래스를 참조할 수 있다.

 

기능 스케치

기능 스케치는 영향 스케치와 매우 유사한데, 화살표 방향이 반대라는 중요한 차이점이 존재한다.

 

중복 제거

중복 코드를 제거하면 설계의 진짜 모습이 드러난다. 중복 제거는 설계를 가다듬는 강력한 방법이다. 설계가 유연해질 뿐만 아니라 변경을 더욱 빠르고 쉽게할 수 있도록 해준다.

 

개방/폐쇄 원칙 [SOLID]

설계가 잘돼 있다면 신규 기능 추가를 위해 그리 많은 코드 변경을 하지 않아도 된다는 뜻이다. 대부분 중복 제거를 잘한 코드는 자연스럽게 개방/폐쇄 원칙을 따르게 된다.

 

괴물 메소드

  • 불릿 메소드: 들여쓰기가 거의 돼있지 않은 메소드를 말한다.
  • 혼잡 메소드: 들여쓰기 된 한개의 대규모 단락으로 구성된 메소드를 말한다.

 

리팩토링

괴물 메소드를 리팩토링할 때는 가급적 추가 클래스, 추가 기능을 넣지 않도록한다. 일단 이상해보여도 메소드 분할을 진행한다. 메소드를 별도의 클래스로 옮기는 것은 어떤 방향으로 변경하는 것이 올바른지 명확해진 이후에도 언제든지 할 수 있다. 작은 조각부터 먼저 추출해서 점진적으로 메소드를 개선해 나가야한다.

프로그래밍을 할 때 한 번에 큰 부분을 건드리기 쉽다. 그렇게 되면 결국 코드가 실제로 어떤 일을 하는지 신중히 작업하기 보다 그저 코드가 동작하게 만드는 데 급급해질 것이다. 천 리 길도 한 걸음 부터다. 

 

천재형 프로그래머

수 많은 상태를 기억하고 코드를 보자마자 이해할 수 있는 능력은 매우 유용한 재능일 수 있다. 개발자들은 이러한 이른바 천재형 프로그래머 이야기에 깊이 매료 되는 경우가 많다. 하지만 그것이 실제로 더 나은 의사 결정을 내리는 데 대단한 도움을 주는 것은 아니다. 현재의 저자는 각 언어의 세세한 점에 대해서는 조금 덜 알지라도 예전보다 더 나은 프로그래머라고 생각한다. 판단력은 중요한 프로그래밍 스킬이다. 천재 프로그래머를 어설프게 모방하다가 곤란한 상황에 빠질 수 있다.

 

의존 관계 제거 기법

  1. 매개변수 적응
    클래스에 인터페이스 추출을 사용할 수 없거나 매개변수를 가짜로 만들기 어려울 때 사용할 수 있는 기법. (😛 테스트를 위한 매개변수를 생성하기 조차 어려울 때 ex. HttpSerlvetRequest) 인터페이스에 세부 구현이 아니라 책임을 전달하도록 한다. 이를 통해 코드의 가독성과 유지 보수성이 향상된다.
  2. 메소드 객체 추출
    어떤 경우에는 클래스를 단독으로 인스턴스화 하는데 많은 시간이 걸릴 수 있다. 이 리팩토링 기법의 개념을 한마디로 말하면 대규모 메소드를 새로운 클래스로 바꾸는 것이다.
  3. 정의 완성
    일부 언어에서는 어떤 타입을 선언한 후 다른 곳에서 이를 정의할 수 있다. 이 기능을 활용해 의존 관계를 제거할 수 있다.
  4. 전역 참조 캡슐화
    전역 변수를 클래스로 옮김으로써 코드를 분리한다.
    (😛 전역 참조를 내재화 하고 캡슐화하기)
  5. 정적 메소드 드러내기
  6. 호출 추출과 재정의
  7. 팩토리 메소드 추출과 재정의
  8. get 메소드 추출과 재정의
  9. 구현체 추출
  10. 더 복잡한 예제
  11. 인터페이스 추출
    많은 언어에서 사용하는 가장 안전한 의존 관계 제거 기법 중 하나. 혹시 순서를 잘못 따라도 컴파일러가 즉시 알려주기 때문에 버그가 생길 가능성이 매우 낮다.
  12. 인스턴스 위임 도입
  13. 정적 set 메소드 도입
    (😛 싱글톤을 테스트하기 위해 어거지로 사용하는 방식. 본질적으로 싱글톤은 두개가 존재해선 안되나 런타임용 싱 톤과 테스트용 싱글톤이 필요하기 때문에 어쩔 수 없이 사용해야하는 경우에 쓴다.)
  14. 연결 대체
    (😛 절차 지향 언어에서 Linke 를 이용한 테스트 용 다형성을 지원하는 방법이라고 이해됨)
  15. 생성자 매개변수화
    생성자 안에서 하나의 객체를 생성하는 경우, 가장 쉽게 대체하는 방법은 생성을 외부화하는 것이다.
    (😛 new 인스턴스를 생성자로 전달하는 새로운 생성자를 따로 만드는 방법)
  16. 메소드 매개변수화
  17. 매개변수 원시화
  18. 특징 끌어올리기
  19. 의존 관계 밀어 내리기
    현재 클래스를 추상으로 만들고 새로운 배포용 클래스가 될 서브 클래스를 하나 생성한다. 그리고 모든 골칫거리 의존 관계를 그 클래스 안으로 밀어 내린다.
  20. 함수를 함수 포인터로 대체
    (😛 회사에서 테스트 가능하게 구조를 변경하는 방법에 대해서 논의한 적이 있는데, Repository 대신 메소드를 전달하자는 의견을 낸적이 있음. 이와 상당히 유사한 듯. 절차 지향 언어에서 사용해볼만하다.)
  21. 전역 참조를 get 메소드로 대체
  22. 서브클래스화와 메소드 재정의
    객체 지향 프로그램에서 의존 관계를 제거하는데 사용되는 주요 기법. 실제로 다른 기법은 이 기법의 변종이라고 할 수 있다.
  23. 인스턴스 변수 대체
  24. 템플릿 재정의
  25. 텍스트 재정의

(😛 결국 모든 기법이 테스트를 위한 fake 를 전달하기 위해 사용되는 방법들이라고 보인다. 리플렉션 같은 기능을 이용한 강제 fake 주입 방법보다, 설계를 이용하여 fake 를 전달하도록 해서 유연한 설계에 더 가까워지도록 할 수 있는 방법들인 듯)

 

매개변수 적용

매개변수 적용 기법 적용 전

public class ARMDispatcher {
    public void populate(HttpServletRequest request) {
        String[] values = request.getParameterValues(pageStateName);
        if (values != null && values.length > 0) {
            marketBindings.put(pageStateName + getDateStamp(), values[0]);
        }
    }
}

매개변수 적은 기법 적용 후

public class ARMDispatcher
    public void populate(ParameterSource source) {
        String values = source.getParameterForName(pageStateName);
        if (value != null) {
            marketBindings.put(pageStateName + getDateStamp(), value);
        }
    }
}

class ServletParameterSource implements ParameterSource {
    private HttpServletRequest request;
    public ServletParameterSource(HttpServletRequest request) {
        this.request = request;
    }
    String getParameterValue(String name) {
        String[] values = request.getParameterValues(name);
        if (values == null || values.length < 1)
            return null;
        return values[0];
    }
}

 

생성자 매개변수화

생성자 매개변수화 적용 전

public class MailChecker {
    public MailChecker(int checkPeriodSeconds) {
        this.receiver = new MailReceiver();
        this.checkPeriodSeconds = checkPeriodSeconds;
    }
}

생성자 매개변수화 적용 후

public class MailChecker {
    public MailChecker(int checkPeriodSeconds) {
        this(new MailReceiver(), checkPeriodSeconds);
    }
    public MailChecker(MailReceiver receiver,
        int checkPeriodSeconds) {
        this.receiver = receiver;
        this.checkPeriodSeconds = checkPeriodSeconds;
    }
}

 

기타

  • xUnit 테스트 프레임 워크는 원래 켄트 벡이 스몰토크로 작성한 것이다. 나중에 켄트벡과 에릭 감마가 자바로 이식한 것이 JUnit 이다.
  • 좋은 설계는 테스트 가능한 설계며, 테스트 불가능한 설계는 나쁜 설계다.
  • 아키텍트는 개발 팀에 참여해 매일 함께 일해야한다. 아키텍트가 따로 있는 것은 좋지만, 아키텍쳐를 유지하려면 팀원 전체가 아키텍처가 무엇인지 알고 관심을 기울여야 한다. 
  • 신규 기능을 추가할 때보다 레거시 코드를 다룰 때가 설계 스킬을 발휘할 기회가 훨씬 많다.
  • 인터페이스 수준에서 단일 책임 원칙을 도입하려면 더 많은 작업이 필요하다. 구현 수준에서 단일 책임 원칙을 제대로 도입하면, 인터페이스 수준에서도 도입하기가 쉽다. 
  • 클래스를 추출할 때 발생하는 버그 중에서 가장 찾기 어려운 것은 상속 관련 버그다. 메소드를 어떤 클래스에서 다른 클래스로 옮기는 것은 상대적으로 매우 안전한 작업이다. 컴파일러가 사라진 메소드에 대한 알람을 띄워주기 때문이다. 하지만 상속을 이용하고 있는 클래스라면 이야기가 달라진다. 메소드를 옮기고 나면 기초 클래스 내에 있는 동일한 이름의 메소드 호출이 되기 때문에 컴파일러가 이를 찾지 못한다.
  • 약어 사용은 지양한다. (378p)
  • 일반적으로 기본 객체들을 변경하는 set 메소드를 제공하는 것은 좋지 않은 관례다. 이러한 set 메소드들은 클라이언트들이 객체가 살아있는 동안 그 객체의 동작을 완전히 변경하게 만들기도 한다.

 

(😛 TDD, DDD, SOLID, OOP, 설계 공부를 하면할 수록 모두가 가리키고 있는 어떤 하나의 그림이 있다는 것을 느낀다. 결국 테스트하기 쉬운 코드가 좋은 코드고 테스트를 고려한 코드가 확장에도 유리한 코드다.)