Swift프로그래밍Swift Developer

**defer** 블록이 범위 종료 시 LIFO 실행 순서를 보장하는 메커니즘을 설명하고, 여러 **defer** 문이 **throw** 또는 **return**과 같은 제어 흐름 문과 얽혀 있을 때 이 동작이 자원 안전성을 보장하는 이유를 설명하십시오.

Hintsage AI 어시스턴트로 면접 통과

질문에 대한 답변

Swift는 각 렉시컬 범위에 연결된 클로저 thunk의 컴파일러 생성 스택을 통해 defer 문을 구현합니다. 컴파일러가 defer 블록을 발견하면 코드를 클로저로 추출하고 현재 범위의 정리 기록에 등록합니다. 범위를 종료할 때—보통의 흐름, return, throw, 또는 break를 통해—런타임은 이러한 클로저를 후입선출(LIFO) 순서로 실행합니다. 이 스택 규율은 나중에 획득한 자원이 먼저 해제되도록 하여 수동 기록을 하지 않고도 의존성 체인을 유지합니다.

질문의 배경

자원 정리는 역사적으로 결정론적 소멸자나 장황한 예외 처리를 의존해왔습니다. **C++**는 RAII를 통해 객체 수명과 정리를 결합하며, Java 및 **C#**는 정리 로직과 획득 코드를 분리하는 명시적 try-finally 블록이 필요합니다. Go는 객체 지향 오버헤드 없이 범위 기반 정리를 제공하기 위해 defer 문을 도입하여 Swift의 설계에 영향을 미쳤습니다. Swift는 오류 처리 모델을 보완하기 위해 2.0 버전에서 defer를 채택하여 guard 문 및 조기 반환과 깔끔하게 통합되는 선언적 대안을 제공합니다.

문제점

인증, 로깅 및 네트워크 전송이 포함된 파일 작업과 같은 여러 종료 경로를 가진 복잡한 함수는 면밀한 자원 관리를 요구합니다. 개발자는 모든 return 또는 throw 지점에서 이전에 획득한 모든 자원이 해제되도록 보장해야 하며, 파일 설명자에서부터 보안 스코프 북마크에 이르기까지 관리해야 합니다. 단일 정리 포인트를 놓치면 누수 또는 교착 상태가 발생하고, 잘못된 순서(트랜잭션 로그를 플러시하기 전에 데이터베이스를 닫는 경우)는 데이터 손상을 초래합니다. 함수의 복잡성이 커짐에 따라 수동 정리는 유지 관리가 불가능해지며, 범위 경계와 관련된 자동, 결정론적, 정렬된 자원 폐기가 필요합니다.

해결책

Swift 컴파일러는 defer 문을 enclosing 범위의 활성화 기록에 저장된 함수 포인터의 스택으로 변환합니다. 각 defer는 실행 중 이 컴파일러 관리 스택에 자신의 thunk를 푸시합니다. 제어 흐름이 범위의 종료 중괄호에 도달하거나 종료 문을 만나면 주입된 에필로그 코드는 역순으로 스택을 반복하여 각 thunk를 실행합니다. 이 메커니즘은 모든 보류 중인 defer 블록이 오류가 외부 catch 범위로 전파되기 전에 실행되도록 보장하여 어떤 종료 경로에서도 정리가 이루어지도록 합니다.

실생활의 상황

암호화된 사용자 데이터를 내보내는 iOS 애플리케이션을 고려하십시오. 이 과정에서 보안 스코프 리소스 URL을 획득하고, FileHandle을 열고, 암호화된 바이트를 작성하고, 결과를 업로드합니다. 각 단계는 실패할 수 있으며, 파일 설명자나 지속적인 리소스 북마크가 누수되지 않도록 엄격한 정리가 필요합니다.

해결책 1: 모든 종료 지점에서 수동 정리.

