defer는 현재 스코프를 벗어날 때 코드 블록의 실행을 예약하는 Swift의 제어 흐름 구조입니다. defer 블록은 스코프가 어떻게 종료되는지에 관계없이 실행됩니다 — return, break, throw, fatalError 또는 정상 완료. Swift Language Guide(2025)에 따르면, 하나의 스코프에 여러 defer가 있을 경우 선언의 역순으로 실행됩니다 — 마지막에 선언된 defer가 먼저 실행됩니다(LIFO). 이로 인해 defer는 리소스의 보장된 정리에 필수적입니다: 파일 디스크립터 닫기, 잠금 해제, 임시 포인터 해제를 조기 종료 시 정리를 놓칠 위험 없이 수행할 수 있습니다.
핵심 포인트
defer는 Swift 2.0(2015)에서 도입된 Swift의 제어 흐름 구조로, 현재 스코프가 종료될 때까지 자체 블록의 실행을 지연시킵니다. 주요 특징: defer는 스코프가 어떻게 종료되는지에 관계없이 본문이 실행됨을 보장합니다 — 성공(return), 오류(throw), 조기 종료(break, continue) 또는 치명적 종료(fatalError, precondition).
구문적으로 defer는 defer { /* 코드 */ }처럼 보이며 스코프 내 어디에나 배치할 수 있습니다. Swift 컴파일러는 defer 선언과 스코프 종료 사이에 예외나 return이 발생하더라도 defer 내부의 코드가 실행됨을 보장합니다. 이는 함수 끝에 배치된 일반 코드가 조기 종료 시 건너뛸 수 있는 것과 근본적으로 다릅니다.
크리스 래트너(Swift 창시자, 2015)의 기사에 따르면, defer는 다른 언어의 유사한 구조 — Go의 defer, Java/Python의 finally, C++의 scope guard — 에서 영감을 받았지만 중요한 차이점이 있습니다: Swift에서 defer는 스코프 끝에서 실행되며 try-catch 블록 바로 다음이 아닙니다. 이는 여러 종료 지점이 있는 함수에서 정리를 위한 더 예측 가능한 동작을 제공합니다.
대칭적 리소스 관리에 defer를 사용하세요: 파일 열기 → defer { close }, 잠금 획득 → defer { unlock }. 이 패턴은 어떤 상황에서도 리소스 해제가 절대 누락되지 않음을 보장합니다.
동일한 스코프에서 여러 defer가 선언된 경우, 선언의 역순으로 실행됩니다(LIFO — Last In, First Out). 즉, 마지막에 선언된 defer가 먼저 실행되고 처음에 선언된 defer가 마지막에 실행됩니다:
func exampleDeferOrder() {
defer { print("첫 번째 defer") }
defer { print("두 번째 defer") }
defer { print("세 번째 defer") }
print("함수 본문")
}
// 출력:
// 함수 본문
// 세 번째 defer
// 두 번째 defer
// 첫 번째 defer
LIFO 순서는 중첩된 리소스의 올바른 관리에 중요합니다. 파일 A가 먼저 열리고 파일 B가 다음에 열린 경우, 역순으로 해제해야 합니다: 먼저 B, 그다음 A. defer를 사용하면 이 작업이 자동으로 수행됩니다 — 각 리소스를 연 직후 defer를 선언하면 함수의 종료 지점 수에 관계없이 정리 순서가 올바릅니다.
Swift by Sundell(2024)에 따르면, 이 기능은 defer를 중첩된 잠금 및 트랜잭션에 이상적으로 만듭니다: 잠금 획득 → defer { unlock } → 다음 획득 → defer { unlock }. LIFO는 잠금이 획득 역순으로 해제됨을 보장하여 교착 상태를 방지합니다.
defer의 주요 사용 사례는 리소스의 보장된 정리입니다. 파일 시스템 작업을 고려해 보세요. FileHandle을 통해 파일을 열려면 명시적 닫기가 필요합니다 — defer는 어떤 시나리오에서도 close가 호출됨을 보장합니다:
func readFile(path: String) throws -> String {
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
defer { try? handle.close() }
let data = try handle.readToEnd()
guard let data else { throw FileError.empty() }
return String(data: data, encoding: .utf8) ?? ""
// throw 또는 return에서도 handle.close()가 호출됩니다
}
또 다른 일반적인 시나리오는 로딩 플래그를 사용한 UI 애니메이션입니다. 로드를 시작하기 전에 플래그 isLoading = true를 설정하고, defer는 요청 성공 또는 실패와 관계없이 함수 종료 시 플래그를 false로 다시 설정합니다. 이는 처리되지 않은 오류로 인해 플래그가 true로 유지되어 인터페이스를 영구히 차단하는 것을 방지합니다.
Bitbucket Engineering Blog(2024)에 따르면, defer는 프로파일링에도 사용됩니다: 함수 시작 시 시간을 기록하고 defer에서 차이를 계산하여 출력합니다. 이는 오류 경로를 포함한 모든 실행 경로에 대한 정확한 성능 측정을 제공합니다.
defer는 throws 함수와 효과적으로 작동합니다. 함수가 모든 단계에서 오류를 던질 수 있는 경우, defer는 모든 catch 블록이나 guard 조기 종료에서 코드를 중복하지 않고 정리를 보장합니다:
func processTransaction() throws {
let db = try openDatabase()
defer { closeDatabase(db) }
let user = try fetchUser(from: db)
defer { logAudit(user) }
let result = try performPayment(user)
sendNotification(result)
// closeDatabase(db)와 logAudit(user)가 호출됩니다
// 모든 throw 또는 return에서
}
중요: defer는 제어가 catch 블록 밖으로 이동되기 전에 실행되지만 오류가 발생한 후입니다. defer 내부에서 오류가 던져진 경우, Swift는 defer 내부에서 직접 try 사용을 허용하지 않습니다 — try? 또는 try!가 필요합니다. Apple 문서에 따르면, Swift는 defer에서 오류가 이스케이프되는 것을 허용하지 않습니다. 이는 블록 실행 보장을 위반하기 때문입니다.
리소스 획득 직후에 defer를 배치하세요. 이는 근접성 원칙을 따릅니다: 읽는 사람이 획득과 해제를 함께 볼 수 있어 코드 신뢰성이 향상되고 코드 검토가 간소화됩니다.
defer는 선언된 스코프를 벗어날 때 실행됩니다. defer가 do 블록 내부에 선언된 경우, 해당 블록을 벗어날 때 실행되며 외부 함수가 아닙니다. for 루프 내부에 있는 경우 — 각 반복에서 실행됩니다:
func scopeExample() {
print("start")
do {
defer { print("do-블록 defer") }
print("inside do")
}
// "do-블록 defer"가 여기에 출력
print("after do")
}
// 출력: start, inside do, do-블록 defer, after do
for i in 1...3 {
defer { print("end iteration \(i)") }
print("iteration \(i)")
}
// 출력: iteration 1, end iteration 1, iteration 2, end iteration 2, ...
defer에 의해 캡처된 변수는 defer 선언 시점이 아닌 스코프 종료 시점에 읽힙니다. defer 선언과 스코프 종료 사이에 변수가 변경되면 defer는 최신 값을 참조합니다. 이는 캡처가 생성 시점에 고정되는 클로저와의 중요한 차이점입니다. 주의하세요: defer를 선언한 후의 변수 변경은 실행에 영향을 미칩니다.
첫 번째 실수 — LIFO 이외의 실행 순서를 가정하는 것. 정리 순서가 중요하고 defer가 잘못된 순서로 선언된 경우, 종속성을 위반하여 리소스가 해제될 수 있습니다. 해결책: 각 리소스를 캡처한 직후 defer를 선언하세요. 두 번째 리소스가 열림 → defer { close second }를 첫 번째 리소스가 닫히기 전에.
두 번째 실수 — 정리와 관련 없는 로직에 defer 사용. defer는 보장된 정리를 위한 것이며 메인 흐름 제어를 위한 것이 아닙니다. defer 내부의 코드가 반환 값에 영향을 미치는 경우 거의 항상 실수입니다. defer는 함수의 반환 값을 변경할 수 없습니다(Java finally와 달리 finally의 return은 원래 return을 덮어씁니다).
세 번째 실수 — defer에서 오류 던지기. Swift는 오류가 외부로 전파될 수 있는 경우 defer 내부에서 try를 금지합니다. 던질 수 있는 작업에는 try? 또는 try!를 사용하거나 throws 없이 별도의 함수로 래핑하세요. O’Reilly “Swift in Depth”(2025)에 따르면, 좋은 방법은 정리 함수를 non-throwing으로 만들거나 defer 내부에서 오류를 처리하는 것입니다.
자주 묻는 질문
defer는 현재 스코프가 종료될 때까지 블록의 실행을 지연시키는 Swift 구조입니다. 블록은 항상 실행됩니다 — return, throw, break 또는 정상 완료 시. 리소스의 보장된 정리(파일 닫기, 잠금 해제)에 사용됩니다.
선언 역순(LIFO) — 마지막에 선언된 defer가 먼저 실행됩니다. 이는 중첩된 리소스의 올바른 정리를 보장합니다: 리소스 B가 A 이후에 열린 경우 A보다 먼저 닫혀 이미 해제된 리소스에 대한 종속성을 방지합니다.
직접적으로는 불가능 — Swift는 defer에서 오류 전파를 방지합니다. 던질 수 있는 작업에는 try? 또는 try!를 사용하세요. 모범 사례는 정리 함수를 non-throwing으로 만들거나 defer 내부에서 오류를 외부로 전파하지 않고 처리하는 것입니다.
defer는 스코프에 바인딩되며 return, throw, break를 포함한 모든 종료 시 실행됩니다. finally(다른 언어)는 try-catch에 바인딩되며 try가 있을 때만 실행됩니다. Swift에는 finally가 없습니다 — defer가 이 시나리오를 완전히 커버하며 오류 처리뿐만 아니라 모든 스코프에서 작동합니다.
네, defer는 선언 시점이 아닌 스코프 종료 시점에 변수를 읽습니다. defer가 선언된 후 변수가 변경되면 defer 블록은 최신 값을 참조합니다. 이는 캡처가 생성 시점에 고정되는 일반 클로저와 다릅니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.