본문 바로가기
읽고쓰고

[책] 클린코드 - 애자일 소프트웨어 장인 정신 (7, 8, 9장)

by 별토끼. 2021. 4. 14.
반응형

클린코드(w.로버트 C. 마틴)를 읽고 개인적인 학습을 위해 요약한 글 입니다. 문제 시 하단에 댓글 달아주시면 감사하겠습니다.

7. 오류 처리

오류코드보다는 예외를 사용해라

예외를 던지면 호출자 코드가 더 깔끔해진다. 논리가 오류 처리 코드와 뒤섞이지 않기 때문이다.

try-catch-finally 문부터 작성하라

강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법 권장.

unchecked 예외를 사용하라 (P.133)

하위 단계에서 코드 변경 시, 상위 단계 메서드 선언부를 전부 고쳐야한다. 모듈 관련 코드가 전혀 바뀌지 않았더라도, 선언부가 바뀌었으므로 다시 빌드한 다음 배포해야 한다.

호출자를 고려해 예외 클래스를 정의하라

ACMEPort port = new ACMEPort(12);
try{
    port.open();
} catch (DeviceResponseException e) {
    reportPortError(e);
    looger.log("Device response exception", e);
} catch (ATM1212UnlockedException e) {

} catch (GMXError e) {

}

위는 형편없이 오류를 분류한 사례이다.
호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하면 된다.

LocalPort port = new LocalPort(12);
try{
    port.open();
} catch (PortDeviceFailure e) {
    reportError(e);
    logger.log(e.getMessage(), e);
} finally {
 ..
}

LocalPort클래스는 단순히 ACMEPort클래스가 던지는 예외를 잡아 변환하는 WrapperClass일 뿐이다.

public class LocalPort{
    private ACMEPort innerPort;
    public LocalPort(int portNumber) {
        innerPort = new ACMEPort(portNumber);
    }

    public void open(){
        try{
            innerPort.open();
        }  catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e);
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceFailure(e);
        } catch (GMXError e) {
            throw new PortDeviceFailure(e);
        }
    }
}

실제로 외부 API를 사용할 때는 감싸기 기법이 최선이다. 의존성이 크게 줄어든다. 다른 라이브러리로 갈아타는 비용도 적다.

정상 흐름을 정의하라

오류 처리가 잘 분리된 코드를 작성하다 보면 오류 감지가 프로그램 언저리로 밀려난다. 멋진 처리 방식이나, 중단이 적합하지 않은 때도 있다. 비용 청구 애플리케이션에서 총계를 계산하는 코드이다.

try{
    MealExpenses expenses = expenseReportDAO.getMeal(employee.getID());
    m_total += expenses.getTotal();
} catch (MealExpensesNotFound e) {
    m_total += getMealPerDiem();
}

예외 논리를 따라가기 어렵게 만든다. 청구한 식비가 없다면, 일일 기본 식비를 반환하는 MealExpense 객체를 반환한다.

public class PerDiemMealExpenses implements MealExpenses {
    public int getTotal() {
        // 기본값으로 일일 기본 식비를 반환한다.
    }
}

null을 반환하지 마라

null을 반환하는 코드는 일거리를 늘린다. 또, 호출자에게 문제를 떠넘긴다. 차라리, 빈 리스트 등을 반환한다면 코드가 더 깔끔해질 것이다.

public List<Employee> getEmployees() {
    if (직원이 없다면)
        return Collections.emptyList();
}

8. 경계

소프트웨어를 가져다 사용할 때 그 경계를 깔끔하게 처리하려면 어떻게 하는게 좋을까?

Map을 예로 들자면, 경계 인터페이스인 Map을 Sensors 안으로 숨긴다.

Map sensors = new HashMap();
Sensor s = (Sensor)sensors.get(sensorId);

의 모습은 클라이언트에 Object를 올바른 유형으로 변환할 책임이 있다. 하지만 아래처럼 바꾼다면 경계 인터페이스인 Map을 Sensors 안으로 숨긴다. 필요한 인터페이스만 제공하기도 하여, 오용하기 어렵다.

public class Sensor{
    private Map sensors = new HashMap();
    public Sensor getById(String id) {
        return (Sensor) sensors.get(id);
    }
}

외부코드 먼저 익히기

외부코드가 완벽한 것은 아니다. 학습테스트를 통해 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 통제된 환경에서 내가 외부코드를 제대로 이해하는지 확인하는 셈이다.