개발자는 모든 return이나 throw 전에 **fileHandle.close()**와 **url.stopAccessingSecurityScopedResource()**를 복제할 수 있습니다. 이 접근 방식은 깨지기 쉽고 새로운 오류 확인을 추가할 경우 여러 사이트를 업데이트해야 하며, 리뷰어는 정리 순서가 획득 순서를 따르는지 확인해야 합니다. 유지 관리 중 새로운 종료 지점이 추가될수록 누수의 위험이 커집니다.

해결책 2: deinit이 있는 래퍼 객체.

정리를 수행하는 ScopeManager 클래스를 생성하여 deinit에서 정리하는 것이 ARC에 의존합니다. 그러나 ARC는 범위 종료 시 즉각적인 해제를 보장하지 않습니다; 객체는 자동 해제 풀이 소모될 때까지 지속되거나 변수가 덮어쓰여질 때까지 유지될 수 있습니다. 장기 실행 루프에서 이는 리소스 해제를 지연시켜 "너무 많은 열려 있는 파일" 시스템 오류를 발생시킵니다.

해결책 3: defer 블록.

팀은 각 리소스를 획득한 직후 defer 블록을 선언했습니다:

func exportData() throws { let url = try acquireResource() defer { url.stopAccessingSecurityScopedResource() } let fileHandle = try FileHandle(forWritingTo: url) defer { fileHandle.close() } let encrypted = try encrypt(data) try fileHandle.write(encrypted) try upload(fileHandle) }

암호화 오류가 throw를 유발할 때 런타임은 자동으로 파일 핸들을 닫고 리소스 접근을 중지하여 올바른 역순서를 유지했습니다. 이 솔루션은 결정론성과 인접성으로 선택되었습니다—정리 코드는 획득 코드와 인접하게 나타납니다.

결과:

내보내기 기능은 파일 설명자 누수 없이 10,000개의 동시 작업으로 스트레스 테스트를 통과했습니다. 코드 검토에서는 정리 경로가 놓친 경우가 없었고, 프로파일링에서는 deinit 접근 방식에 비해 즉각적인 리소스 해제가 이루어졌습니다.

지원자가 종종 놓치는 점

질문 1: 함수가 fatalError 또는 무한 루프에 의해 종료되면 defer 블록이 실행됩니까?

아니요. defer는 제어 흐름이 enclosing 범위의 끝에 도달했을 때만 실행됩니다. fatalError가 호출되면, 프로세스는 즉시 종료되며 범위를 풀거나 정리 블록을 실행하지 않습니다. 마찬가지로, 무한 while 루프는 범위를 종료하지 않습니다; 루프 본문 내의 defer 블록은 반복이 완료될 때만 실행되지만, 함수 수준의 while true 루프는 함수 수준의 defer 블록을 결코 트리거하지 않습니다.

질문 2: defer가 선언 이후 변수 변경을 어떻게 처리합니까?

defer는 기본적으로 변수의 참조를 캡처합니다, 값으로는 아닙니다. 예:

var count = 0 defer { print("Deferred: \(count)") } count = 5 // 0이 아닌 5가 출력됩니다.

선언 당시 값을 캡처하려면 개발자는 명시적인 캡처 리스트를 사용해야 합니다: defer { [value = currentValue] in ... }. 지원자는 defer가 선언 시점에 스냅샷을 캡처한다고 가정하여 루프나 변형 알고리즘에서 논리 오류를 초래합니다.

질문 3: defer 블록이 조건 분기 안에 중첩될 때와 부모 범위에서의 실행 순서는 어떻게 됩니까?

defer 블록은 함수 범위가 아니라 나타나는 렉시컬 범위에 묶입니다. if 블록 내의 defer는 해당 if 블록이 종료될 때 실행됩니다, 함수가 반환될 때는 아닙니다. 서로 다른 중첩 수준에 여러 defer 블록이 존재하는 경우, 가장 안쪽 범위의 defer가 해당 특정 블록을 종료할 때 먼저 실행됩니다. 이는 개발자가 모든 defer 블록이 함수 종료 시 실행될 것으로 기대할 때 직관에 어긋나는 순서를 초래하며, 조기 하위 범위 종료를 생성하는 guard 문과 defer를 얽힐 때 특히 그럴 수 있습니다.