Skip to content

Latest commit

 

History

History
680 lines (475 loc) · 14.4 KB

003. Error Handling.md

File metadata and controls

680 lines (475 loc) · 14.4 KB

Error Handling

2021년 7월 3주차

목차

  1. 에러 표현 방식
  2. 에러 처리
  3. rethrow
  4. defer

1. 에러 표현 방식

: 타입이 Error protocol을 채택하여 오류를 표현할 수 있다

1. Enum

  • 오류의 종류를 표현하기에 적합함
  • associated value를 사용하여 오류에 관한 부가 정보를 제공할 수 있음
enum ValidationError: Error {
    case tooShort
    case tooLong
    case invalidCharacterFound(Character)
}

func validate(username: String) throws {
    guard username.count > 4 else {
        throw ValidationError.tooShort
    }

    guard username.count < 11 else {
        throw ValidationError.tooLong
    }

    for character in username {
        guard character.isLetter else {
            throw ValidationError.invalidCharacterFound(character)
        }
    }
}

do {
    try validate(username: "Lia!!!")
} catch {
    print(error) // invalidCharacterFound("!")
}

2. Struct / Class

Error 타입 구분하기

struct CustomErrorStruct : Error {
    var message : String
}

class CustomErrorClass : Error { }


do {
    let error = CustomErrorStruct(message:"some error")
    throw error
} catch let error where error is CustomErrorStruct {
    print("error with Struct")
} catch let error where error is CustomErrorClass {
    print("eroor with Class")
} catch let error {
    print("unknown error", error)
}

Xcode가 보여주는 error 와 비슷한 형태

struct XMLParsingError: Error {
   enum ErrorKind {
       case invalidCharacter
       case mismatchedTag
       case internalError
   }

   let line: Int
   let column: Int
   let kind: ErrorKind
}

func parse(_ source: String) throws -> XMLDoc {
   // ...
   throw XMLParsingError(line: 19, column: 5, kind: .mismatchedTag)
   // ...
}

do {
   let xmlDoc = try parse(myXMLData)
} catch let error as XMLParsingError {
   print("Parsing error: \(error.kind) [\(error.line):\(error.column)]")
} catch {
   print("Other error: \(error)")
}
// Prints "Parsing error: mismatchedTag [19:5]"

3. LocalizedError

  • Error protocol을 채택한 타입을 확장하여 LocalizedError을 채택하면 된다
  • 원하는 에러 메시지 구현 가능

참고) NSLocalizedString의 comment는 이해하기 쉽게 하기 위한 주석으로, 기능 상 이점은 크게 없다

enum ValidationError: Error {
    case tooShort
    case tooLong
    case invalidCharacterFound(Character)
}

extension ValidationError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .tooShort:
            return NSLocalizedString(
                "Under length",
                comment: "Username needs to be at least 5 characters long"
            )
        case .tooLong:
            return NSLocalizedString(
                "Exceeding length",
                comment: "Username can't be longer than 10 characters"
            )
        case .invalidCharacterFound(let character):
            let format = NSLocalizedString(
                "Invalid character exception : '%@' contained",
                comment: "Username can't contain the character '%@'"
            )

            return String(format: format, String(character))
        }
    }
}

func validate(username: String) throws {
    guard username.count > 4 else {
        throw ValidationError.tooShort
    }

    guard username.count < 11 else {
        throw ValidationError.tooLong
    }

    for character in username {
        guard character.isLetter else {
            throw ValidationError.invalidCharacterFound(character)
        }
    }
}

do {
    try validate(username: "Lia!!!")
} catch {
    print(error.localizedDescription) // Invalid character exception : '!' contained
}

2. 에러 처리

1. try, throw

  • 함수에서 발생한 오류를 해당 함수를 호출한 코드에 알리는 방법
  • throws 키워드를 사용해야 오류를 던질 수 있음
  • 함수, 메서드, 이니셜라이져에 throws 키워드 사용 가능
  • throws는 함수의 자체 타입에도 영향을 미침
    • 같은 이름의 함수 ↔ 같은 이름의 throws 함수 구분
    • throws 함수 → 일반 함수 재정의 ❌
    • 일반 함수 → throws 함수 재정의 ⭕️
  • 에러를 던질 가능성이 있다면, throws 키워드를 붙여줘야함
enum AgeError: Error {
    case impossibleAge
    case tooYoung
    case tooOld
}

func validate(age: Int) throws {
    guard age > 0 && age < 130 else {
        throw AgeError.impossibleAge
    }
    guard age > 16 else {
        throw AgeError.tooYoung
    }
    guard age < 60 else {
        throw AgeError.tooOld
    }
    
    print("나이 \(age)세, 기준을 충족합니다")
}

