Swift 에러 핸들링
프로그램이 실행되다 보면, 무조건 에러가 발생하게 된다. 예를 들어 카카오톡 같은 메신저 앱의 경우, 인터넷 연결이 계속 유지되어야 정상적으로 상대방과 채팅을 할 수 있다. 하지만 사용자가 와이파이를 끈다던가, 비행기 모드를 키는 등의 행동으로 인터넷 연결이 끊어질 경우, 네트워크 통신 시 에러가 발생하게 되며 프로그램은 그런 에러를 정상적으로 처리할 수 있어야 한다.
만약 발생 가능한 모든 에러를 처리하지 못한다면, 런타임 에러로 인해 작동 중인 프로그램이 강제로 종료될 것이다.
Swift 의 에러 핸들링
Swift 는 Java 나 Kotlin 같은 언어와 거의 유사하게 에러를 처리한다. 기능 동작 중 에러가 발생할 경우 에러를 던질 수 있고 (throws), 던진 에러를 감지해서(catch) 처리할 수 있다.
Swift에선 Error 프로토콜을 따르는 모든 타입을 던질 수 있는데, IOS SDK 의 많은 API 들도 앱의 코드 내에서 처리되어야 할 에러를 던지므로 적절히 처리하는 것이 매우 중요하다.
Swift에선 Erorr 프로토콜을 따르는 모든 타입을 던질 수 있다.
에러 타입 선언하기
네트워크를 이용해 파일을 전송하는 메소드가 있다고 가정해보자. 이 메소드는 여러 원인으로 인하여 파일 전송에 실패할 가능성이 있다.
- 네트워크 연결이 끊김
- 네트워크 속도가 너무 느림
- 파일이 없음
이러한 모든 에러는 Error 프로토콜을 따르는 열거형으로 표현될 수 있다.
enum FileTransferError : Error {
case noConnection //네트워크 연결 X
case lowBandWidth //느린 속도
case fileNotFount //파일 발견 X
}
Error 타입을 지정했으므로, 에러가 발생했을 때 이러한 에러를 던지도록 처리할 수 있다.
에러를 던지는 함수 만들기
함수를 실행하기 전에, 함수가 정상적으로 동작하는데 필요한 모든 조건을 만족하는지 확인해야 한다. 파일 전송 메소드의 경우, 네트워크 연결이 되있는지 확인하고 (WIFI 확인), 네트워크 속도가 충분한지 확인해야 하며, 마지막으로 보내려는 파일이 존재하는지 확인해야 한다.
파일을 전송하는 함수인 transferFile 함수를 선언해보자
let connectionOK = true //네트워크 연결 상태
let connectionSpeed = 30.00 //네트워크 속도
let fileFound = false //파일 발견 여부
func transferFile() throws -> Bool {
guard connectionOK else {
throw FileTransferError.noConnection
}
guard connectionSpeed > 30 else {
throw FileTransferError.lowBandWidth
}
guard fileFound else {
throw FileTransferError.fileNotFount
}
return true
}
반환 타입 이전에 throws 키워드를 작성함으로써 컴파일러에게 에러를 던지는 함수라고 선언할 수 있다.
guard 문을 통해 각 조건을 만족하는지 확인하고, 만족하지 않으면 throw 문을 통해 우리가 선언한 에러 타입을 던지는 것을 확인할 수 있다.
에러를 던지는 메소드 호출하기
throws 키워드를 통해 에러를 던지는 함수를 선언했으면 일반적인 방법으로는 이 함수를 호출할 수 없다. 이러한 메소드를 호출할 때는 항상 try 키워드를 앞에 붙여야한다.
try transferFile()
단순 try 구문을 사용해 함수를 호출하는 것으로는 던져진 에러를 잡을 수 없다. 따라서 위 함수를 그냥 try 문만을 사용해 호출하면 에러가 발생하고 프로그램이 충돌한다.
try 구문을 사용하는 것 이외에도 던져진 모든 에러를 잡아서 처리하는 do - catch 문 내에서 호출하는 방법도 있다. 에러를 던지는 transferFile 함수를 호출하고, 에러를 처리하는 sendFile() 함수를 만들어보자
func sendFile() -> String {
do {
try transferFile()
} catch FileTransferError.noConnection {
return "No Network Connection"
} catch FileTransferError.lowBandWidth {
return "Low BandWidth"
} catch FileTransferError.fileNotFount {
return "File Not Found"
} catch {
return "Unknown Error"
}
return "Successful Transfer"
}
do 블럭 내부에서 에러를 던지는 함수를 실행할 수 있다. 이 함수가 실행되다가 에러를 던지면, catch 문에서 에러를 잡을 수 있는데, 특정 error 타입을 catch 함으로써 각 에러를 적절히 처리해줄 수 있다.
그러나 Java 와 Kotlin 과는 달리 Swift 는 모든 에러를 처리할 수 있어야 하는데, 따라서 마지막 catch - all 구문을 무조건 선언해줘야 컴파일 에러가 발생하지 않는다
이제 이 함수를 실행해보자.
print(sendFile())
//출력 결과
Low BandWidth
에러 객체에 접근하기
메소드 실행 중 에러가 발생하면 실패한 원인을 구별할 수 있는 NSError 객체가 반환된다. catch 문에서 이 에러 객체에 접근해, 에러 원인에 대한 자세한 설명을 얻을 수 있다.
func sendFile() -> String {
do {
try transferFile()
}
//let error 를 통해 에러 객체에 접근할 수 있다
catch let error {
return error.localizedDescription
}
return "Successful Transfer"
}
에러 캐치 비활성화하기
try! 구문을 사용하면 에러를 catch 하지 않는다고 선언할 수 있다. 에러가 발생하지 않을 것으로 확신할 수 있는 상황의 경우 쓰일 수 있는데, 이 때 에러가 발생하면 바로 런타임 에러로 프로그램이 종료하므로 가급적 사용하지 말도록 하자.
try! transferFile()
defer 구문으로 함수 반환 전 필요한 작업을 수행하기
Kotlin 이나 Java 에선 try catch 문 다음에 finally 구문을 사용해 정상적으로 실행되던 에러가 발생하던가에 상관없이 마무리 작업을 수행할 수 있었다. 예를 들어 파일 입출력을 실행하고 난 뒤 입출력 스트림을 닫는 것등의 작업 말이다. Swift 에서도 동일하게 함수 실행 - 에러 캐치 작업 후 마지막으로 수행되어야 할 코드를 defer 구문 내에 포함할 수 있다.
네트워크를 통해 파일을 전송하고 난 뒤, 네트워크 연결을 닫도록 함수를 수정해보자
func sendFile() -> String {
//defer 구문 내의 코드는 함수가 에러를 던지거나, 반환되기 전 마지막으로 실행된다!
defer {
closeNetworkConnection()
}
do {
try transferFile()
} catch let error {
return error.localizedDescription
}
return "Successful Transfer"
}
defer 구문은 함수가 종료되거나 에러를 던지기전 무조건 실행되므로, 아주 유용하게 활용할 수 있다.