본문 링크 (Original Link)

Swift의 Assertion 살펴보기

2017.09.25

#

by John Sundell, translated by pilgwon

Assertion은 테스트를 작성할 때 필수일 뿐만 아니라, 그들은 코드를 더 쉽게 디버깅 가능하고 예측가능하게 작성하기 위해서 사용할 때도 매우 유용합니다.

스위프트는 타입 시스템을 코드를 더 믿을 수 있게 만드는 데에 많은 초점을 두고 있음에도 불구하고, 우리는 특정 조건이 충족되었는지 100% 확신할 수 없는 경우가 있습니다. 그러한 상황에서 잘 만들어진 Assert는 어떤 문제인지 확인하는 과정에 매우 도움이 될 수 있습니다.

스위프트에서 실패하는 올바른 방법 선택하기로 돌아가서, 우리는 우리가 스위프트에서 처리할 수 있는 다양한 오류 핸들링 매커니즘과 어떤 상황에서 적용할 것인지 알아보았습니다. 이번 주에는, 특별히 Assertion에 조금 더 깊이 들어가봅시다. 이것이 어떻게 작동하는지, 다양한 확인에 assert() 함수를 어떻게 구현할 것인지 알아볼 것입니다.

언제 assert할 것인가

실제 코드에서, Assertion은 디버그 빌드에서만 평가되는 되돌릴 수 없는 오류를 발생할 수 있는 방법을 제공합니다. 예를 들어, 그들은 개발자(사용자 말고)에게 무언가가 잘못됐다고 말하기에 완벽합니다. 실행을 기대하지 않는 코드 경로에 assert를 배치하는 것은 논리적인 문제를 찾아내고 오류를 빠르게 없앨 수 있는 훌륭한 방법입니다.

예를 들어, 우리가 UITableViewDataSource의 dequeue된 셀이 특정 커스텀 클래스가 될 것으로 예상하는 구현이 있다고 해봅시다. 강제 캐스팅을 사용하고 크래쉬를 일으키는 것보다, 부정확한 셀타입을 만난 것에 대비해 assertionFailure을 호출하는 것은 동료 개발자(아마도 미래의 당신일 수도 있습니다 😅)가 무엇이 잘못됐는지 이해할 수 있게 해줄 것입니다:

func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let anyCell = tableView.dequeueReusableCell(withIdentifier: "cell",
                                                for: indexPath)

    guard let cell = anyCell as? MyCustomCell else {
        assertionFailure("예상하지 못 한 셀 타입: \(type(of: anyCell))")
        return anyCell
    }

    // 셀 설정
    ...
}

당연히도, assert의 가장 보편적인 사용법은 우리의 프로덕션 코드에는 없습니다 - 우리의 테스트에 있고 assertion의 가족인 XCTAssert를 사용하는 곳에서 값과 객체가 우리의 예상과 맞는지 확인할 때 사용합니다.

func testAddingNumbers() {
    calculator.number = 5
    calculator.add(3)
    XCTAssertEqual(calculator.number, 8)
}

살펴보기

그러면 assert는 어떻게 작동하는 걸까요? 그들의 구현 방법은 실제론 꽤 쉽지만, 그들은 더 자세히 살펴볼 흥미로운 몇가지 컴파일러의 기능을 사용합니다. 여기 스위프트 표준 라이브러리에서 assert의 구현 방식이 어떻게 생겼는지 보여줍니다:

func assert(_ condition: @autoclosure () -> Bool,
            _ message: @autoclosure () -> String,
            file: StaticString = #file,
            line: UInt = #line) {
    // 디버그 빌드에선 평가하지 마세요
    guard isDebugAssertConfiguration() else {
        return
    }

    // 조건문을 실행하세요 (false == failure)
    // Execute the condition (false == failure)
    if !condition() {
        // assert 및 오류를 보고하기 위해 호출을 시스템 API로 전달합니다
        _assertionFailure("Assertion failed", message(), file: file, line: line)
    }
}

위에서 볼 수 있듯이, assert의 함수 서명이 사용하는 몇가지 영리한 요령은 API를 사용하기 쉽게 만들어줍니다.

