defer (async throwing contexts)
안녕하세요. 그린입니다 🍏
오늘 포스팅에서는 async 및 throws 컨텍스트에서 defer를 사용하는 방법에 대해 한번 작성해볼까 합니다 🙋🏻
최근 Swift by Sundell 사이트에서 해당 관련한 포스트를 접하게 되었고 꽤 흥미로운 내용이라 한번 보고 정리해보려고 해요!
해당 원본 아티클을 보시려는분은 요걸 보시면 됩니다 😃
Using Swift’s defer keyword within async and throwing contexts | Swift by Sundell
How Swift’s defer keyword can be incredibly useful when working with code scopes that have multiple exit points, such as throwing or async functions.
www.swiftbysundell.com
어렵지 않게 나름대로 해석을 가미해볼까 합니다.

defer (async throwing contexts)
사실 defer는 많이들 아실거에요.
현재 스코프를 벗어나가지고 실행되어야 하는 코드를 정의할 수 있게 해주죠.
앱 개발에서는 여러 exit 포인트들이 존재하고 특히나 throws나 async와 같이 다수 분기를 가지는 함수에서는 코드 안정성과 가독성 측면에서 defer를 더 잘 활용할 수 있습니다.
그래서 이번 포스팅은 defer가 비동기 함수와 에러 처리가 혼합된 상황속에서 어떻게 유용하게 활용되는지를 살펴보는 겁니다 ☺️
in throws
코드를 하나 볼까요?
actor SearchService {
private let database: Database
...
func loadItems(maching searchString: String) throws -> [Item] {
let connection = database.connect()
do {
let items: [Item] = try connection.runQuery(.entries(
matching: searchString
))
connection.close()
return items
} catch {
connection.close()
throw error
}
}
}
위 코드는 쿼리 성공/실패 여부에 따라서 connection.close()를 명시적으로 호출해야 합니다.
동작은 하지만 중복 코드가 생기고 추후 유지보수에 어려움이 있을 수 있어요.
이를 해결하는 방안 두가지를 봐볼께요.
1️⃣ Result 사용
actor SearchService {
private let database: Database
...
func loadItems(maching searchString: String) throws -> [Item] {
let connection = database.connect()
let result = Result<[Item], Error> {
try connection.runQuery(.entries(matching: searchString))
}
connection.close()
return try result.get()
}
}
많이들 사용하고 계실 수 있는 방법인데요.
Result 타입을 활용해 분기를 하나로 통합할 수 있어요.
해당 방식을 통해 중복도 제거하면서 정상적인 흐름과 에러 흐름 모두를 커버할 수 있죠.
2️⃣ defer로 정리 로직 자동화
1번 방식도 좋지만 지금 소개하는 defer를 이용하면 더 깔끔하게 해결할 수 있습니다.
actor SearchService {
private let database: Database
...
func loadItems(maching searchString: String) throws -> [Item] {
let connection = database.connect()
defer { connection.close() }
return try connection.runQuery(.entries(matching: searchString))
}
}
이렇게 connect()와 close() 호출 자체를 붙여두는 것만으로도 스코프의 어떤 경로로 빠져나가든 정리 작업을 수행해주죠.
코드 가독성과 안정성 측면에서도 효율적입니다.
다만 defer 사용 시에도 주의할 부분이 있어요.
defer 실행 순서 주의 포인트
defer는 선언된 순서가 아니라 스코프 종료 시점에 실행됩니다.
그렇기에 위 코드에서의 실행 순서는 다음과 같아요.
1️⃣ connection
2️⃣ query
3️⃣ close (defer)
코드 상에서는 1 -> 3 -> 2 처럼 보이겠지만 실제 순서는 1 -> 2 -> 3입니다.
이건 defer를 아신다면 기본적으로 아시겠지만 한번 체크해보면 좋습니다.
in async
Swift Concurrency가 도입되면서 코드 플로우를 보기 편해졌지만 비동기 상태 관리에서는 여전히 정리 작업이 필요하긴 합니다.
코드를 하나 볼까요?
actor ItemListService {
private let networking: NetworkingService
private var isLoading = false
...
func loadItems(after lastItem: Item) async throws -> [Item] {
guard !isLoading else { throw Error.alreadyLoading }
isLoading = true
do {
let request = requestForLoadingItems(after: lastItem)
let response = try await networking.performRequest(request)
let items: [Item] = try response.decoded()
isLoading = false
return items
} catch {
isLoading = false
throw error
}
}
}
이전과 마찬가지로 isLoading = false가 중복되었는데 역시나 defer로 해결할 수 있죠.
actor ItemListService {
private let networking: NetworkingService
private var isLoading = false
...
func loadItems(after lastItem: Item) async throws -> [Item] {
guard !isLoading else { throw LoadingError.alreadyLoading }
isLoading = true
defer { isLoading = false }
let request = requestForLoadingItems(after: lastItem)
let response = try await networking.performRequest(request)
return try response.decoded()
}
}
이렇게 되면 isLoading = false는 항상 호출되고 성공/실패 여부에 관계없이 안전하게 상태를 복구해주죠.
좀 더 나아가서 이런 사용도 해볼 수 있어요 🙋🏻
actor ItemListService {
private let networking: NetworkingService
private var activeTasksForLastItemID = [Item.ID: Task<[Item], Error>]()
...
func loadItems(after lastItem: Item) async throws -> [Item] {
if let existingTask = activeTasksForLastItemID[lastItem.id] {
return try await existingTask.value
}
let task = Task {
defer { activeTasksForLastItemID[lastItem.id] = nil }
let request = requestForLoadingItems(after: lastItem)
let response = try await networking.performRequest(request)
return try response.decoded() as [Item]
}
activeTasksForLastItemID[lastItem.id] = task
return try await task.value
}
}
코드는 중복 로딩 요청을 방지하기 위해 ID 별 Task를 캐싱하고 종료 시점에 해당 Task를 자동으로 제거하는 코드입니다.
Swift의 actor는 동기 코드의 레이스 컨디션은 방지하지만, await 이후에는 다른 호출도 수용할 수 있어요.
따라서 위처럼 Task와 defer를 조합하면 중복 호출을 쿨하게 막을 수 있습니다.
Conclusion
사실 defer를 이렇게 비동기 측면에서 활용해보진 않았던것 같은데 깔끔해질 수 있네요!
defer는 리소스 해제나 상태 복원, 중복 제거 등의 작업에서 아주 좋은 요소에요.
여러 흐름이 발생할 수 있는 컨텍스트에서의 안정성과 일관성을 보장해줄 수 있는 강점이 있습니다.
다들 defer 활용을 해보는건 어떨까요?
References
Using Swift’s defer keyword within async and throwing contexts | Swift by Sundell
How Swift’s defer keyword can be incredibly useful when working with code scopes that have multiple exit points, such as throwing or async functions.
www.swiftbysundell.com