2025/07/20

12. Swift 오류 처리(Error Handling): 견고한 앱을 만드는 필수 요소

12. Swift 오류처리 썸네일
안녕하세요! 지난 블로그에서는 코드의 유연성과 확장성을 높이는 설계도, 프로토콜에 대해 알아보며 Swift다운 프로그래밍 패러다임을 이해했습니다. 오늘은 여러분이 만드는 앱을 더욱 견고하고 안정적으로 만들어 줄, 바로 오류 처리(Error Handling)에 대해 깊이 파고들 것입니다. 🛡️

프로그램을 만들다 보면 예상치 못한 상황이 발생하곤 합니다. 예를 들어, 존재하지 않는 파일을 열려고 하거나, 네트워크 연결이 끊기거나, 유효하지 않은 입력값을 받거나 하는 경우들이죠. 이러한 상황을 적절하게 처리하지 않으면 앱이 강제로 종료(크래시)되어 사용자 경험을 심각하게 해칠 수 있습니다. Swift는 이러한 오류(Error) 상황을 우아하고 안전하게 다룰 수 있는 메커니즘을 제공합니다.

1. 오류(Error)란 무엇인가요?

프로그래밍에서 오류(Error)는 프로그램의 정상적인 흐름을 방해하는 예외적인 상황을 의미합니다. Swift에서 오류는 Error 프로토콜을 준수하는 타입으로 표현됩니다. 주로 열거형을 사용하여 특정 함수나 메서드에서 발생할 수 있는 여러 종류의 오류를 정의합니다.

💡 왜 오류 처리가 필요한가요?

  • 안정성: 예기치 않은 문제로 인해 앱이 갑자기 종료되는 것을 방지하고, 사용자에게 친절한 피드백을 제공할 수 있습니다.
  • 사용자 경험: 오류가 발생했을 때 적절한 메시지를 보여주거나 대안을 제시하여 사용자가 혼란을 겪지 않도록 돕습니다.
  • 예측 가능성: 발생 가능한 모든 오류 상황을 명시적으로 다루어 코드의 예측 가능성을 높입니다.

1.1. 오류 타입 정의하기

Swift에서 사용자 정의 오류 타입을 만들려면, Error 프로토콜을 채택하는 열거형을 사용하는 것이 일반적입니다.
//Swift 예제
enum DataFetchingError: Error {
    case invalidURL
    case networkError(String) // 연관 값을 통해 에러 상세 정보 포함
    case decodingFailed
    case noDataFound
}

enum PaymentError: Error {
    case insufficientFunds(currentBalance: Double)
    case invalidCard
    case transactionFailed
}
위 예시에서 DataFetchingError와 PaymentError는 각각 데이터 가져오기 및 결제 과정에서 발생할 수 있는 구체적인 오류 상황을 나타냅니다.

2. 오류 발생시키기: throws 키워드와 throw 문

함수나 메서드가 오류를 발생시킬 수 있음을 나타내려면, 해당 함수 선언부에 throws 키워드를 추가해야 합니다. 실제 오류가 발생했을 때는 throw 문을 사용하여 정의된 오류 타입을 던집니다.
//Swift 예제
func fetchData(from urlString: String) throws -> String {
    guard let url = URL(string: urlString) else {
        // 유효하지 않은 URL이면 invalidURL 오류를 던짐
        throw DataFetchingError.invalidURL
    }

    // 실제 네트워크 통신 또는 데이터 처리 로직 (가정)
    // 여기서는 간단히 예시를 위해 조건부로 오류를 던집니다.
    if urlString.contains("bad_network") {
        // 네트워크 문제가 발생하면 networkError 오류를 던짐
        throw DataFetchingError.networkError("네트워크 연결이 불안정합니다.")
    }

    if urlString.contains("no_data") {
        // 데이터가 없으면 noDataFound 오류를 던짐
        throw DataFetchingError.noDataFound
    }

    print("데이터를 성공적으로 가져왔습니다: \(url)")
    return "Fetched data from \(url)"
}
throws가 붙은 함수는 일반 함수처럼 직접 호출할 수 없으며, 반드시 오류를 처리하는 구문 내에서 호출해야 합니다.

