에러 핸들링 이해하기

jonghwan·2022년 10월 4일
0

멋쟁이사자처럼

목록 보기
19/28
post-thumbnail

에러 핸들링 이해하기

Swift 코드를 아무리 신중하게 설계하고 구현했다 해도 앱을 통제할 수 없는 상황은 언제든지 발생할 것이다.

활성화된 인터넷 연결을 기반으로 동작하는 앱은 아이폰이 네트워크 신호를 잃는 것을 제어할 수 없다.

사용자가 비행기 모드(airplane mode)를 비활성화하는 것도 막을 수 없다.

앱이 해낼 수 있는 것은 그러한 에러를 확실하게 처리하도록 구현하는 것이다.

앱을 계속 사용하려면 활성화된 인터넷 연결이 필요하다는 것을 사용자가 알 수 있도록 메시지를 표시할 수 있다.

Swift에서 에러를 처리하는 데는 두 가지 단계가 있다.

  • iOS 앱의 메서드 내에서 원하는 결과가 나오지 않을 경우에 에러를 발생(즉, 스로잉(throwing)하기

  • 메서드가 던진(throwing) 에러를 잡아서 처리하기

에러를 던질 경우, 해당 에러는 에러의 특성을 식별하여 취할 수 있는 가장 적절한 동작을 결정하는 데 사용되는 특정 에러 타입 중 하나가 될 것이다.

에러 타입 값은 Error 프로토콜을 따르는 모든 값이 될 수 있다.

앱 내의 메서드에서 에러를 던지도록(throw) 구현하는 것도 중요하지만, iOS SDK의 많은 API 메서드(특히, 파일 처리와 관룐된 메서드)도 앱의 코드 내에서 처리되어야 할 에러를 던진다는 것도 알아두어야 한다.

에러 타입 선언하기

예를 들어, 원격 서버에 파일을 전송하는 메서드가 있다고 하자.

이 메서드는 여러 원인으로 인하여 파일 전송에 실패할 가능성이 있다.

  • 네트워크 연결이 없거나 너무 느림

  • 전송할 파일을 찾지 못함

이러한 모든 에러는 다음과 같이 Error 프로토콜을 따르는 열거형 내에서 표현되도록 할 수 있다.

아래와 같이 에러 타입(error type)을 선언하면 에러가 발생했을 때 사용할 수 있다.

enum FileTransferError: Error {
  	case noConnection
  	case lowBandwidth
  	case fileNotFound
}

에러 던지기

메서드나 함수가 에러를 던질 수 있다는 것을 선언할 때는 다음과 같이 throws 키워드를 이용한다.

func transferFile() throws {
  
}

결과를 반환하는 메서드나 함수의 경우, throws 키워드는 다음과 같이 반환 타입 앞에 위치하게 된다.

func transferFile() throws -> Bool {

}

에러를 던질 수 있도록 메서드를 선언했으니 오류가 발생할 때 에러를 던지는 코드를 추가할 수 있다.

이것은 throw 구문과 guard 구문을 결합하여 사용하게 된다.

다음의 코드는 상태 값으로 제공되는 상수들을 선언한 다음, 메서드에 대한 guard 동작과 throw 동작을 구현하였다.

메서드 내에 있는 각각의 guard 구문은 각 조건이 참인지를 검사한다.

만약 거짓이라면 else 구문에 포함된 코드가 실행된다.

이 코드에서는 throw 구문을 사용하여 FileTransferError 열거형에 있는 에러 값들 중 하나를 던지고 있다.

let connectionOK = true
let connectionSpeed = 30.00
let fileFound = false

enum FileTransferError: Error {
  	case noConnection
  	case lowBandwidth
  	case fileNotFound
}

func transferFile() throws {

  guard connectionOK else {
  	throw FileTransferError.noConnection
  }

  guard connectionSpeed > 30 else {
    throw FileTransferError.lowBandwidth
  }

  guard fileFound else {
    throw FileTransferError.fileNotFound
  }
}

에러를 던지는 메서드와 함수 호출하기

메서드 또는 함수가 에러를 던지도록(오류가 발생했다는 것을 알려주도록) 선언했다면 일반적인 방법으로는 호출 할 수 없다.

이러한 메서드를 호출할 때는 다음과 같이 앞에 try 구문을 붙여야한다.

try transferFile()

try 구문을 이용하는 방법 외에도 던져진 모든 에러를 잡아서 처리하는 do-catch 구문 내에서 호출하는 방법도 있다.

예를 들어, fileTransfer 메서드를 sendFile 이라는 이름의 메서드 내에서 호출해야 한다고 가정해보자.

이 경우에는 다음과 같이 구현할 수 있다.

func sendFile() -> String {
  	do {
    	try fileTransfer()
    } catch FileTransferError.noConnection {
        return "No Network Connection"
    } catch FileTransferError.lowBandwidth {
        return "File Transfer Speed too low"
    } catch FileTransferError.fileNotFound {
        return "File not Found"
    } catch {
        return "Unknown error"
    }   
    return "Successful transfer"
}

이 메서드는 세 가지 에러 조건에 대한 catch를 포함하고 있는 do-catch 구문 내에서 fileTransfer 메서드를 호출한다.

이 메서드는 여러 에러에 대하여 해당 에러에 대한 설명을 담고 있는 문자열 값을 반환한다.

네 번째 catch 절은 에러에 대한 패턴 매칭이 이뤄지지 않은 상태에 대한 것이다.

이것은 'catch all' 구문으로, 앞선 catch 구문과 일치하지 않은 모든 에러를 처리할 수 있도록 해준다.

이것은 반드시 필요한데, do-catch 구문은 가능한 모든 에러에 대해 처리할 수 있도록 구성되어야 하기 때문이다.

여러 객체에 접근하기

메서드 호출이 실패하면 반드시 실패한 원인을 구별할 수 있는 NSError 객체가 반환될 것이다.

catch 구문에서 가장 필요한 것은 이 객체에 대해 접근하여 앱 코드 내에서 취할 수 있는 가장 적절한 동작을 실시하는 것이다.

다음 코드는 새로운 파일 시스템 디렉터리를 생성하고자 할 때 catch 구문 내에서 여러 객체에 접근하는 방법을 보여준다.

do {
  	try filemgr.createDirectory(atPath: newDir, withIntermediateDirectories: true, attributes: nil)
} catch let error {
	print("Error: \(error.localizedDescription)")
}

에러 캐칭 비활성화하기

다음과 같이 try! 구문을 사용하면 do-catch 구문 내에서 메서드가 호출되도록 감싸지 않아도 스로잉 메서드가 강제로 실행된다.

이러한 방법을 사용하는 것은 컴파일러에게 이 메서드 호출은 어떠한 에러도 발생하지 않을 것이라고 알려주는 것과 동일하다.

이러한 방법을 사용했는데도 에러가 발생한다면 런타임 에러가 될 것이다.

그러므로 이러한 방법은 가급적 사용하지 않도록 하자.

try! transferFile()

defer 구문 사용하기

앞에서 구현한 sendFile 메서드는 에러를 처리하는 일반적인 시나리오를 보여준다.

do-catch 구문에 있는 각각의 catch 절은 호출하는 메서드에게 제어권을 반환하기 위하여 return 구문을 포함하였다.

하지만, 에러의 종류와는 상관없이 제어권을 반환하기 전에 어떠한 별도의 작업을 수행하는 게 더 효과적인 경우가 있을 수 있다.

예를 들어 sendFile 메서드에서는 제어권을 반환하기 전에 임시 파일들을 지워야 할 경우가 발생할 수 있다.

이것은 defer 구문을 이용하면 가능하다.

defer 구문은 메서드가 결과를 반환하기 직전에 실행되어야 하는 일련의 코드를 지정할 수 있게 해준다.

다음의 코드는 defer 구문이 포함되도록 sendFile 메서드를 수정한 것이다.

이제 defer 구문을 추가했으니 이 메서드가 어떠한 반환을 하든지 제어권을 반환하기 전에 removeTmpFiles 메서드와 closeConnection 메서드가 항상 호출될 것이다.

func sendFile() -> String {
    defer {
      removeTmpFiles()
      closeConnection()
    }
    
    do {
        try fileTransfer()
    } catch FileTransferError.noConnection {
        return "No Network Connection"
    } catch FileTransferError.lowBandwidth {
        return "File Transfer Speed too low"
    } catch FileTransferError.fileNotFound {
        return "File not Found"
    } catch {
        return "Unknown error"
    }   
    return "Successful transfer"
}

요약

에러 핸들링은 강력하고 안정적인 iOS 앱을 만드는 가장 기본적인 파트다.

Swift 2의 등장 덕분에 이제는 에러를 처리하는 작업이 훨씬 쉬워졌다.

에러 타입들은 Error 프로토콜을 따르는 값들을 이용하여 생성되며, 열거형처럼 구현되는 것이 가장 일반적이다.

에러를 던지는 메서드와 함수는 throw 키워드를 이용하면 선언된다.

guardthrow 구문은 에러 타입을 기반으로 한 에러들을 던지기 위하여 메서드나 함수 코드 내에서 사용된다.

에러는 던질 수 있으나 메서드는 try 구문을 이용하여 호출되며, 반드시 do-catch 구문으로 감싸여야 한다.

do-catch 구문은 철저하게 나열된 catch 패턴으로 구성되며, 각각의 catch 구문은 특정 에러에 실행될 코드를 담는다.

메서드가 반환될 때 실행될 정리(cleanup) 작업은 defer 구문을 이용하여 정의할 수 있다.

예제

// 임의로 상황들을 만들어보는 예제
let connectionOK: Bool = true
let connectionSpeed: Double = 40.00
let fileFound: Bool = true
let fileSize: Int = 10

// Error 프로토콜을 따르는 나만의 에러 타입(열거형)
enum FileTransferError: Error {
    // 에러가 갖게 될 상황들을 값으로 정리함
    case noConnection
    case lowBandwidth
    case fileNotFound
    case tooBigSize
}

// 이 함수는 실행중에 오류가 발생할 수 있음
func transferFile() throws {
    // 함수 안에서는 최대한 모든 오류 상황들을 throw로 알려줄 수 있도록 만들어야 한다.
    
    // connectionOK가 false이면 else 구문 실행되고 함수 종료
    guard connectionOK else {
        throw FileTransferError.noConnection
    }
    
    guard connectionSpeed > 30 else {
        throw FileTransferError.lowBandwidth
    }
    
    guard fileFound else {
        throw FileTransferError.fileNotFound
    }
    
    guard fileSize < 20 else {
        throw FileTransferError.tooBigSize
    }
}

func removeTmpFiles() {
    print("Remove Temporary files")
}

func closeConnection() {
    print("Close connection")
}

// transferFile 함수를 호출하는 응용 코드
// 파일 전송을 "시도"하고 결과를 문자열로 반환해서 안내함
func sendFile() -> String {
    
    // defer는 에러가 발생하든 안하든 return으로 제어권이 넘어가는 함수 종료 직전에 늘 실행
    defer {
        removeTmpFiles()
        closeConnection()
    }
    
    // 오류가 발생할 수 있는 함수를 사용하는 쪽에서는 모든 오류 내용에 대응을 못하더라도 최대한 많이 대응 가능하도록 해줄 필요가 있다.
    
    print("senfFile function start...")
    
    do {
        try transferFile()
    } catch FileTransferError.noConnection {
        return "No Network Connection"
    } catch FileTransferError.lowBandwidth {
        return "File Transfer Speed too low"
    } catch FileTransferError.fileNotFound {
        return "File not Found"
    } catch let error {
        // 아직 챙기지 못한 오류상황 확인용 함수 내부 출력
        print("error: \(error)")
        return "Unknown error"
    }
    
    return "Successful transfer"
}

print("\(sendFile())")

0개의 댓글