try validate(age: 22) // "나이 22세, 기준을 충족합니다"
try validate(age: 7) // error!

// Fatal error: Error raised at top level: AgeError.tooYoung

2. do - catch

  • 에러 발생을 전달받으면, 에러를 처리해줌
  • do 절 내부에서 코드 오류를 던지고, catch 절에서 에러를 전달받아 처리해줌
func applyForPosition(age: Int) {
    do {
        try validate(age: age)
    } catch {
        print(error)
    }
}

applyForPosition(age: 22) // "나이 22세, 기준을 충족합니다"
applyForPosition(age: 7)  // tooYoung

3. try? try!

  • try? 는 호출한 함수가 에러를 던질 때, 리턴값이 nil이 된다
  • 에러가 아닐 경우는 그냥 optional 로 리턴

optional 값으로 에러 처리

func someThrowingFunction(shouldThrowError: Bool) throws -> Int {
    if shouldThrowError {
        enum SomeError: Error {
            case justSomeError
        }
        throw SomeError.justSomeError
    }
    return 100
}

let x: Optional = try? someThrowingFunction(shouldThrowError: true)
print(x)   // nil

let y: Optional = try? someThrowingFunction(shouldThrowError: false)
print(y)   // Optional(100)

optional 반환 타입과 optional 값으로 에러 처리하는 방법의 결합

func fetchData() -> Data? {
    if let data = try? fetchDataFromDisk() {
        return data
    }
    if let data = try? fetchDataFromServer() {
        return data
    }
    return nil
}
  • try! 는 에러가 발생하지 않을 것이라 확신하고 쓰는 방법
  • 에러 발생 시, 런타임 오류
  • try! 는 지양하자
func someThrowingFunction(shouldThrowError: Bool) throws -> Int {
    if shouldThrowError {
        enum SomeError: Error {
            case justSomeError
        }
        throw SomeError.justSomeError
    }
    return 100
}


let y: Optional = try! someThrowingFunction(shouldThrowError: false)
print(y)   // 100

let x: Optional = try! someThrowingFunction(shouldThrowError: true)
print(x)   // Fatal error: 'try!' expression unexpectedly raised an error

3. rethrow

  • rethrow 키워드를 통해, 매개변수로 받은 함수가 오류를 던진다는 것을 명시
  • 에러를 다시 받아서 던지는지(rethrowing), 그냥 자신의 에러를 던지는지(throwing) 명시하는 효과

1. 사용법

func someThrowingFunc() throws {
  enum SomeError: Error {
    case justSomeError
  }
  
  throw SomeError.justSomeError
}

func someFunction(callback: () throws -> Void) rethrows {
  enum AnotherError: Error {
    case justAnotherError
  }
  
  do {
    try callback()
  } catch {
    throw AnotherError.justAnotherError
  }
  
  do {
    try someThrowingFunc()
  } catch {
    // 에러 발생!!
    throw AnotehrError.justAnotherError
  }
  
  // catch 절 외부에서 단독으로 오류 던질 수 없음
  // 에러 발생!!
  throw AnotherError.justAnotherError
}

2. 상속에서 규칙

protocol SomeProtocol {
    func someThrowingFunctionFromProtocol(callback: () throws -> Void) throws
    func someRethrowingFunctionFromProtocol(callback: () throws -> Void) rethrows
}

class SomeClass: SomeProtocol {
    func someThrowingFunction(callback: () throws -> Void) throws { }
    func someRethrowingFunction(callback: () throws -> Void) rethrows { }
    
    // rethrow 메서드는 throw 메서드를 요구하는 프로토콜의 요구사항에 부합
    func someThrowingFunctionFromProtocol(callback: () throws -> Void) rethrows { }
    
    // throw 메서드는 rethrow 메서드를 요구하는 프로토콜의 요구사항을 충족할 수 없음
    // 컴파일 에러!
    func someRethrowingFunctionFromProtocol(callback: () throws -> Void) throws { }
    
}

class SomeChildClass: SomeClass {
    // 부모클래스의 throw 메서드는 자식클래스에서 rethrow 메서드로 재정의 가능
    override func someThrowingFunction(callback: () throws -> Void) rethrows { }
    
    // 부모 클래스의 rethrow 메서드는 자식클래스의 throw 메서드로 재정의 불가
    // 컴파일 에러!
    override func someRethrowingFunction(callback: () throws -> Void) throws { }
}
  • throw → rethrow ⭕️
  • rethrow → throw ❌

3. Swift에서 rethrows 사용 예시

func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T]

사용 예시

enum MyError: Error {
    case cannotDivide
}