3. 오류 처리하기: do-catch 문

Swift에서 발생한 오류를 처리하는 가장 일반적인 방법은 do-catch 문입니다. do 블록 안에서 오류를 발생시킬 수 있는 코드를 실행하고, catch 블록에서 특정 오류가 발생했을 때 어떻게 대응할지 정의합니다.
//Swift 예제
func processData(url: String) {
    do {
        // 오류를 발생시킬 수 있는 함수 호출
        let data = try fetchData(from: url)
        print("데이터 처리 성공: \(data)")
    } catch DataFetchingError.invalidURL {
        // invalidURL 오류 처리
        print("오류: 유효하지 않은 URL 형식입니다.")
    } catch DataFetchingError.networkError(let message) {
        // networkError 오류와 연관 값 처리
        print("오류: 네트워크 문제 발생 - \(message)")
    } catch DataFetchingError.noDataFound {
        // noDataFound 오류 처리
        print("오류: 요청한 데이터를 찾을 수 없습니다.")
    } catch { // 모든 다른 오류를 처리하는 catch 블록
        print("알 수 없는 오류 발생: \(error.localizedDescription)") // error는 암시적으로 생성된 상수
    }
}

print("--- 유효한 URL로 데이터 가져오기 ---")
processData(url: "https://example.com/data")
// 출력: 데이터를 성공적으로 가져왔습니다: https://example.com/data
//       데이터 처리 성공: Fetched data from https://example.com/data

print("\n--- 유효하지 않은 URL로 데이터 가져오기 ---")
processData(url: "invalid-url")
// 출력: 오류: 유효하지 않은 URL 형식입니다.

print("\n--- 네트워크 오류 발생시키기 ---")
processData(url: "https://example.com/bad_network")
// 출력: 오류: 네트워크 문제 발생 - 네트워크 연결이 불안정합니다.

print("\n--- 데이터 없음 오류 발생시키기 ---")
processData(url: "https://example.com/no_data")
// 출력: 오류: 요청한 데이터를 찾을 수 없습니다.
catch 블록은 특정 오류 타입을 명시하여 해당 오류만 처리할 수 있고, 마지막 catch 블록은 모든 남은 오류를 처리하는 데 사용됩니다. error라는 암시적인 상수를 통해 발생한 오류 객체에 접근할 수 있습니다.

4. 옵셔널 try? 및 강제 try!

do-catch 문 외에도 Swift는 오류 처리의 편의를 위한 두 가지 방법을 제공합니다.

4.1. try?: 오류를 옵셔널로 변환

try?를 사용하여 오류를 발생시킬 수 있는 표현식을 호출하면, 오류가 발생했을 때 nil을 반환하는 옵셔널 값을 얻을 수 있습니다. 오류가 발생하지 않으면 옵셔널 래핑된 결과 값을 반환합니다. 오류 내용 자체는 중요하지 않고, 성공 또는 실패 여부만 중요할 때 유용합니다.
//Swift 예제
let validData = try? fetchData(from: "https://example.com/data")
print("유효한 데이터 시도: \(validData ?? "nil")") // 출력: 유효한 데이터 시도: Optional("Fetched data from https://example.com/data")

let invalidURLData = try? fetchData(from: "bad-url")
print("유효하지 않은 URL 시도: \(invalidURLData ?? "nil")") // 출력: 유효하지 않은 URL 시도: nil

4.2. try!: 오류가 발생하지 않을 것이라고 확신

