안녕하세요~! 오늘은 스위프트의 오류처리에 대해서 공부를 해보겠습니다. 프로그램이 실행되다가 많은 오류가 생기는 데 그때 당황하지 않고 처리를 해주는 것이라고 생각하면 쉽게 이해가 될 것 같군요! 그럼 더 자세하게 알아보겠습니다.
오류 처리는 예외 처리 (exception handling)라고도 합니다. 오류나 예외를 잘 처리해주어서 프로그래밍 멈추지 않게 하는 것입니다. 한마디로 작업에 실패할 때 코드가 적절히 응답할 수 있게 해서 오류를 해결하고 원인을 이해하는 데 도움이 된다고 합니다. 런타임 시 오류를 발견하고 복구하는 과정이라고 정리할 수 있겠네요. Swift 개발자 문서에 가보면 자세히 나와있습니다.
Swift에서 오류를 처리하는 방법은 크게 4가지로 나눌 수 있습니다.
- Throwing Functions을 이용한 오류 전파
- Do-Catch를 이용한 오류 처리
- Error를 Optional Values로 변환
- 오류 전파 비활성화
이 중에서도 위에 두 방법을 많이 사용한다고 하네요. 그럼 밑에서 오류의 방법들을 자세히 살펴보겠습니다.
함수, 메소드, 이니셜라이저가 에러를 발생시킬 수 있음을 알리기 위해, 매개 변수 선언 뒤에 throws 키워드를 작성합니다. throws가 표시되어 있는 함수를 throwing function이라고 부르고 만약 함수가 반환 타입을 지정했다면, 매개 변수 선언과 반환 화살표(->) 사이에 키워드를 적습니다. 아래의 코드와 같이 throws를 씁니다.
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
오류가 발생할 수 있다는 것을 표현하기 위해 throws를 반환 화살표(->)앞에 표시를 합니다. 이것은 반환 전에 오류가 발생하면, 오류객체를 반환한다는 의미입니다. 그리고는 함수안에 throw와 오류명을 넣어서 오류를 반환할 수 있습니다. 코드를 짜면서 이해를 해보겠습니다.
우선 오류를 정의해주는 enum을 하나 만들겠습니다. Error 프로토콜을 이용하여 쉽게 에러를 표시할 수 있습니다.
enum RiseError : Error {
case overSizeString
case incorrectData(part: String)
}
//throwing function
func getNextYear(year: Int) throws -> Int {
guard year <= 2020 else {
throw RiseError.overSizeYear
}
guard year >= 0 else {
throw RiseError.incorrectData(part: year)
}
return year+1
}
다음과 같이 간단한 함수를 하나 만들었습니다. 현재의 년도를 입력받으면 내년의 년도를 반환해주는 함수입니다. 하지만 이 함수는 throws를 사용하여 throwing function이기 떄문에 이미 알고 있는 함수와는 조금 모양이 다릅니다.
guard문을 써서 조건을 만들고 만약 입력 년도가 2020보다 크면 RiseError.overSizeYear를 throw합니다. 여기서 throw는 오류를 반환해주는 키워드로 return과 비슷하다고 보면됩니다. 마찬가지로 입력 년도가 0보다 작게 되면 RiseError.incorrectData(part: year)를 throw하게 되는 것이지요.
함수를 호출해서 실행을 해보겠습니다. 하지만 여기서 중요한 점은 throwing function은 일반적인 함수와 다르기 때문에 일반적으로 실행을 할 수 없습니다. throwing function을 호출하기 위해서는 항상 try가 들어가야 하죠. try를 사용하여 함수를 호출해보겠습니다.
오류 처리를 확실하게 해주었기 때문에 에러가 뜨지 않고 열거형의 case가 실행되는 것을 볼 수 있습니다.
do-catch문을 사용하여 오류를 처리할 수도 있습니다. 또한 do-catch문을 사용하여 throw functions도 호출을 할 수 있습니다. 그럼 간단한 코드를 작성하여 실행해보겠습니다.
func getNextYearDoCatch(paramYear: Int) {
var err: Int = 0
do {
err = try getNextYear(year: paramYear)
} catch RiseError.overSizeYear {
print("년도를 초과해서 입력하였습니다")
} catch RiseError.incorrectData(let part){
print("입력한 값이 \(part)이므로 오류입니다.")
} catch {
print("default error catch")
}
print(err)
}
다음과 같이 getNextYearDoCatch함수를 만들었습니다. do부분에는 오류가 발생할 수 있는 코드를 써줍니다. getNextYear함수를 호출하기 위해 try를 써서 변수에 넣어주었습니다. 이때 만약 오류가 발생하게 되면 오류의 종류에 따라 catch문이 실행되게 됩니다. 뭔가 패턴이 switch문하고 비슷한 걸 확인할 수 있었네요. 그렇다면 이 코드가 잘 실행이 되는지 확인을 해보겠습니다.
getNextYearDoCatch함수에 다양한 매개변수들을 넣고 실행을 해보았습니다. 에러의 종류에 따라 catch문이 실행되면서 다른 출력을 하는 것을 확인할 수 있었습니다.
try? 키워드를 사용해 오류를 옵셔널 값으로 변환하여 처리할 수 있습니다. 만약 오류가 try? 표현이 평가되는 도중 발생한다면, 그 표현의 결과는 nil이 되는 것이지요. 예를 들어 다음 코드에서 x와 y는 같은 값과 행동을 갖고 있다. 위의 코드를 이용해서 간단한 코드로 확인을 해보겠습니다.
let x = try? getNextYear(year: 3000)
print(x) //nil
let x = try? getNextYear(year: 2000)
print(x!) //2001
이렇게 try? 키워드를 사용해서 상수 x에 값을 넣어줬습니다. 첫번째 코드는 에러가 나타나는 코드이므로 x에는 nil이 들어갔고, 두번째 코드는 에러가 나타나지 않았으므로 강제 언래핑을 통해 값을 추출할 수 있습니다.
만약 함수나 메소드가 에러를 발생시키지 않을 것이라 확신하는 경우, try! 키워드를 사용합니다. 뭔가 강제 언래핑과 개념이 비슷하다고 보시면 됩니다! 에러 전파를 비활성화하고, 에러가 발생하지 않도록 runtime assertion으로 호출을 래핑할 수 있습니다. 하지만 강제 언래핑과 마찬가지로 실제 에러가 발생할 시 런타임 에러가 나타난다는 단점이 있지요..
let x = try! getNextYear(year: 3000)
print(x) //런타임 에러!
let x = try! getNextYear(year: 2000)
print(x) //2001
두번째 코드를 먼저 살펴보면 2000년을 넣었으므로 오류가 나지 않아 x값에 2001이 잘 들어가게 됩니다. 하지만 첫번째 코드는 오류가 나는 코드이므로 런타임 에러가 납니다. 따라서 이것또한 강제 언래핑과 마찬가지로 아주 조심스럽게 사용을 해야될 것 같습니다. 경로에 이미지 리소스를 로드하거나 경로가 없을 시 에러를 발생시키는 loadImage(atPath:) 함수를 사용할 때 오류 전파 비활성화 방법을 많이 사용한다고 합니다. 이미지는 애플리케이션과 함께 제공되므로 런타임 시 에러가 발생하지 않을 것이라 확신할 수 있기 때문이라네요.
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")
오늘은 오류 처리에 관해 공부를 해보았습니다. 아직까지는 무거운 프로그램을 다루지 않아서 오류 처리에 대한 필요성을 크게 못 느끼고 있지만 나중에는 오류가 일어나는 사태를 막아야하는 매우 중요한 작업이 될 것 같습니다. 그럼 오늘 하루도 수고 많으셨습니다~