[TIL]04.28

rbw·2022년 4월 29일
0

TIL

목록 보기
16/98
post-thumbnail

Error Handling

Runtime Error

런타임 에러는 프로그램이 실행되는 동안 발생합니다. 보통 발생하면 프로그램이 강제 종료됩니다.

먼저, 에러 형식에 대해서 알아보겠습니다.

// 에러를 전달하는 것을 던진다고 표현합니다, 문법은 다음과 같습니다.
// throws 는 에러를 던질 수 있음을 의미합니다. throw는 에러를 던지는 키워드이므로 잘 구분해야 합니다
throw expression

func name(parameters) throws -> ReturnType {
    statements
}

init(parameters) throws {
    statements
}

{ (parameters) throws -> ReturnType in
    statements
}

// 에러는 필수로 선언해야할 속성이 없어서, 아래와 같은 방식으로 선언만 하면 된다.
enum DataParsingError: Error {
    case inivalidType
    case invalidField
    case minssingRequiredField(String)
}

func parsing(data: [String: Any]) throws {
    guard let _ = data["name"] else {
        throw DataParsingError.missingRequiredField("name")
    }

    guard let _ = data["age"] as? Int else {
        throw DataParsingError.invalidType 
    }
}

try Statements

에러를 리턴하는 함수를 호출할 때는 try 표현식을 사용합니다.

// 문법
try expression
try? expression
try! expression

try? parsing(data: [:]) // 처음 guard문에서 else가 실행되고 nil로 반환후 종료함

에러를 처리하는 방식은 크게 3가지 입니다.

  1. do-catch Statements - 주로 코드에서 발생한 에러를 개별적으로 처리 할 때 사용
  2. try Expression + Optional Binding
  3. hand over - 전달 받은 에러를 다른 코드 블럭으로 다시 전달합니다.

do-catch Statements

// 문법
do {
    try expression
    statements
} catch pattern {
    statements
} catch pattern where condition {
    statements
}

// 위의 예제
// 캐치 블럭을 작성할 때에는 가장 까다로운 에러 케이스 부터 작성해야 한다.
do {
    try parsing(data: [:])
} catch DataParsingError.invalidType {
    print("Invalid Type Error")
} catch {
    pritn("handl error")
}

기본적으로, do-catch문은 do 블럭에서 생길 수 있는 에러를 전부 처리해야만 합니다. 그게 불가능 하다면 다른 코드에 에러를 던지도록 해야 합니다. 따라서 함수 내부에 있는 do-catch문은 해당 에러를 다 처리하는 코드를 작성해야 합니다.

패턴이 없는 catch 블럭에는 error 라는 로컬 상수가 제공됩니다. Error 프로토콜이므로, 사용시 타입 캐스팅이 필요합니다.

func handleError() throws {
    do {
    try parsing(data: [:])
    } catch {
        if let error = error as? DataParsingError {
            switch error {
                case .invalidType:
                    print("Invalid type")
                default:
                    print("handle error")
            }

        }
    }
}

Multi-pattern Catch Clauses(5.3+)

하나의 catch블록에서 두 개 이상의 에러를 매칭 시킬 수 있습니다.

이 기능이 없을 떄에는 아래와 같이 해결하였습니다.

do {
    try parsing(data: [:])
    // err에 형식을 바인딩하여 여러 케이스를 살펴봄
} catch let err as DataParsingError {
    switch err {
    case .invalidType, .invalidField:
        // error handling
        break
    default:
        break
    }
}

// 새로운 기능이 나온 후에는 콤마로 구분하면 끝입니다.
do {
    try parsing(data: [:])
} catch DataParsingError.invalidType, DataParsingError.invalidField {
    // error handling
} 

defer statements

defer문은 코드의 실행을 스코프가 종료되는 시점으로 연기시킵니다. 주로 코드에서 사용했던 자원을 정리할 때 사용합니다.

// 문법
defer {
    statements
}

// 파라미터로 전달된 경로에서 해당되는 파일을 읽어서 작업 후, 닫는 함수
func processFile(path: String) {
    let file = FileHandle(forReadingAtPath: path)

    // 아래와 같이 defer문을 사용하지 않는다면
    // 파일이 갑자기 닫힐 가능성이 있어서 위험합니다.
    //  if path.hasSuffix(".jpg") {
    //     return
    // }
    
   // file?.closeFile() 
    defer {
        file?.closefile()
    }

     if path.hasSuffix(".jpg") {
        return
     }
}

Result Type (Swift 5+)

