오류 처리 (Error Handling)
이번 강의에서는 Swift에서 오류 처리 방법에 대해 상세히 배워보겠습니다. Swift는 안정적인 앱 개발을 위해 강력한 오류 처리 메커니즘을 제공합니다. try, catch, throws, do-catch 구문을 사용하여, 코드 실행 중 발생할 수 있는 오류를 예측하고 적절히 대처할 수 있습니다. 이 강의를 통해 여러분은 Swift의 오류 처리 기본 개념부터 실무에서 유용하게 쓰이는 다양한 기법까지 폭넓게 이해할 수 있을 것입니다.
1. 오류 처리 기본 구문
Swift의 오류 처리는 do-catch 구문을 통해 이루어집니다. do 블록 안에 오류를 발생시킬 수 있는 코드를 작성하고, 오류가 발생하면 catch 블록에서 이를 잡아 처리합니다. catch는 여러 개를 둘 수 있어서 발생한 오류의 종류별로 맞춤 대응이 가능합니다.
enum FileError: Error {
case fileNotFound
case insufficientPermissions
}
func openFile(filename: String) throws {
if filename == "missing.txt" {
throw FileError.fileNotFound
} else if filename == "restricted.txt" {
throw FileError.insufficientPermissions
}
print("File opened successfully")
}
do {
try openFile(filename: "missing.txt")
} catch FileError.fileNotFound {
print("File not found error")
} catch FileError.insufficientPermissions {
print("Insufficient permissions error")
} catch {
print("Unknown error")
}
위 예시에서 openFile 함수는 throws를 선언해 오류를 던질 수 있음을 알리고, 특정 조건에 따라 throw 키워드로 FileError 타입의 오류를 발생시킵니다. 호출부에서는 do-catch 구문으로 오류를 잡아내어, 각 상황에 맞게 메시지를 출력합니다.
2. 오류 정의 및 throw 키워드
Swift에서 오류는 Error 프로토콜을 채택한 타입으로 정의합니다. 보통 enum 타입으로 만들어, 가능한 오류 상황을 명확히 분류하는 데 유리합니다. 오류를 발생시킬 때는 throw 키워드를 사용하며, 이때 함수는 반드시 throws 키워드를 명시해야 합니다.
enum CalculationError: Error {
case divisionByZero
}
func divide(_ numerator: Int, by denominator: Int) throws -> Int {
if denominator == 0 {
throw CalculationError.divisionByZero
}
return numerator / denominator
}
do {
let result = try divide(10, by: 0)
print("Result: \(result)")
} catch CalculationError.divisionByZero {
print("Cannot divide by zero")
} catch {
print("Unknown error")
}
이 예시에서는 divide 함수가 0으로 나누기를 시도할 때 divisionByZero 오류를 던집니다. 호출부에서는 do-catch 구문으로 오류를 안전하게 처리합니다.
3. try, catch, throws 사용법
오류가 발생할 가능성이 있는 함수를 호출할 때는 try 키워드를 사용합니다. try는 오류가 발생하면 호출 위치로 오류를 던지고, do-catch 구문으로 처리하도록 돕습니다. throws 키워드는 함수 선언부에서 해당 함수가 오류를 던질 수 있음을 표시합니다.
func processFile(filename: String) throws {
if filename.isEmpty {
throw FileError.fileNotFound
}
print("File processed successfully")
}
do {
try processFile(filename: "document.txt")
} catch {
print("Error processing file")
}
위 코드는 빈 문자열이 들어오면 FileError.fileNotFound 오류를 던지고, 이를 do-catch에서 처리하는 모습입니다. 이런 방식을 통해 프로그램이 예외 상황에서도 안전하게 동작하도록 할 수 있습니다.
4. 간단한 오류 처리: try?, try!
Swift는 간단히 오류를 무시하거나 강제로 처리하는 방법도 제공합니다. try? 는 오류가 발생하면 nil을 반환해 실패를 표현하며, try! 는 오류 발생 시 프로그램을 즉시 종료시킵니다. 두 방식 모두 상황에 따라 적절히 선택해야 합니다.
// try? 사용 예시
let result = try? divide(10, by: 2)
print(result ?? "Error") // 5
// try! 사용 예시
let result2 = try! divide(10, by: 2)
print(result2) // 5
try?를 쓰면 오류 발생 시 nil이 되므로, 선택적 바인딩 등으로 쉽게 오류를 처리할 수 있습니다. 반면 try! 는 오류가 절대 발생하지 않을 것을 확신할 때만 사용하며, 오류 발생 시 앱이 크래시가 발생하므로 주의해야 합니다.
5. 여러 catch 절과 오류 바인딩
catch 절은 여러 개 작성 가능하며, 발생한 오류에 따라 각각 다르게 처리할 수 있습니다. 또한, catch 절에서 오류 객체를 변수로 바인딩하여 구체적인 정보를 얻을 수도 있습니다.
enum NetworkError: Error {
case badURL(String)
case timeout
case unknown
}
func fetchResource(url: String) throws {
if !url.hasPrefix("https://") {
throw NetworkError.badURL(url)
} else if url == "https://timeout.com" {
throw NetworkError.timeout
}
print("Resource fetched successfully")
}
do {
try fetchResource(url: "http://invalid-url.com")
} catch NetworkError.badURL(let url) {
print("잘못된 URL: \(url)")
} catch NetworkError.timeout {
print("네트워크 요청 시간 초과")
} catch {
print("알 수 없는 오류 발생")
}
위 예시에서 catch NetworkError.badURL(let url) 구문은 오류 객체에서 URL 정보를 받아 구체적으로 처리합니다. 이런 방식으로 오류의 상세 내용을 활용할 수 있습니다.
6. 오류 전파와 throws 함수 체인
오류를 처리하지 않고 호출자에게 넘기는 것도 가능합니다. 함수가 오류를 처리하지 않으면 throws를 유지한 채 호출자에게 전달하고, 호출자가 do-catch로 처리하도록 합니다. 이를 통해 복잡한 오류 처리 로직을 계층적으로 구성할 수 있습니다.
func readConfig(filename: String) throws -> String {
// 파일 읽기 중 오류 발생 가능
if filename == "config.txt" {
return "Configuration Data"
} else {
throw FileError.fileNotFound
}
}
func loadApplication() throws {
let config = try readConfig(filename: "missing.txt")
print(config)
}
do {
try loadApplication()
} catch {
print("애플리케이션 실행 중 오류 발생: \(error)")
}
이 코드에서 readConfig가 오류를 던지면 loadApplication도 throws를 붙여 오류를 그대로 전달합니다. 최종적으로 호출한 do-catch 블록에서 모든 오류를 받아 처리합니다.
7. 사용자 정의 오류 타입 활용하기
오류 처리를 더 체계적으로 하려면, enum뿐 아니라 struct나 class를 활용한 사용자 정의 오류 타입도 만들 수 있습니다. 특히 오류에 부가적인 정보를 담거나, 복잡한 상태를 관리할 때 유용합니다.
struct ValidationError: Error {
let message: String
}
func validateUsername(_ username: String) throws {
if username.count < 3 {
throw ValidationError(message: "아이디는 최소 3자 이상이어야 합니다.")
}
print("아이디가 유효합니다.")
}
do {
try validateUsername("ab")
} catch let error as ValidationError {
print("유효성 검사 실패: \(error.message)")
} catch {
print("알 수 없는 오류")
}
이처럼 사용자 정의 오류 타입을 만들어 오류 메시지 등 상세 데이터를 함께 전달할 수 있습니다. catch에서 타입 캐스팅(as)을 통해 구체적 오류를 분기 처리할 수 있습니다.
8. 옵셔널과 오류 처리 비교
Swift에서는 오류 처리 외에도 Optional 타입을 사용하여 실패를 표현합니다. 하지만 오류 처리 구문은 실패 원인을 명확히 표현하고 복잡한 흐름 제어가 가능하다는 점에서 차별화됩니다.
- 옵셔널은 값이 있거나 없음을 간단히 표현합니다. 실패 원인에 대한 정보는 제공하지 않습니다.
- 오류 처리는 구체적인 실패 원인과 처리를 위한 분기 로직을 제공합니다.
따라서 단순한 실패 여부 판단엔 옵셔널이 적합하지만, 원인 분석 및 복잡한 흐름 제어가 필요할 땐 오류 처리 구문을 권장합니다.
9. 비동기 오류 처리 (Swift 5.5 이후)
Swift 5.5부터는 async/await 패턴에 오류 처리를 결합할 수 있습니다. async throws 함수는 비동기 작업 중 발생하는 오류를 호출자가 try await로 처리하도록 돕습니다.
enum NetworkError: Error {
case serverError
}
func fetchData() async throws -> String {
let success = false
if !success {
throw NetworkError.serverError
}
return "데이터"
}
Task {
do {
let data = try await fetchData()
print("데이터 수신: \(data)")
} catch {
print("네트워크 오류 발생: \(error)")
}
}
비동기 오류 처리 덕분에 네트워크 호출이나 파일 I/O 같은 비동기 작업도 간결하고 안전하게 관리할 수 있습니다.
10. 정리 및 팁
- 오류 타입을 명확히 설계하여 각 오류 상황을 구분하세요.
do-catch구문을 활용해 오류 발생 시 사용자에게 적절한 메시지를 전달하세요.try? 는간단한 오류 무시용,try! 는오류 발생이 절대 없다고 확신할 때만 사용하세요.- 복잡한 함수 호출에서는 오류를 단계별로 전파하고 마지막에 처리하는 방식을 권장합니다.
- Swift 5.5 이상의 프로젝트에서는
async throws패턴으로 비동기 오류 처리를 익히세요.
Swift의 오류 처리는 안전하고 예측 가능한 프로그램을 만드는 데 핵심적인 요소입니다. 본 강의를 통해 기본 개념과 실전 활용법을 숙지하여, 여러분의 Swift 코드에 견고한 오류 처리 로직을 적용해 보세요!
'Programming' 카테고리의 다른 글
| Swift 메모리 관리 (36) | 2025.08.05 |
|---|---|
| Swift 고급 타입 (56) | 2025.08.04 |
| Swift 확장과 접근 제어 (47) | 2025.08.02 |
| Swift 프로토콜 (37) | 2025.08.01 |
| Swift 클로저(Closure) (55) | 2025.07.31 |