올바르게 오류 처리하기

오류 처리 방법

Swift에서 오류를 처리할 수 있는 방법은 다음과 같다.

nil 또는 error enum case 사용하기:

  • 오류 처리의 가장 간단한 형태는 오류가 발생한 함수로 부터 nil 또는 .error case (Result enum 타입을 반환 타입으로 사용하는 경우)를 반환한다.
  • 많은 상황에서 정말 유용할 수 있지만, 지나치게 사용하면 사용하기 번거로운 API로 이어질 수 있고 또한 문제를 발생시키는 로직을 숨길 위험이 있다.

throw Error 사용하기:

  • caller가 do ~ try ~ catch 패턴을 사용하여 잠재적인 오류를 처리해야 한다. 또는 try? 를 사용하여 오류를 무시할 수 있다.

assert()과 assertionFailure() 사용하기:

  • 특정 조건이 참인지 확인하라. 디버그 빌드에서 치명적인 오류가 발생하는 반면 릴리즈 빌드에서는 무시된다. 따라서 asset가 발동되면 실행이 중지된다는 것은 보장되지 않기 때문에 일종의 심각한 런타임 경고와 같은 것이다.

assert 대신 precondition()과 preconditionFailure() 사용하기:

  • assert와 중요한 차이점은 릴리즈 빌드에서도 항상 실행된다는 것이다. 즉, 조건이 충족되지 않으면 실행이 절대 지속되지 않는다는 보장을 받는다.

fatelError() 호출하기:

  • UIViewController와 같은 NSCoding을 준수하는 시스템 클래스의 생성자 init(coder:) 메서드에서 봤을 것이다. 직접 호출하면 프로세스를 종료한다.

exit() 호출하기:

  • 글로벌 범위를 벗어나고 싶을 때, 커맨드라인 도구 및 스크립트에서 매우 유용하다. (예를 들어, main.swift)

Recoverable vs non-recoverable

올바른 실패 방법을 선택할 때 고려해야 할 핵심은 발생한 오류가 복구 가능한지 여부를 결정하는 것이다.

Recoverable

  • nil을 리턴하거나 error enum case를 사용한다.
  • throw error를 사용한다.

Non-recoverable

  • assert() 사용하기
  • precondition() 사용하기
  • fatalError() 호출하기
  • exit() 호출하기

 

비동기 작업을 처리하는 경우, nil 또는 error enum case를 사용하는 것이 아마도 최선의 선택일 것이다.

class DataLoader {
    enum Result {
        case success(Data)
        case failure(Error?)
    }

    func loadData(from url: URL,
                  completionHandler: @escaping (Result) -> Void) {
        let task = urlSession.dataTask(with: url) { data, _, error in
            guard let data = data else {
                completionHandler(.failure(error))
                return
            }

            completionHandler(.success(data))
        }

        task.resume()
    }
}

동기식 API의 경우, API 사용자가 적절한 방식으로 오류를 처리하도록 "강제"하므로, 에러를 throwing하는 것은 훌륭한 옵션이다.

class StringFormatter {
    enum Error: Swift.Error {
        case emptyString
    }

    func format(_ string: String) throws -> String {
        guard !string.isEmpty else {
            throw Error.emptyString
        }

        return string.replacingOccurences(of: "\n", with: " ")
    }
}

그러나 오류를 복구할 수 없는 경우도 있다. 예를 들어, 앱 실행시 구성 파일을 로드해야 한다고 가정하자. 만약, 그 구성 파일이 없어지면, 앱을 undefined 상태로 만들 것이다. 그래서 이 경우, 계속되는 프로그램 실행보다 크래시나는 것이 더 낫다. 이를 위해, 더 강하고 복구 불가능한 실패 방법 중 하나를 사용하는 것이 더 적절하다.

 

이 경우, 구성 파일이 누락되면 preconditionFailure()을 사용하여 실행을 중지한다.

guard let config = FileLoader().loadFile(named: "Config.json") else {
    preconditionFailure("Failed to load config file")
}

 

Programmer errors vs execution errors

또 다른 중요한 구별은 오류가 논리적으로 잘못되거나 잘못된 구성에 의해 발생했는지 또는 오류가 애플리케이션 흐름의 정당한 부분으로 간주되어야 하는지 여부다. 기본적으로 프로그래머가 오류를 일으켰는지 아니면 외부 요인이 발생했는지이다.

 

프로그래머 오류로부터 보호할 때, 거의 항상 복구 불가능한 기술을 사용하기를 원한다. 그렇게 하면, 앱 전체에 걸쳐서 특별한 상황을 코딩할 필요가 없고, 좋은 테스트들이 그러한 유형의 오류가 가능한 한 빨리 잡힐 수 있을 것이다.

 

예를 들어, 뷰 모델이 사용되기 전에 뷰 모델에 바인딩되어야 하는 뷰를 구축하고 있다고 가정하자. 뷰 모델은 우리 코드의 선택사항이 될 것이지만, 우리는 그것을 사용할때마다 언래핑할 필요가 없다. 그러나 뷰 모델이 누락된 경우 반드시 프로덕션에서 애플리케이션을 중단시키고 싶지는 않다. 디버그에서 그에 대한 오류를 얻는 것만으로도 충분하다. 이는 다음과 같은 assert()를 사용하는 경우에 해당한다.

class DetailView: UIView {
    struct ViewModel {
        var title: String
        var subtitle: String
        var action: String
    }

    var viewModel: ViewModel?

    override func didMoveToSuperview() {
        super.didMoveToSuperview()

        guard let viewModel = viewModel else {
            assertionFailure("No view model assigned to DetailView")
            return
        }

        titleLabel.text = viewModel.title
        subtitleLabel.text = viewModel.subtitle
        actionButton.setTitle(viewModel.action, for: .normal)
    }
}

assertionFailure()는 릴리즈 빌드에서 자동으로 실패하므로 위의 guard 문을 다시 리턴해야한다는 점을 유의하라.

 

스위프트에서 이용할 수 있는 다양한 오류 처리 기법에 익숙해지자!

 

원문: www.swiftbysundell.com/articles/picking-the-right-way-of-failing-in-swift/#programmer-errors-vs-execution-errors

댓글