func divideNumber(first: Float, second: Float) throws -> Float {
    if second == 0 {
        throw MyError.cannotDivide
    }
    return first/second
}

func calculateFunction(function: (Float, Float) throws -> Float) rethrows {
    print(try function(2, 0))
}

do {
    try calculateFunction(function: divideNumber)
} catch {
    print(error, ": 0으로 나눌 수 없음!")
}
  • rethrow 를 throw 라고 적어도 문제 없음

4. defer

: 미루다, 연기하다는 뜻

  • 현재 코드 블록을 나가기 전에 꼭 특정 코드를 실행해야 하는 경우 사용
  • 에러 처리 외에도 사용될 수 있음

1. 기본 사용법

  • 실행 순서
    • 맨 마지막에 작성된 구문부터 역순으로 실행됨
    • 오류가 던져지면, 그 직전 defer 까지 실행됨
    • 현재 코드 블록을 나가기 전에 무조건 실행됨
  • defer 내부에는 break, return 같이 구문을 빠져나가는 코드나, 오류 던지는 코드를 작성해서는 안됨
func someThrowingFunction(shouldThrowError: Bool) throws -> Int {
  print("1")
  
  defer {
    print("2")
  }
  
  do {
    defer {
      print("3")
    }
    print("4")
  }
  
  defer {
    print("5")
  }
  
  if shouldThrowError {
    enum SomeError: Error {
      case justSomeError
    }
    
    throw SomeError.justSomeError
  }
  
  defer {
    print("6")
  }
  
  print("7")
  
  return 100
}

try? someThrowingFunction(shouldThrowError: true)
// 1
// 4
// 3
// 5
// 2

try? someThrowingFunction(shouldThrowError: false)
// 1
// 4
// 3
// 7
// 6
// 5
// 2

2. 에러 처리 시 사용법

  • 주의 사항 : throw보다 앞에 작성해야 실행됨
func someThrowingFunction() throws {
    defer {
        print("finished")
    }
    throw CustomErrorStruct(message: "struct error~")
}

try someThrowingFunction()

// finished
// Swift/ErrorType.swift:200:
// Fatal error: Error raised at top level
// : study_swift.CustomErrorStruct(message: "struct error~")
  • NSLock 을 이용해 데드락을 피할 방법으로 쓸 수 있음
let myLock: NSLock = .init()

func fetchData() {
    myLock.lock()
    
    defer { myLock.unlock() }
    
    if data == nil { return }
    
    //이후 작업
}

3. defer가 호출되지 않는 경우

: 함수가 종료되는 경우

[1] throw로 오류를 던질 경우
func test(isError: Bool) throws -> Void{
    defer {
        print("1")
    }

    if isError {
        enum TestError: Error {
            case error
        }

        throw TestError.error
    }

    defer {
        print("2")
    }

    print("3")
}

try? test(isError: true)
// 1
try? test(isError: false)
// 3 2 1
  • throw 로 인해 함수가 종료될 경우, 아래 선언된 defer에 도달하지 못해 호출되지 않는다
[2] if, guard 문을 사용하여 중간에 함수를 종료하는 경우
func test(string: String?){
    defer {
        print("1")
    }

    guard let str = string else {
        return
    }

    defer {
        print("2")
    }

    print("3")
}

test(string: nil)
// 1
test(string: "test")
// 3 2 1
  • throw 와 마찬가지로 defer 에 도달하기 전에 함수가 종료되어 호출되지 않는다
[3] 리턴값이 Never(비반환함수)인 경우

Never : 정상적으로 끝나지 않은 함수

  • 오류를 보고하는 일을 하고, 프로세스를 종료한다
func test() -> Never {
    defer {
        print("test 1")
    }

    defer {
        print("test 2")
    }

    defer {
        print("test 3")
    }

    abort() // test 중일 때 앱을 종료하는 함수
}
  • 에러가 발생하면서 함수를 반환하지 않고 실행을 종료하기 때문에 defer가 호출되지 않는다

4. defer가 예상치 못하게 동작하는 경우 (동작 원리)

var a: String? = nil

func b() -> String {
    a = "Hello world"
    defer { a = nil }
    return a!
}

print(b()) // Hello world
  • force unwrapping에서 crash가 날 것이라 예상했으나, 잘 출력됨

Hopper 를 이용해 수도 코드 분석하기

img

수도 코드에서 색칠된 부분과 함수 코드 매칭

  • 앱이 crash 나지 않는 이유 : a값을 읽는 작업 후에 a = nil이 실행되기 때문

  • 이를 통해 defer 가 로직 이전이나 이후가 아니라 로직 중간에 나타남을 알 수 있음

  • 리턴 후에 defer 호출

출처