학습 테스트는 중요하다

새로운 경계 인터페이스를 log4j로 설명하고 있다. 테스트 작성 과정에서의 의식의 흐름을 설명한다. 이렇게 해야지. (P.148)

학습테스트는 예상대로 도는지 보기 위함이다. 투자 비용 대비 성과가 더 크다. 패키지 새 버전이 나온다면 학습 테스트를 돌려 차이가 있는지 확인한다. 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다.

안만들어진 코드 사용하기

안만들어진 코드란 유형을 모르는 코드를 말한다.
이에 대한 방법은 설계되지 않은 API를 대상으로 자체적 인터페이스를 정의할 수 있다. transmitter라는 클래스를 만든 후 transmit라는 메서드를 추가했다. 인터페이스는 관련 도메인과 자료 스트림을 입력으로 받았다. 이러한 형태는 인터페이스를 전적으로 통제할 수 있다. 또한, 가독성과 코드 의도가 분명해진다.

설계가 우수하다면 재작업이 요구되지 않는다. 통제 못하는 코드는 향후 변경비용이 너무 커지지 않도록 주의해야한다. 경계 위치의 코드는 깔끔히 분리한다.

9. 단위 테스트

TDD 법칙

  1. 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  3. 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

이렇게 일하면 매일 수십개의 테스트 케이스가 나온다. 하지만 실제 코드와 맞먹는 방대한 테스트 코드는 심각한 관리 문제를 유발한다.

깨끗한 테스트 코드 유지하기

실제 코드가 진화하면 테스트 코드도 변해야 한다. 그런데 테스트 코드가 지저분하다면? 테스트 코드가 복잡할수록 실제 코드를 짜는 시간보다 테스트 케이스를 추가하는 시간이 더 걸린다. 새 버전 출시마다 유지보수 비용도 늘어난다. 그러다 결국 테스트 슈트를 폐기해야 하는 상황이 온다. 결국 테스트 슈트도 없고, 얼기설기 뒤섞인 코드, 불만족 가득한 고객만 남는다.

테스트는 유연성, 유지보수성, 재사용성을 제공한다

테스트 케이스는 실제 코드를 유연하게한다. 테스트 케이스가 있으면 변경이 두렵지 않다. 별다른 우려 없이 변경할 수 있다. 자동화된 단위 테스트 슈트는 설계와 아키텍처를 최대한 깨끗하게 보존해주는 열쇠이다.

깨끗한 테스트 코드

가독성, 가독성, 가독성. 테스트 코드에서 제일 중요한 것이다.
잡다하고 세세한 코드를 없애야한다. 진짜 필요한 자료 유형과 함수만 사용한다.

도메인에 특화된 테스트 언어

P.160에서 표현하는 코드는 도메인에 특화된 언어로 테스트 코드를 구현했다. 시스템 조작 API를 사용하는 대신 API 위에다 함수와 유틸리티를 구현한 후 그것을 사용하므로 가독성이 높아진다.

테스트 당 개념 하나

이것 저것 잡다한 개념을 연속으로 테스트하는 긴 함수는 피한다. 여러 개념을 한 함수로 몰아넣으면 독자가 각 절이 거기에 존재하는 이유와 각 절이 테스트하는 개념을 모두 이해해야 한다. P.166은 assert문이 여럿이라는 것이 문제가 아닌, 테스트 함수에서 여러 개념을 테스트한다는 사실이 문제이다. 이렇게 장황하게 짜지 말자.

F.I.R.S.T

꺠끗한 테스트는 다섯 가지 규칙을 따른다.

  1. Fast
    테스트는 빨리 돌아야한다. 자주 돌려야하니까.
  2. Independent
    서로 의존하면 안 된다. 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안된다.
  3. Repeatable
    테스트는 어떤 환경에서도 반복 가능해야한다.
  4. Self-Validating
    bool값으로 결과를 내야한다. 성공아니면 실패다. 통과 여부를 알리고 로그 파일을 읽게 만들어서는 안 된다.
  5. Timely
    테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다. 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다. (테스트 불가능하게 설계할지도 모른다)

여기서 설명한 깨끗한 테스트 코드는 겉핥기 정도 밖에 안했다고 한다. 그만큼 고려해야할 것이 많고 중요하다. 유연성, 유지보수성, 재사용성을 위해 깨끗하게 관리하자.

반응형

댓글