기존 스위프트의 에러를 처리하는 방식의 코드에는 throws 키워드로 에러를 던지는 것을 알수는 있지만 에러의 형식을 특정하지는 못합니다. 하나의 블럭에서 다양한 형식의 에러를 던질 수 있고, 코드 블럭을 호출하는 부분에서는 어떤 형식이 전달되는지 문법적으로 파악하지 못합니다.

catch 블럭으로 전달되는 시점에는, 에러의 프로토콜 형식이 전달됩니다. 여기서 문제가 생깁니다. 에러를 처리하기 위해서는 함수가 전달하는 에러의 형식을 알고 있어야 합니다.

// 기존 에러를 처리하는 코드
func process(oddNumber: Int) throws -> Int {
    guard oddNumber >= 0 else {
        throw NumberError.negativeNumber
    }
    guard !oddNumber.isMultipple(of: 2)  else {
        throw NumberError.evenNumber
    }
}


do {
    let result = try process(oddNumber: 1)
    print(result)

}// 에러를 던질 때 에러의 형식을 알아야 하므로, 타입 캐스팅이 필요하다. 
  catch let myErr as NumberError {
     switch myErr {
     case .negativeNumber:
        print("negative number")    
     case .evenNumber:
        print("even number")    
    }
} catch {
    print(error.localizedDescription)
} // 에러가 도중에 추가되는 경우 여기서 처릭된다. 이는
 // 에러를 올바르게 처리했다고 볼 수 없다.

이러한 방법을 해결하기 위해 제네릭 열거형인 Result 타입을 사용합니다.

// Result 타입을 사용한 에러처리, 이는 throwing 클로저로 초기화 하는 생성자를 제공합니다
let result = Result { try process(oddNumber: 1) }

switch result {
    case .success(let data):
        print(data)
    case .failure(let error):
        print(error.localizedDescription)
}  

// 성공시의 리턴형식, 실패시의 리턴형식을 명시해줘야한다.
// 이 타입으로 에러를 처리할 때는 함수에서 에러를 던지지 않고, 연관값으로 저장해서 리턴합니다.
func processResult(oddNumber: Int) -> Result<Int, NubmerError> {
    guard oddNumber >= 0 else {
        return .failure(.negativeNubmer)
    }
    guard !oddNumber.isMultipple(of: 2)  else {
        return .failure(.evenNumber)
    }

    return .success(oddNubmer * 2)
}

성공과 실패를 열거형 으로 처리하고, 에러는 실제로 결과를 사용하는 시점에서 처리됩니다.

에러를 처리하는 시점이 함수를 호출하는 시점에서, 작업 결과를 사용하는 시점으로 이동한 패턴을 Delayed Error Handling 라고 부릅니다.

컴파일 때 에러 형식을 명확히 인식할 수 있다는 것은 형식 안정성이 보장이 된다는 의미이다. 타입 캐스팅 없이 에러를 처리할 수 있고, 형식 추론을 통해서 코드가 더 단순해집니다.

Result 고차함수

4가지 고차함수를 제공합니다.

func doSomethingWithResult(data: Int) -> Result<Int, MyError> {
    guard data.isMultiple(of: 2) else {
        return .failure(MyError.error)
    }
    return .success(data) 
}

let a = doSomethingWithResult(data: 0)
let b = a.map { $0.isMultiple(of:2) ? "even nubmer" : "odd number" }

// faltMap은 Result 인스턴스로 리턴한다. 
let c = a.flatMap {$0.isMultiple(of:2) ? .success("even nubmer") : .success("odd number")}

// 아래 코드는 실패값을 반환하는 차이를 빼면 위의 메소드와 차이가 없다.
a.mapError(transform: (MyError) -> Error)
a.flatMapError(transform: (MyError) -> Result<Int, Error>)

Assertion and Precondition

코드가 실행 중인 상태에서 입력된 값의 무결성을 확인하거나, 실행 결과를 검증할 때 사용하는 디버깅 도구입니다. 특정 조건이 true이면 진행하고 false이면 중지하고 메시지 출력 후 해당 코드로 이동합니다

assert는 디버깅 모드에서만 사용되므로, 앱을 출시할 때 코드에 포함되어 있어도 상관이 없다

릴리즈 모드에서 assert를 활용하고 싶다면, precondition 함수를 사용하면 된다. 하지만 이는 종료만 시키고, 디버그 정보를 포함하지 않는다. 이를 사용하는 경우는, 특정 조건을 만족시키지 않을 떄 종료를 시키는 경우가 생긴다면 그 때 활용한다. 예를 들면, 비밀번호를 너무많이 틀린다거나 등등?

// 문법
assert(number > 0, "negative number or zero not allowed")
precondition(number > 0, "negative number or zero not allowed")
profile
hi there 👋

0개의 댓글