먼저, @autoclosure를 사용해서 비 디버그 설정 설정에서 표현식을 평가하지 않도록 합니다. 만약 당신이 @autoclosure에 대해 더 배우고 싶고 이걸 이용해서 나만의 API를 어떻게 만드는지에 대해 알고 싶으면 스위프트 API를 디자인할 때 @autoclosure 사용하기를 확인해보세요.

또한 스위프트의 제한된(하지만 존재하고 있는) 사전 처리 매크로를 컴파일러가 어떤 함수가 호출했는지 자동으로 파일 이름과 줄 번호 를 채워줍니다. Xcode는 자동완성에서 심지어 매크로의 이 타입(#file, #line & #column과 같이)을 사용해서 파라미터를 숨기므로 API 사용에 영향을 미치지 않고 이러한 타입의 정보를 전달할 수 있습니다.

이제 어떻게 작동하는지 알았으니, 우리의 것을 알아봅시다!

스위프트 표준 라이브러리 중 제가 가장 좋아하는 것 중 하나는 쓰여진 컴파일러의 기능과 기술은 스위프트 프로그램을 작성하는 모든 사람에게 사용가능하다는 것입니다. 이것은 우리가 실제로 기존과 같은 스타일로 만들 수 있지만 더 많은 특화된 assertion 기능을 제공하는 우리의 assert 함수를 만들 수 있다는 것을 의미합니다.

우리가 우리의 클래스가 무언가 잘 못 됐을 때 정확한 에러를 던질 수 있는지 확인하는 테스트를 작성하고 싶다고 해봅시다. 우리의 테스트 코드를 깔끔하고 읽기 쉽게 만들기 위해, 우리는 아래와 같은 것을 쓸 수 있기를 바랍니다:

class FileLoaderTests {
    func testLoadingNonExistingFileThrows() {
        let loader = FileLoader()

        assert(loader.loadFile(at: "Not a file"),
               throwsError: FileLoader.Error.missingFile)
    }
}

저건 커스텀 assert 함수로 쉽게 달성할 수 있습니다. 우리는 표준 라이브러리에서 수행되는 방법과 매우 유사하게 정의합니다:

public func assert<T, E: Error>(
    at file: StaticString = #file,
    line: UInt = #line,
    _ expression: @autoclosure () throws -> T,
    throwsError errorExpression: @autoclosure () -> E
) where E: Equatable {
    do {
        // 표현식을 평가하고 어떤 결과값과 함께 던집니다 (throw)
        _ = try expression()

        // 여전히 실행되면, 그것은 표현식이 던져지지 않았다는 의미입니다
        XCTFail("Expected expression to throw", file: file, line: line)
    } catch let thrownError as E {
        let expectedError = errorExpression()

        XCTAssert(thrownError == expectedError,
                  "Incorrect error thrown. \(thrownError) is not equal to \(expectedError)",
                  file: file,
                  line: line)
    } catch {
        XCTFail("Invalid error thrown: \(error)", file: file, line: line)
    }
}

위 템플릿을 사용하면 테스트 및 프로덕션 코드의 조건 확인을 위해 특수화된 assert 함수를 신속하게 정의할 수 있습니다. 저는 저의 모든 커스텀 테스트 assert 함수를 모아서 GitHub에 올려두었습니다.

결과

Assertions은 개념적으로 꽤 쉽지만, 올바른 위치에 배치되면 매우 강력해질 수 있습니다. 그것들이 어떻게 작동하는지 이해하면서, 우리는 또한 깔끔하고 더 표현력이 있는 코드를 만드는 데에 도움을 주는 특화된 우리만의 assert 함수를 쉽게 만들 수 있게 되었습니다.

저는 개인적으로 테스트와 인라인 assertions을 잘 섞으면 각 API의 예상되는 동작과 주의 사항이 명확해지고 개발 과정동안 잘못된 사용이 신속하게 발견되어서 시간이 지나도 쉽게 코드를 유지할 수 있다는 것을 알게되었습니다.

어떻게 생각하시나요? 여러분은 각자의 프로젝트에서 assertions을 어떻게 사용하시나요? 자신에게 유용한 것을 만들기 위한 기능을 찾으셨나요? 댓글, 질문 또는 피드백으로 저에게 알려주세요. 아래의 댓글란이나 트위터(@johnsundell)로 하실 수 있습니다.

읽어주셔서 감사합니다! 🚀