Swift 에러 처리 | do-catch, throw, Result
이 글의 핵심
Swift 에러 처리에 대해 정리한 개발 블로그 글입니다. enum NetworkError: Error { case badURL case timeout case noConnection case serverError(Int) }
들어가며
throws·try·do–catch로 실패 가능한 연산을 시그니처에 드러냅니다. Result 타입으로 값과 오류를 한꺼번에 다루는 패턴도 많습니다.
1. 에러 정의
Error 프로토콜
enum NetworkError: Error {
case badURL
case timeout
case noConnection
case serverError(Int)
}
enum ValidationError: Error {
case emptyField(String)
case invalidFormat(String)
case outOfRange(String, min: Int, max: Int)
}
LocalizedError
enum FileError: LocalizedError {
case notFound
case permissionDenied
var errorDescription: String? {
switch self {
case .notFound:
return "파일을 찾을 수 없습니다"
case .permissionDenied:
return "권한이 없습니다"
}
}
}
2. throw와 try
기본 에러 처리
func fetchData(from url: String) throws -> String {
guard !url.isEmpty else {
throw NetworkError.badURL
}
// 네트워크 요청 시뮬레이션
if url.contains("timeout") {
throw NetworkError.timeout
}
return "데이터"
}
// do-catch로 처리
do {
let data = try fetchData(from: "https://example.com")
print("성공: \(data)")
} catch NetworkError.badURL {
print("잘못된 URL")
} catch NetworkError.timeout {
print("타임아웃")
} catch {
print("기타 에러: \(error)")
}
try? - Optional 변환
// 에러 무시, nil 반환
let data = try? fetchData(from: "https://example.com")
if let data = data {
print("데이터: \(data)")
} else {
print("실패")
}
// 기본값 제공
let result = try? fetchData(from: "bad") ?? "기본값"
try! - 강제 언래핑
// 에러 발생 시 크래시 (확실할 때만 사용)
let data = try! fetchData(from: "https://example.com")
3. Result<Success, Failure>
Result 사용
func fetchData(from url: String) -> Result<String, NetworkError> {
guard !url.isEmpty else {
return .failure(.badURL)
}
if url.contains("timeout") {
return .failure(.timeout)
}
return .success("데이터")
}
// 패턴 매칭
switch fetchData(from: "https://example.com") {
case .success(let data):
print("성공: \(data)")
case .failure(let error):
print("실패: \(error)")
}
// map, flatMap
let result = fetchData(from: "https://example.com")
.map { $0.uppercased() }
.mapError { _ in NetworkError.noConnection }
Result와 throws 변환
// Result → throws
func getData() throws -> String {
let result = fetchData(from: "https://example.com")
return try result.get()
}
// throws → Result
func getDataResult() -> Result<String, Error> {
Result { try fetchData(from: "https://example.com") }
}
4. 실전 예제
예제: 사용자 검증
enum ValidationError: LocalizedError {
case emptyName
case invalidAge
case invalidEmail
var errorDescription: String? {
switch self {
case .emptyName:
return "이름은 필수입니다"
case .invalidAge:
return "나이는 0~150 사이여야 합니다"
case .invalidEmail:
return "유효하지 않은 이메일"
}
}
}
struct User {
let name: String
let age: Int
let email: String
static func validate(name: String, age: Int, email: String) throws -> User {
guard !name.isEmpty else {
throw ValidationError.emptyName
}
guard age >= 0 && age <= 150 else {
throw ValidationError.invalidAge
}
guard email.contains("@") else {
throw ValidationError.invalidEmail
}
return User(name: name, age: age, email: email)
}
}
// 사용
do {
let user = try User.validate(name: "", age: 25, email: "[email protected]")
print("사용자 생성: \(user.name)")
} catch {
print("검증 실패: \(error.localizedDescription)")
}
실전 심화 보강
실전 예제: Result로 네트워크 레이어 경계 정리
throws만 쓰는 대신 Result를 반환하면 비동기 체이닝과 테스트가 단순해질 때가 많습니다.
import Foundation
enum NetworkFailure: Error {
case invalidURL
case status(Int)
case decoding
}
struct HTTP {
static func getString(_ urlString: String) async -> Result<String, NetworkFailure> {
guard let url = URL(string: urlString) else {
return .failure(.invalidURL)
}
do {
let (data, resp) = try await URLSession.shared.data(from: url)
if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
return .failure(.status(http.statusCode))
}
guard let text = String(data: data, encoding: .utf8) else {
return .failure(.decoding)
}
return .success(text)
} catch {
return .failure(.decoding)
}
}
}
async throws와의 선택은 호출부 스타일과 팀 컨벤션에 따릅니다.
자주 하는 실수
try!로 네트워크 응답을 강제 언래핑해 크래시를 내는 경우.LocalizedError없이 사용자에게 내부 enum 케이스 이름이 그대로 노출되는 경우.Result의map/flatMap실패 타입이 달라져 타입 추론이 꼬이는 경우.
주의사항
@frozen이 아닌 라이브러리 에러 타입은 미래 케이스 추가에 대한switch경고가 생길 수 있습니다(@unknown default).
실무에서는 이렇게
- 도메인 에러와 전송 계층 에러를 분리하고, UI에는 매핑된 메시지만 노출합니다.
- 로깅은
NSErroruserInfo 또는OSLog에 상관 ID를 남깁니다.
비교 및 대안
| API | 장점 |
|---|---|
throws | 언어 관용, 짧은 호출부 |
Result | 값으로 합성, 동기 코드와 유사 |
async + throws | Swift 동시성과 자연스럽게 결합 |
추가 리소스
정리
핵심 요약
- Error 프로토콜: 에러 타입 정의
- throw: 에러 발생
- do-catch: 에러 처리
- try/try?/try!: 에러 전파 방식
- Result: 성공/실패를 값으로 표현
다음 단계
- Swift 비동기
- Swift SwiftUI
- Swift Combine
관련 글
- C++ expected |
- JavaScript 에러 처리 | try-catch, Error 객체, 커스텀 에러
- Rust 에러 처리 | Result, Option, ? 연산자
- C++ 예외 처리 | try/catch/throw
- C++ 네트워크 에러 완벽 가이드 | errno·타임아웃·재시도·서킷브레이커 [#28-3]