이번 글에서는 Swift에서 비동기 작업을 수행할 때 사용하는 async, await, throws의 개념과 함께, 오류를 세분화하여 처리하기 위한 중첩된 do-catch 구조에 대해 알아보겠습니다.
이 글에서는 fetchNowPlayingMovies라는 실제 예제 함수를 중심으로 설명합니다.
func fetchNowPlayingMovies(page: Int = 1) async throws -> TMDBData {
// Base URL
guard let url = URL(string: "https://api.themoviedb.org/3/movie/now_playing") else {
throw APIError.invalidURL
}
// URL Components
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
components.queryItems = [
URLQueryItem(name: "language", value: "ko-KR"),
URLQueryItem(name: "page", value: "\(page)")
]
// URLRequest
guard let finalURL = components.url else {
throw APIError.invalidURL
}
var request = URLRequest(url: finalURL)
request.httpMethod = "GET"
request.timeoutInterval = 10
request.allHTTPHeaderFields = [
"accept": "application/json",
"Authorization": "Bearer YOUR_API_KEY"
]
// API Call
do {
let (data, response) = try await URLSession.shared.data(for: request)
// HTTP 응답 상태 확인
if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
throw APIError.requestFailed("HTTP Status Code: \(httpResponse.statusCode)")
}
// JSON 디코딩
do {
var decodedData = try JSONDecoder().decode(TMDBData.self, from: data)
decodedData.type = .noewPlayingMovie
return decodedData
} catch {
throw APIError.decodingError("Decoding Failed: \(error.localizedDescription)")
}
} catch {
throw APIError.requestFailed("Request Failed: \(error.localizedDescription)")
}
}
비동기 함수의 핵심 키워드
- async
- 함수가 비동기 작업을 포함하고 있음을 표시합니다.
- 이 함수는 호출 시 await 키워드를 사용해야 합니다.
- await
- 비동기 작업의 결과를 기다리기 위해 사용됩니다.
- 실행 흐름을 일시 중단하지만, 다른 작업은 계속 진행됩니다.
- throws
- 함수 내에서 오류를 던질 수 있음을 나타냅니다.
- 호출자는 오류를 처리하기 위해 try 또는 do-catch를 사용해야 합니다.
사용 방법
fetchNowPlayingMovies 함수를 ViewController에서 호출하는 코드는 아래와 같습니다:
private func fetchMovies() {
Task {
do {
let movies = try await fetchNowPlayingMovies(page: 1)
DispatchQueue.main.async {
self.displayMovies(movies)
}
} catch {
DispatchQueue.main.async {
self.showError(error)
}
}
}
}
사용 시 중요한 점
- 비동기 호출
- Task 또는 다른 async 함수 내에서 await를 사용해 호출해야 합니다.
- 에러 처리
- try와 함께 사용하거나, do-catch 블록을 통해 에러를 명확히 처리해야 합니다.
- UI 업데이트
- 네트워크 작업은 백그라운드에서 실행되므로, UI 변경은 반드시 DispatchQueue.main.async를 사용해야 안전합니다.
중첩된 do-catch 구조의 이유
왜 중첩 구조를 사용할까?
함수 fetchNowPlayingMovies에서 do-catch가 중첩된 이유는, 서로 다른 작업 단계에서 발생할 수 있는 오류를 구분하여 처리하기 위해서입니다.
do {
let (data, response) = try await URLSession.shared.data(for: request)
// Check HTTP Response Status
if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
throw APIError.requestFailed("HTTP Status Code: \(httpResponse.statusCode)")
}
// Decode JSON Response
do {
var decodedData = try JSONDecoder().decode(TMDBData.self, from: data)
decodedData.type = .noewPlayingMovie
return decodedData
} catch {
throw APIError.decodingError("Decoding Failed: \(error.localizedDescription)")
}
} catch {
throw APIError.requestFailed("Request Failed: \(error.localizedDescription)")
}
첫 번째 do-catch: 네트워크 요청 오류
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
throw APIError.requestFailed("HTTP Status Code: \(httpResponse.statusCode)")
}
- 역할: 네트워크 요청이 실패하거나 HTTP 응답 상태 코드가 200~299 범위에 들지 않을 경우, 오류를 던집니다.
- 오류 처리: APIError.requestFailed를 통해 네트워크 관련 오류를 상위로 전달합니다.
두 번째 do-catch: JSON 디코딩 오류
do {
var decodedData = try JSONDecoder().decode(TMDBData.self, from: data)
decodedData.type = .noewPlayingMovie
return decodedData
} catch {
throw APIError.decodingError("Decoding Failed: \(error.localizedDescription)")
}
- 역할: JSON 디코딩 중 데이터 형식이 맞지 않을 경우 오류를 던집니다.
- 오류 처리: APIError.decodingError를 통해 디코딩 관련 오류를 상위로 전달합니다.
왜 하나의 do-catch로 처리하지 않을까?
모든 오류를 단일 do-catch로 처리하면, 어떤 단계에서 오류가 발생했는지 명확히 알기 어렵습니다.
단일 do-catch의 문제점:
do {
let (data, response) = try await URLSession.shared.data(for: request)
var decodedData = try JSONDecoder().decode(TMDBData.self, from: data)
decodedData.type = .noewPlayingMovie
return decodedData
} catch {
throw APIError.requestFailed("An error occurred: \(error.localizedDescription)")
}
- 모든 오류가 동일한 블록에서 처리됩니다.
- 오류 원인을 구분하기 어렵고, 디버깅이 복잡해질 수 있습니다.
throws 위치?
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
throw APIError.requestFailed("HTTP Status Code: \(httpResponse.statusCode)")
}
- 이 부분에서 throw는 HTTP 응답 상태 코드가 200~299 범위에 들지 않으면 APIError.requestFailed라는 에러를 발생시키는 역할을 합니다.
- 왜 if문 안에서 throw를 쓰는지: HTTP 요청이 실패했을 때 그 즉시 에러를 던지기 위해 if문 내부에서 throw를 사용합니다. 만약 이 조건이 충족되면 에러를 던지고, 호출한 곳에서 그 에러를 처리하도록 합니다.
- 이유: throw가 반드시 catch 블록 내에서만 사용되는 것은 아니며, 함수 내에서 에러를 발생시키고 싶을 때 자유롭게 사용할 수 있습니다.
throw가 catch 블록 내에서만 사용되는 것이 일반적인 생각일 수 있지만, 사실 throw는 에러를 발생시키는 기능으로, 어떤 블록에서든 사용할 수 있습니다. 다만, throws가 붙은 함수 내에서만 사용해야 합니다.
throw 사용 위치에 대한 설명
throw는 에러를 발생시키기 위한 키워드로, 함수 내의 어떤 곳에서도 사용될 수 있습니다. throw가 쓰인 위치는 에러를 던지려는 의도가 있는 곳입니다.
- 에러를 발생시키는 위치:
- throw는 코드 실행 중 문제가 발생했을 때 해당 에러를 함수의 호출자에게 전달하려는 목적입니다. 예를 들어, URLSession의 네트워크 요청에서 HTTP 응답 상태 코드가 200~299 범위에 포함되지 않으면 에러를 던지려고 합니다. 이 에러는 호출한 곳에서 처리해야 합니다.
- if문 내에서 throw를 사용하는 이유는 특정 조건이 충족되었을 때(여기선 HTTP 상태 코드가 200~299가 아닐 때) 즉시 에러를 발생시키기 위함입니다. 이 위치에서 에러를 던지면 호출한 곳에서 처리할 수 있습니다.
throws와 throw의 관계
- throws는 함수 선언 시 사용:
- throws는 함수가 에러를 던질 수 있음을 선언하는 역할을 합니다. 즉, throws가 붙은 함수는 throw를 사용하여 에러를 발생시킬 수 있는 함수입니다.
- 예를 들어, func fetchNowPlayingMovies(page: Int = 1) async throws -> TMDBData라고 선언된 함수에서는 throw를 사용하여 에러를 발생시킬 수 있습니다. 그리고 이 함수는 호출 시 try로 감싸야 하고, 에러를 처리하기 위한 do-catch 블록이 필요합니다.
- throw는 에러를 발생시킬 때 사용:
- throw는 실제로 에러를 던지는 역할을 합니다. 이는 함수 내의 특정 조건에서 발생할 수 있는 문제를 나타내는 데 사용됩니다. 예를 들어, 네트워크 요청에서 HTTP 상태 코드가 유효하지 않을 때 throw를 사용하여 해당 오류를 호출자에게 전달합니다.
throw와 catch의 관계
- catch는 에러가 발생했을 때 그 에러를 처리하는 역할을 합니다.
- throw는 에러를 발생시키기 위한 키워드이므로, throw를 사용하여 에러를 발생시키면 그 에러는 해당 함수에서 호출한 곳으로 전달됩니다. 이 에러를 처리하기 위해 호출자는 do-catch 블록을 사용하여 그 에러를 잡고 처리해야 합니다.
다시 말해, throw는 반드시 throws 함수 안에서만 사용해야 한다는 규칙이 있습니다. 함수 선언부에 throws가 있으면 그 함수 내에서는 자유롭게 throw를 사용하여 에러를 발생시킬 수 있습니다.
'Swift' 카테고리의 다른 글
setCustomSpacing(_:after:)의 역할 (0) | 2025.01.05 |
---|---|
UICollectionViewCompositionalLayout 관련 데이터 소스 관리 (0) | 2025.01.03 |
async/await란? (0) | 2024.12.18 |
async과 await 개요 (0) | 2024.12.10 |
컨텍스트 (context)는 무엇인가요? (1) | 2024.12.10 |