7 ~ 9 : 오류 처리, 경계, 단위 테스트

7장 : 오류 처리

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

public class DeviceController {
    ...
    public void sendShutDown() {
        DeviceHandle handle = getHandle(DEV1);
        
        if (handle != DeviceHandle.INVALID) {
            retrieveDeviceRecord(handle);
            
            if (record.getStatus() != DEVICE_SUSPENDED) {
                pauseDevice(handle);
                clearDeviceWorkQueue(handle);
                closeDevice(handle);
            } else {
                logger.log("Device suspended. Unable to shut down");
            } 
        } else {
            logger.log("Invalid handle for : " + DEV1.toString());
        }
    }
    ...
}

앞서 뒤섞였던 개념, 즉 디바이스를 종료하는 알고리즘과 오류를 처리하는 알고리즘을 분리하며 코드 품질이 나아졌다.

Try-Catch-Finally문부터 작성하라

  • 예외에서 프로그램 안에다 범위를 정의한다는 사실은 매우 흥미롭다. try-catch-finally 문에서 try 블록에 들어가는 코드를 실행하면 어느 시점에서든 실행이 중단된 후 catch 블록으로 넘어갈 수 있다.

  • 어떤 면에서 try 블록은 트랜잭션과 비슷하다. try 블록에서 무슨 일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다. 그러므로 예외가 발생할 코드를 짤 때는 try-catch-finally 문으로 시작하는 편이 낫다.

예제

@Test(expected = StorageException.class) 
public void retrieveSecrionShouldThrowOnInvalidFileName() {
    sectionStore.retrieveSection("invalid - file");
}
  • try-catch 구조로 범위를 정의했으므로 TDD를 사용해 필요한 나머지 논리를 추가한다. 나머지 논리는 FileInputStream을 생성하는 코드와 close 호출문 사이에 넣으며 오류나 예외가 전혀 발생하지 않는다고 가정한다.

  • 먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다. 그러면 자연스럽게 try 블록의 트랜잭션 범위부터 구현하게 되므로 범위 내에서 트랜잭션 본직을 유지하기 쉬워진다.

미확인(unchecked) 예외를 사용하라

확인된 오류가 치르는 비용

  • 확인된 예외는 OCP(Open Closed Principle)를 위반한다. 하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다. 일반적인 애플리케이션은 의존성이라는 비용이 이익보다 크다.

예외에 의미를 제공하라

  • 예외를 던질 때는 전후 상황을 충분히 덧붙인다. 그러면 오류가 발생한 원인과 위치를 찾기가 쉬워진다.

  • 오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실해 유형도 언급한다. 애플리케이션이 로깅 기능을 사용한다면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

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

  • 오류를 분류하는 방법은 다양한데, 오류가 발생한 위치로 분류가 가능하다. 하지만 애플리케이션에서 오류를 정의할 때 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.

  • 감싸는 클래스를 작성하면 특정 업체가 API를 설계한 방식에 발목 잡히지 않는다.

정상 흐름 정의하라

  • 비즈니스 논리와 오류 처리가 잘 분리된 코드를 작성하면, 대부분 깨끗하고 간결한 알고리즘으로 보이지만, 오류 감지가 프로그램 언저리로 밀려난다.

  • 특수 사례 패턴은 클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식이다. 그러면 클래스나 객체가 예외적인 상황을 캡슐화해서 처리하므로 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.

null을 반환하지 마라

  • null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다.

  • 메서드에서 null을 반환하고픈 유혹이 든다면 그 대신 예외를 던지거나 특수 사례 객체를 반환한다.

null을 전달하지 마라

  • 메서드에서 null을 반환하는 방식도 나쁘지만 메서드로 null을 전달하는 방식은 더 나쯔다. 정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피한다.

8장 : 경계

시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 때로는 패키지를 사고, 때로는 오픈 소스를 이용한다. 어떤 식으로든 이 외부 코드를 우리 코드에 깔끔하게 통합 해야만 한다. 이 장에서는 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴본다.

외부 코드 사용하기

  • Map과 같은 경계 인터페이스를 이용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다. Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다.

경계 살피고 익히기

  • 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 것을 학습 테스트라 부른다.

  • 학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다. 통베된 환경에서 API를 제대로 이해하는지를 확인하는 셈이다. 학습테스트는 API를 사용하려는 목적에 초점을 맞춘다.

log4j 익히기

학습 테스트는 공짜 이상이다

  • 학습 테스트는 패키지가 예상대로 도는지 검증한다.

  • 학습 테스트를 이용한 학습이 필요하든 그렇지 않든, 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다.

아직 존재하지 않는 코드를 사용하기

  • 경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.

  • 우리에게 필요한 경계 인터페이스 구현

깨끗한 경계

  • 경계에서는 변경과 같은 흥미로운 일이 많이 벌어진다. 소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.

  • 경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다.

  • 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. Map에서 봤듯이, 새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자. 어느 방법이든 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.

9장 : 단위 테스트

TDD 법칙 세 가지

  • 첫째 법칙 - 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.

  • 둘째 법칙 - 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다 .

  • 셋째 법칙 - 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

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

