Swift에서 에러의 종류와 원인은 정말 다양하지만, 크게 컴파일 에러(Compile Error)와 런타임 에러(Runtime Error)로 나뉩니다.
📌 컴파일 에러(Compile Error)
- 컴파일 에러는 개발자가 코드를 잘못 작성하여 발생하는 에러입니다.
- 컴파일 에러가 발생 시 컴파일러가 에러 메시지를 개발자에게 보여주기 때문에 비교적 쉽게 문제점을 해결할 수 있습니다.
📌 런타임 에러(Runtime Error)
- 코드의 문법적인 문제 없지만, 코드 실행 시 여러 가지 요인(설계 미숙, 언어 및 IDE의 업데이트) 때문에 발생하는 에러입니다.
- 런타임 에러는 즉각적으로 확인할 수 없기 때문에 에러 처리 문법을 사용하여 에러 발생을 대처해야 합니다.
코드 실행 중 런타임 에러가 발생하면 프로그램이 강제로 다운되는 "크래시(crash)" 현상이 발생할 수 있기 때문에 적절한 조치가 필요합니다.
Swift의 에러 처리는 크게 3단계로 나뉩니다.
1️⃣ 에러 정의 (어떤 에러가 발생할지 미리 정의)
2️⃣ 에러가 발생할 수 있는 함수 정의
3️⃣ 에러가 발생할 수 있는 함수 실행
✅ 1단계: 에러 정의
어떤 경우의 에러가 발생할지 미리 정의해야 합니다.
개발자는 열거형 타입으로 에러를 정의해야 하며, 에러 프로토콜(Error)을 채택해야 합니다.
enum NameError: Error{ // 개발자가 만든 열거형 타입의 에러에 에러 프로토콜(Error)을 채택 case noName }
✅ 2단계: 에러 발생 함수 정의
에러가 발생할 수 있는 함수를 정의할 때는 파라미터(parameter) 괄호 다음에 throws라는 키워드를 작성해야 합니다.
throws라는 키워드를 통해 해당 함수는 "에러를 던질 수 있는 함수 타입"으로 변합니다.
func checkingName(name: String) throws -> String{ // throws라는 키워드를 작성 if name.isEmpty{ throw NameError.noName // 에러를 던지는 코드 } else{ return name } }
✅ 3단계: 에러 발생 함수 실행
throws라는 키워드가 들어간 함수는 바로 사용할 수 없습니다.
에러 발생 함수를 실행하기 위해서는 "do{ try }, catch{ }" 문법을 사용해야 합니다.
do{ // 함수를 실행하는 블럭 try print(checkingName(name: "김철수")) } catch{ // 에러를 처리(실행)하는 블록 print("이름이 없습니다.") }
✅ 종합적인 코드
// 1단계 에러 정의 enum NameError: Error{ // 개발자가 만든 열거형 타입의 에러에 에러 프로토콜(Error)을 채택 case noName } // 2단계 에러 발생 함수 정의 func checkingName(name: String) throws -> String{ if name.isEmpty{ throw NameError.noName // 에러를 던지는 코드 } else{ return name } } // 3단계 에러 발생 함수 실행 do{ // 정상적인 처리(실행)를 하는 블럭 try print(checkingName(name: "")) } catch{ // 에러를 처리(실행)하는 블록 print("이름이 없습니다.") } /* 출력 결과 이름이 없습니다. */
✅ try
- 일반적인 방법의 에러 처리입니다.
- 일반적인 에러를 처리할 때는 do{ 정상 실행 } 문과 catch{ 에러 실행 } 문을 각각 작성해야 합니다.
do{ try print(checkingName(name: "")) } catch{ print("이름이 없습니다.") }
✅ try? (옵셔널 트라이)
- 함수의 결과를 옵셔널 타입으로 리턴하는 방식의 에러 처리입니다.
- 정상 실행의 경우에는 옵셔널 타입으로 값을 리턴합니다.
- 에러 실행의 경우에는 nil을 리턴합니다.
- 에러 실행 시 nil을 리턴하기 때문에 catch{ 에러 실행 }문을 작성할 필요가 없습니다.
// 에러 발생시 nil 리턴 do{ try? print(checkingName(name: "")) // 이름이 없기 때문에 에러 발생 } /* 출력 결과 <출력 결과가 없습니다.> */ // 정상 실행 결과는 옵셔널 타입으로 리턴됩니다. do{ var kim = try? checkingName(name: "김철수") print(kim) // Optional("김철수") } /* 출력 결과 Optional("김철수") */
✅ try! (Forced 트라이)
- 정상 실행의 경우에는 정상 타입으로 값을 리턴합니다.
- 에러 실행의 경우에는 런타임 에러가 발생합니다.
- 에러 실행의 경우에는 런타임 에러가 발생하기 때문에 catch{ 에러 실행 } 문을 작성할 필요가 없습니다.
- 에러가 발생할 수 없다고 확신하는 경우에만 사용합니다.
do{ var kim = try! checkingName(name: "김철수") print(kim) // 김철수 } /* 출력 결과 김철수 */ do{ var kim = try! checkingName(name: "") // 🚨에러 발생 print(kim) // 김철수 }
catch{ } 블럭의 개수 또는 형태에 따라 다양한 경우의 에러를 자세하게 처리할 수 있습니다.
(에러를 자세하게 처리하고 싶을 때는 catch{ } 블럭을 여러 개 작성하면 됩니다.)
✅ 기본적인 Catch블럭 처리법
enum ageError: Error{ case maxAge case minAge } func checkingAge(age: Int) throws -> String{ if age < 1{ throw ageError.minAge } else if age > 19{ throw ageError.maxAge } else{ return "미성년자 입니다." } } do{ try print(checkingAge(age: 0)) } catch{ // catch 블록이 1개인 경우 자세한 에러 처리가 힘들다. (1개의 catch 블록이 디폴트 에러처리 블록입니다.) print("나이 범위를 벗어났습니다.") } /* 처리 결과 나이 범위를 벗어났습니다. */
✅ 모든 에러 패턴을 정의
- 각 catch블럭에 모든 경우의 에러를 정의
do{ try print(checkingAge(age: 0)) } catch ageError.maxAge{ print("성인 입니다.") } catch ageError.minAge{ print("나이가 0살 이하입니다.") } catch{ // 디폴트 에러 print("에러 발생") } /* 처리 결과 나이가 0살 이하입니다. */
✅ 패턴 없이 정의
- catch블럭에서 기본으로 제공하는 error 상수를 사용하여 블록 내부에서 구체적으로 정의
do{ try print(checkingAge(age: 25)) } catch { if let error = error as? ageError{ switch error{ case .maxAge: print("성인 입니다.") case .minAge: print("0살 이하힙니다.") } } } /* 처리 결과 성인 입니다. */
함수 안에 에러를 던지는 함수를 정의하여 사용하는 방식입니다.
해당 방식을 통해 다양한 형태의 축약 표현을 할 수 있습니다.
✅ 에러를 던지는 함수를 처리하는 함수 - 일반적인 정의
enum NameError: Error{ case noName } // 에러를 던지는 함수 정의 func checkingName(name: String) throws -> String{ if name.isEmpty{ throw NameError.noName } else{ return name } } //에러를 던지는 함수를 처리하는 함수 정의 func handleError(){ // 함수 내부에 do{ try } ~ catch{ }문 정의 do{ try print(checkingName(name: "")) } catch{ print("이름이 없습니다.") } } handleError() /* 출력 결과 이름이 없습니다. */
✅ 에러를 던지는 함수를 처리하는 함수 - throwing 함수로 에러 다시 던지기
enum NameError: Error{ case noName } func checkingName(name: String) throws -> String{ if name.isEmpty{ throw NameError.noName } else{ return name } } func handleError() throws{ do{ try print(checkingName(name: "")) } // catch{ // 생략 가능 // print("이름이 없습니다.") // } } do{ try handleError() // 정상 작동시 handleError() 실행 } catch { print("이름이 없습니다.") // 에러 발생시 실행 } /* 출력 결과 이름이 없습니다. */
✅ 에러를 던지는 함수를 처리하는 함수 - rethrowing 함수로 에러 다시 던지기
enum SomeError: Error { case aError } // 무조건 에러를 던지는 함수 정의 func throwingFunc() throws { throw SomeError.aError } // 에러를 던지는 throwing 함수로 받는 함수를 파라미터로 받는 함수 정의 // rethrows -> 내부에서 다시 에러를 던지는 키워드 func someFunction1(callback: () throws -> Void) rethrows { try callback() // 에러를 다시 던짐(직접 던지지 못함) // throw (X) } do { try someFunction1(callback: throwingFunc) } catch { print("에러 발생") } /* 출력 결과 에러 발생 */
위의 에러 처리 코드를 확인해보면 알 수 있듯이 처리 과정이 생각보다 복잡하고 귀찮습니다...
이러한 단점을 보완하고자 Swift5 이후에 나온 기능이... 바로 Result Type 문법이 되겠습니다.