2015년 6월 10일 수요일

Swift 2.0 - 예외처리(Error Handling Model)

Swift 2.0 에서 드디어 예외처리 - 공식 블로그에는 Error Handling Model 이라고 표현하고 있다 - 이라는 OOP 언어라면 누구나(?) 다들 가지고 있는 기능(?)이 추가되었다. 그런데 기존 다른 언어들과는 조금 다른 모습이긴 하다

일단 Swift 2.0 에서 이 예외처리는 대충 do-try-catch 구분이라고 표현하고자 한다. 이것만 봐도 모양이 느껴지는데, 다른 언어들은 대게 try-catch 혹은 try-except 방식으로 사용하기 때문이다.

예제

아래와 같은 기존 방식(?)대로 만든 코드가 있다고 치자.
let url = NSURL(fileURLWithPath: "/foo/bar")
var error: NSError?

NSFileManager.defaultManager().contentsOfDirectoryAtURL(
    url,
    includingPropertiesForKeys: nil,
    options: .SkipsHiddenFiles,
    error: &error)

if let err = error {
    // Process Error
    println("Error: " + err.description)
}
위 코드가 무엇을 하는지는 크게 신경쓰지 않아도 된다. 다만 호출 시 마지막에 넘겨주는 인자인 error가 있다는 걸 알아야 한다. 이 error는 만약 처리에 실패할 경우 NSError 타입의 오브젝트가 넘어오고 성공한다면 nil이 된다. 따라서 if 문을 통해 이 에러를 파악해서 처리하면 된다.

그런데 이 코드를 Xcode 7 에서 빌드하려고 하면 에러가 발생한다.

do-try-catch

자 이제 Swift 2.0 방식으로 위 코드를 뜯어고쳐보자. Xcode 7 Beta1 에서 위에서 사용한 contentsOfDirectoryAtURL 메소드는 아래와 같은 식으로 바뀌었다.
func contentsOfDirectoryAtURL(_ url: NSURL,
    includingPropertiesForKeys keys: [String]?,
                       options mask: NSDirectoryEnumerationOptions) throws -> [NSURL]
마지막에 throws 가 들어있다는 점만 빼면 딱히 이상해 보이는 점은 없다. 어쨌거나 고쳐보자.
let url = NSURL(fileURLWithPath: "/foo/bar")
do {
    try NSFileManager.defaultManager().contentsOfDirectoryAtURL(
        url,
        includingPropertiesForKeys: nil,
        options: .SkipsHiddenFiles)
}
catch let error as NSError {
    // Process Error
    print("Error: " + error.description)
}
'do' 라는 녀석이 등장했는데 이건 do-while문과는 관계가 없나보다. 어쨌든 예외를 던질 수 있는(throws) 녀석은 do 내부에서 try 를 앞에 붙이고 호출해야 한다.
참고로 try 를 빼먹으면 오류가 발생한다. Swift 2.0 에서 throw를 하는 메소드나 함수는 모두 try 가 강제된다. 
do 블럭 내부에서 try를 이용해 한 줄의 코드를 테스트 하는 방식은 좀 신선하다. 하지만 그렇다고 다른 언어들과 확연히 다른 것도 아니니 그다지 어색하지는 않은 것 같다. 

어쨌거나 이제 Swift 2.0 에선 이런 식으로 에러 핸들링이 가능해 졌다는 말이다.

PS. 따로 이야기 하지는 않았는데, 로그 찍을때 사용하던 함수 'println' 이 'print' 로 이름이 바뀌었다. 컴파일시 에러로 알려주니 쉽게 고칠 수 있으니 그러려니 생각하자. -_-;

순수 Swift 예외처리

지금까지는 기존 파운데이션에 정의된 NSError를 리턴하는 메소드류의 변화에 가까운 내용이다. 이제는 예외처리를 순수 Swift 방식으로 구현하는 방법에 대해 살펴보자.