테스트 코드는 실제 코드 못지 않게 중요하다.

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

  • 테스트 코드를 깨끗하게 유지하지 않으면 결국은 잃어버린다. 그리고 테스트 케이스가 없으면 실제 코드를 유연하게 만드는 버팀목도 사라진다. 코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트다. 이유는 테스트 케이스 있으면 변경이 두렵지 않다는 것이다.

  • 테스트 케이스가 없다면 모든 병경이 잠정적인 버그다. 아키텍처가 아무리 유연하더라도, 설계를 아무리 잘 나눴더라도, 테스트 케이스가 없으면 개발자는 변경을 주저하게 된다.

💡 실제 코드를 점검하는 자동화된 단위 테스트 슈트는 설계와 아키텍처를 최대한 깨끗하게 보존하는 열쇠다.

깨끗한 테스트 코드

  • 깨끗한 테스트 코드를 만드려면 가독성이 매우 중요하다.

  • 테스트 코드에서 가독성을 높이려면 명료성, 단순성, 풍부한 표현력이 필요하다. 테스트 코드는 최소의 표현으로 많은 것을 나타내야 한다.

BUILD-OPERATE-CHECK 패턴

  1. 테스트 자료를 만든다.

  2. 테스트 자료를 조작한다.

  3. 조작한 결과가 올바른지 확인한다.

public void testGetpageHierarchyAsXml() throws Exception {

    makePages("PageOne", "PageOne.ChildOne", "PageTwo");
    
    submitRequest("root", "type:pages");
    
    assertResponseIsXML();
    assertResponseContains(
        "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
    );
}

public void testSymbolicLinksAreNotINXmlPageHierarchy() throws Exception {

    WikiPage page = makePage("PageOne");
    makePages("PageOne.ChildOne", "PageTwo");
    
    addLinkTo(page, "PageTwo", "SymPage");
    
    submitRequest("root", "type:pages");
    
    assertResponseIsXML();
    assertResponseContains(
        "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
    );
}

public void testGetDataAsXml() throws Excpetion {
    makePageWithContent("TestPageOne", "test page");
    submitRequest("TestPageOne", "type:data");
    
    assertResponseXML();
    assertResponseContains("test page", "<Test");
}

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

  • 위 테스트는 도메인에 특화된 언어로 테스트 코드를 구현하는 기법을 보여준다. 흔히 쓰는 시스템 조작 API를 사용하는 대신 API 위에다 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용하므로 테스트 코드를 짜기도 읽기도 쉬워진다.

  • 이렇게 구현한 함수와 유틸리티는 테스트 코드에서 사용하는 특수 API가 되며, 테스트를 구현하는 당사자와 나중에 테스트를 읽어볼 독자를 도와주는 테스트 언어이다.

이중 표준

  • 실제 환경과 테스트 환경은 요구사항이 판이하게 다르다.

  • 실제 환경에서는 절대로 안 되지만 테스트 환경에서는 전혀 문제 없는 방식이 있다. 대개 메모리나 CPU 효율과 관련 있는 경우다. 코드의 깨끗함과는 철저히 무관하다.

테스트 당 assert 하나

  • 테스트 당 assert 문이 단 하나인 함수는 결론이 하나라서 코드를 이해하기 쉽고 빠르다.

public void testGetpageHierarchyAsXml() throws Exception {
    givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
    
    whenRequestIsIssued("root", "type:pages");
    
    thenResponseShouldBeXML();
}

public void testGetPageHierarchyHasRightTags() throws Exception {
    givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
    
    whenRequestIsIssued("root", "type:pages");
    
    thenResponseShouldContain(
        "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"
    );
}
  • 함수 이름을 바꿔 given-when-then이라는 관례를 사용했다.

  • 테스트를 분리하면 중복되는 코드가 많아지는데, TMPLATE METHOD 패턴을 사용하여 중복을 제거할 수 있다.

    • given/when 부분을 부모 클래스에 두고 then 부분을 자식 클래스에 두면 된다.

    • 아니면 완전히 독자적인 테스트 클래스를 만들어 @Before 함수에 given/when 부분을 넣고 @Test 함수에 then 부분을 넣어도 된다.

    • 모두 배보다 배꼽이 더 큰 것을 감안하면 assert 문을 여럿 사용하는 편이 좋을지도

TEMPLATE METHOD

테스트 당 개념 하나

  • 어쩌면 "테스트 함수마다 한 개념만 테스트하라"는 규칙이 더 낫겠다.

💡 "개념 당 assert 문 수 최소로 줄이기" + "테스트 함수 하나는 개념 하나만 테스트 하기 "

F.I.R.S.T

깨끗한 테스트가 따르는 다섯 가지 규칙

  • 빠르게 (Fast) - 테스트는 빨라야 한다.

  • 독립적으로 (Independent) - 각 테스트는 서로 의존하면 안 된다.

  • 반복가능하게 (Repeatable) - 테스트는 어떤 환경에서도 반복 가능해야 한다.

  • 자가검증하는 (Self-Validating) - 테스트는 부울(bool) 값으로 결과를 내야 한다. 성공 아니면 실패다.

  • 적시에 (Timely) - 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다.

https://weheartit.com/entry/300652990

Last updated