try!는 오류를 발생시킬 수 있는 표현식을 호출하지만, 오류가 절대로 발생하지 않을 것이라고 확신할 때 사용합니다. 만약 실제로 오류가 발생하면 런타임 오류(Crash)가 발생합니다. 이는 강제 언래핑(!)과 유사하게 매우 위험하므로, 정말 확실한 상황이 아니라면 사용을 자제해야 합니다.
//Swift 예제
let guaranteedData = try! fetchData(from: "https://always-works.com/data")
print("확실한 데이터: \(guaranteedData)") // 출력: 확실한 데이터: Fetched data from https://always-works.com/data

// let crashingData = try! fetchData(from: "invalid-url") // 런타임 오류 발생!

5. defer 문: 코드 블록 종료 전 실행되는 코드

defer 문은 현재 코드 블록을 벗어나기 직전에 특정 코드를 실행하도록 합니다. 이는 함수나 메서드가 성공적으로 종료되든, 오류로 인해 종료되든 상관없이 항상 실행되어야 하는 정리 작업(예: 파일 핸들 닫기, 리소스 해제)에 매우 유용합니다.
//Swift 예제
func processFile(filename: String) throws {
    print("파일 \(filename) 열기 시도...")
    let fileDescriptor = 100 // 가상의 파일 핸들

    defer { // 함수가 종료되기 직전에 항상 실행
        print("파일 핸들 \(fileDescriptor) 닫기.")
    }

    if filename.contains("error") {
        throw DataFetchingError.networkError("파일 읽기 중 오류 발생")
    }

    print("파일 \(filename) 성공적으로 처리 완료.")
    // return // 함수 종료 시 defer 블록 실행
}

do {
    try processFile(filename: "my_document.txt")
} catch {
    print("오류 발생: \(error.localizedDescription)")
}
// 출력:
// 파일 my_document.txt 열기 시도...
// 파일 my_document.txt 성공적으로 처리 완료.
// 파일 핸들 100 닫기.

print("\n--- 오류 발생 시 defer ---")
do {
    try processFile(filename: "error_file.txt")
} catch {
    print("오류 발생: \(error.localizedDescription)")
}
// 출력:
// 파일 error_file.txt 열기 시도...
// 파일 핸들 100 닫기.
// 오류 발생: The operation couldn’t be completed. (DataFetchingError error 1.)
defer 블록은 코드의 어떤 경로를 통해서든 해당 스코프를 벗어날 때 실행되므로, 리소스 관리에 매우 효과적입니다.

정리하며

오늘은 Swift에서 앱의 안정성과 사용자 경험을 높이는 오류 처리(Error Handling) 방법에 대해 자세히 알아보았습니다.

  • 오류 정의: Error 프로토콜을 준수하는 열거형으로 발생 가능한 오류 상황을 명확히 정의합니다.
  • 오류 발생(throws, throw): 함수나 메서드가 오류를 던질 수 있음을 명시하고, 실제 오류 발생 시 throw 합니다.
  • 오류 처리(do-catch): try로 오류를 던지는 코드를 실행하고, catch 블록에서 특정 오류에 대한 대응 로직을 구현합니다.
  • 간편한 오류 처리(try?, try!): 옵셔널이나 강제 언래핑을 통해 오류를 처리하는 간편한 방법이지만, try!는 신중하게 사용해야 합니다.
  • 정리 작업(defer): 코드 블록을 벗어나기 직전에 항상 실행되어야 하는 정리 작업을 정의할 때 사용합니다.
오류 처리는 단순히 앱 크래시를 막는 것을 넘어, 프로그램의 논리적 흐름을 견고하게 만들고 사용자에게 더 나은 경험을 제공하는 데 필수적인 부분입니다. 오늘 배운 내용을 바탕으로 여러분의 앱에 발생할 수 있는 잠재적인 오류 상황들을 미리 고민하고, 적절한 오류 처리 로직을 구현해 보세요!

다음 시간에는 Swift의 강력한 비동기 프로그래밍 방식인 동시성(Concurrency) 개념에 대해 알아보겠습니다.

궁금한 점이 있다면 언제든지 댓글로 남겨주세요. 감사합니다.

0 comments:

댓글 쓰기