우선 에러를 정의해야 한다. Swift 2.0 에선 에러는 enum 타입으로 정의하는게 가장 쉬운 것 같다. 물론 이 외에 struct 등등 다양한 방법으로 만들 수 있는데, 단지 ErrorType 프로토콜을 따르기만 하면 된다.
enum MyError: ErrorType {
    case NoZero
    case UnknownError
}
이제 이 MyError 를 이용해 예외를 던지는(?) 함수를 하나 만들어 보자. 일반적으로 예외를 던질 때는 throw 라는 용어가 많이 쓰인다는 점을 알고 있다면 아래 예제 코드는 쉽게 읽혀질 것이다.
func ILikeZero(value: Int) throws -> Int {
    if value != 0 {
        throw MyError.NoZero
    }
    return value
}
value 값을 0이 아닌 값으로 주면 예외가 발생하는 코드이다. 굳이 리턴 타입까지 명시한 이유는 try 문을 사용 할 때의 예를 보여주기 위함이니 굳이 생각하진 않아도 된다.

이제 이 함수를 호출해서 예외처리하는 코드를 보자.
do {
    let value = try ILikeZero(1)
}
catch MyError.NoZero {
    print("I want ZERO!")
}
try 를 이용해 함수를 호출했다. 코드 상 예외가 발생하게 되고 콘솔에 "I want ZERO!" 라는 내용이 찍히게 된다.

try? try!

Swift 2.0 (정확히 Xcode 7 Beta 6) 부터 도입된 새로운 키워드로 try? 가 있다. 이것도 예외사항을 발생시키는 메소드나 함수에 붙여서 사용하는 건데 그 이름에서 유추 가능하듯이 예외가 발생하면 nil을 주게 되어있다. 물음표(?)는 옵셔널임을 생각해보자.
enum MyError: ErrorType {
    case ValueIsFalse
}

func ExceptWhenFalse(value: Bool) throws -> Bool {
    if value {
        return true
    }
    else {
        throw MyError.ValueIsFalse
    }
}

let result = try? ExceptWhenFalse(false)
// result 의 값은 nil

if let value = try? ExceptWhenFalse(false) {
    print("False")
}
이런 식으로 단순하게 사용이 가능해진다.

간단해 보이긴 하지만, 하단의 if 문 처럼 쓰게 되는 경우가 있을텐데 예외가 명확하지 않다면 에러에 제대로 대응 할 수 없다는 단점이 있다. 그냥 필요한 경우에만 쓰는 것으로 생각하면 될 것 같다.

이 외에 try! 라는 식으로 강제로 옵셔널을 벗겨낼 때 쓰던 느낌표(!)를 try 뒤에 붙이는 문법도 추가되었다. 이 경우 예외사항이 발생하면 앱이 죽어버리므로 디버깅에 유리하게 쓸 수는 있겠지만 프로덕트 코드에서의 사용은 지양하는 편이 좋을 것 같다.

마무리

이제서야 이런게 들어왔다는게 좀 의아하기도 하다. 예외처리는 거의 기본에 해당하는 것이 아닌가 하는 생각이 들어서 말이다. 물론 기존에 코딩 잘 하고 있었으니 필수적인 건 아니긴 하지만 -_-;;;

앞서 본 NSError 를 던지는 방식을 구현해서 테스트 해 보고 싶은데 아직은 방법을 잘 모르겠다. NSError를 던지는 것도 안되고 NSException을 raise() 시키는 것도 안된다. 도데체 뭘로 해야할까 -_-;;

catch 하고픈 마음이 없다면 do 블럭을 없애고 그냥 try 문으로 메소드를 호출하면 된다. 물론 이렇게 쓸 이유는 별로 없어 보이지만 귀차니즘은 만병의 근원이기도 하니... 음... 응?

참고로 위 글은 Xcode 7 Beta6 기준으로 쓰여진 글이다. 버전업이 됨에 따라 바뀔 가능성이 충분히 있음을 알아두자.

[관련글] 스위프트(Swift) 가이드

댓글 1개 :

jusung :

좋은 글 공유해주셔서 감사합니다!