View in English

  • Apple Developer
    • 시작하기

    시작하기 탐색

    • 개요
    • 알아보기
    • Apple Developer Program

    알림 받기

    • 최신 뉴스
    • Hello Developer
    • 플랫폼

    플랫폼 탐색

    • Apple 플랫폼
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store

    피처링

    • 디자인
    • 배포
    • 게임
    • 액세서리
    • 웹
    • 홈
    • CarPlay
    • 기술

    기술 탐색

    • 개요
    • Xcode
    • Swift
    • SwiftUI

    피처링

    • 손쉬운 사용
    • AI 및 머신러닝
    • 앱 인텐트
    • Apple Intelligence
    • 게임
    • 보안
    • Xcode Cloud
    • 커뮤니티

    커뮤니티 탐색

    • 개요
    • Apple과의 만남 이벤트
    • 커뮤니티 이벤트
    • 개발자 포럼
    • 오픈 소스

    피처링

    • WWDC
    • Swift Student Challenge
    • 개발자 이야기
    • App Store 어워드
    • Apple 디자인 어워드
    • 문서

    문서 탐색

    • 문서 라이브러리
    • 기술 개요
    • 샘플 코드
    • 휴먼 인터페이스 가이드라인
    • 비디오

    릴리즈 노트

    • 피처링 업데이트
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • tvOS
    • Xcode
    • 다운로드

    다운로드 탐색

    • 모든 다운로드
    • 운영 체제
    • 애플리케이션
    • 디자인 리소스

    피처링

    • Xcode
    • TestFlight
    • 서체
    • SF Symbols
    • Icon Composer
    • 지원

    지원 탐색

    • 개요
    • 도움말
    • 개발자 포럼
    • 피드백 지원
    • 문의하기

    피처링

    • 계정 도움말
    • 앱 심사 지침
    • App Store Connect 도움말
    • 새로 추가될 요구 사항
    • 계약 및 지침
    • 시스템 상태
  • 빠른 링크

    • 이벤트
    • 뉴스
    • 포럼
    • 샘플 코드
    • 비디오
 

비디오

메뉴 열기 메뉴 닫기
  • 컬렉션
  • 전체 비디오
  • 소개
  • 소개
  • 자막 전문
  • Swift로 보안에 민감한 코드 작성하기

    보안에 중요한 코드를 Swift로 작성하여 메모리 안전 소프트웨어를 빌드하고 취약성 클래스를 제거하는 방법을 알아보세요. Swift가 어떻게 경계, 사용 시간, 유형, 초기화, 동시성 전반에서 안전성을 보장하는지 살펴보세요. Span 및 복사가 불가능한 유형 같은 고성능 프리미티브를 알아보고, Strict Memory Safety로 안전하지 않은 구성체를 감사하는 방법을 살펴보며, 기존 C 모듈을 점진적으로 마이그레이션하는 과정을 알아보세요.

    이 세션은 원래 Apple과의 만남 이벤트 ‘앱의 보안 강화하기: 보안 강화를 위한 필수 전략'의 일부로 진행되었습니다. 전체 비디오를 시청하여 더 많은 인사이트와 관련 세션을 확인하세요.

    리소스

      • HD 비디오
      • SD 비디오
  • 비디오 검색…

    저는 Swift 언어 팀의 더그입니다. 오늘은 동료 펠릭스와 함께 메모리, 안전성, 그리고 Swift에 대해 이야기해 보려고 합니다. 메모리 안전성은 주요 보안 과제 중 하나입니다. 지금까지 다른 발표들에서는 C 및 C++ 코드베이스에서 메모리 안전성 버그를 찾아내거나, 이러한 버그가 보안 문제로 이어지는 것을 방지하는 완화 방안에 대해 다뤘습니다. 하지만 이러한 방법 중 어느 것도 프로그래밍 언어 자체에 메모리 안전성 문제를 해결하는 방식을 정의하는 것만큼 포괄적이거나 효과적이지는 않습니다. 그래서 먼저 Swift의 설계가 메모리 안전성을 어떻게 해결하는지 살펴보겠습니다.

    그 다음에는 성능에 대해 이야기하고, 최근 도입된 Swift 기능을 활용하여 안전성을 저해하지 않으면서 저수준 코드에서 최상의 성능을 끌어내는 방법에 대해 논의하겠습니다.

    메모리 안전 언어로의 전환에는 매우 오랜 시간이 걸릴 것입니다. 따라서 안전하지 않은 코드를 캡슐화하는 모범 사례와 C 계열 언어에서 Swift로 점진적으로 전환하는 방법에 대해 논의해 보겠습니다.

    우선, 메모리 안전성은 모든 오류를 방지한다는 식의 포괄적인 개념이 아닙니다. 훌륭한 언어 설계는 특정 유형의 오류를 방지하거나, 코드에서 그러한 오류가 발생하기 어렵게 만들 수 있습니다. 하지만 현실적으로 프로그래밍 오류는 발생하기 마련입니다. 따라서 메모리 안전성이란 프로그램이 작성된 대로 동작하도록 보장하는 것을 의미합니다. 이 말은 언뜻 들으면 터무니없게 들릴 수 있습니다. 물론 프로그램은 작성된 대로 동작하지만, 앞서 언급했듯이 공격자가 메모리 안전성 결함을 악용하면 코드에 작성되지 않은 작업을 프로그램에 강제로 수행하게 만들 수 있습니다. 이것이 바로 메모리 안전성이 보안 문제에서 매우 중요한 부분을 차지하는 이유입니다. C나 C++ 코드와 같은 곳 어디에서든 메모리 안전성 결함이 발생하면, 프로그램이 거의 어떤 행동이든 할 수 있게 되기 때문입니다. 메모리 안전성을 보장하는 언어는 이러한 터무니없는 상황을 방지해 줍니다. 이를 위한 방법은 두 가지가 있습니다. 메모리 안전성 문제가 포함될 가능성이 있는 코드의 컴파일을 거부하거나, 런타임에 검사 기능을 삽입하여 문제가 발생한 시점을 감지하는 것입니다. 이 두 가지 전략을 결합할 수도 있는데, 실제로 Swift는 컴파일 시점과 런타임 시점의 검사를 혼합하여 사용하며, 이에 대해서는 잠시 후에 설명하겠습니다. 여기에는 단 하나의 규칙만 있습니다. 바로 잠재적인 메모리 안전성 문제가 발생하면 프로그램이 절대 계속 실행되어서는 안 된다는 것입니다. 손상된 상태에서 실행을 계속하려는 시도가 바로 프로그래밍 오류가 메모리 안전성 취약점으로 이어지는 원인입니다.

    이 주제에 대해서는 이전에 다룬 바 있으며, 우리는 종종 메모리 안전성을 프로그래밍 언어가 해결해야 할 다섯 가지 축으로 분류합니다. 바로 경계 안전성, 수명 안전성, 유형 안전성, 초기화 안전성, 스레드 안전성입니다. C 계열 언어는 이러한 축 중 어느 하나에 대해서도 안전성을 보장하지 않지만, 일부 문제를 완화하는 데 도움이 되는 대책은 존재합니다. 이제 Swift가 이러한 각 영역을 어떻게 다루는지 자세히 살펴보겠습니다.

    경계 안전성이 가장 이해하기 쉬운 개념입니다. 이는 메모리 블록 내부의 데이터에만 접근할 수 있도록 확인하는 것입니다. 해당 메모리 블록의 경계를 벗어나지 않도록 하는 것이죠. C 언어에서 보았듯이, 할당된 블록의 끝을 넘어 포인터를 조정하고, 관련 없는 다른 값을 읽거나 수정함으로써 메모리 안전성 문제를 일으킬 수 있습니다. 여기 있는 Swift 코드는 동일한 취약점을 재현하려고 시도합니다. 12개의 요소를 가진 배열이 있습니다. 우리는 15번째 요소에 접근하려고 시도합니다. Swift는 런타임 경계 검사를 수행하여, 앞서 경계 안전성에서 다룬 내용과 마찬가지로 이 코드가 즉시 오류로 처리되도록 보장합니다. 이건 쉬운 예시입니다. 이 다음부터는 더 흥미로워집니다. 수명 안전성은 메모리에 접근할 때 해당 메모리가 여전히 유효한지 보장하는 것입니다. C 언어에서는 ‘use after free’와 같은 방식으로 수명 안전성을 위반하는 코드를 작성하기가 꽤 쉽습니다. 이 경우 코드가 할당된 메모리를 해제한 후, 이제는 완전히 다른 것을 가리킬 수도 있는 유효하지 않은 포인터를 통해 해당 메모리에 접근하려고 시도합니다. Swift는 ‘free’ 연산을 제거함으로써 ‘use after free’ 문제를 근절합니다. 여기 코드가 있다면, 제 클래스의 인스턴스를 할당할 수 있습니다. 이를 지역 변수에 저장합니다. 이 인스턴스를 사용하여 메서드를 호출할 수 있으며, 어디에도 `free`가 없다는 점에 주목하세요. 대신, 컴파일러는 해당 인스턴스가 더 이상 사용되지 않을 때 소멸 처리를 삽입할 것이며, 컴파일러는 항상 올바른 방식으로 이를 수행할 것입니다, 그렇죠?

    Swift 컬렉션 클래스에서도 동일한 자동 수명 관리가 사용됩니다. 배열이나 딕셔너리 같은 것들이죠. 여기서 배열은 첫 번째 줄에서 생성되며, 더 이상 사용되지 않게 되면 자동으로 해제됩니다.

    Swift는 Automatic Reference Counting이라는 기법을 사용합니다. 우리는 다른 전통적인 가비지 컬렉션 기법 대신 이 기법을 선택했는데, 이는 엔지니어링 측면에서 매우 훌륭한 절충점을 제공하기 때문입니다. Automatic Reference Counting을 사용하면 기본적으로 메모리 관리에 대해 신경 쓰지 않고도 Swift로 프로그래밍할 수 있습니다. 모든 패턴이 잘 작동하는 것처럼 보이며, 대부분의 경우 수명에 대해 고려할 필요가 없습니다. 반면, Automatic Reference Counting은 매우 빠릅니다. 성능과 메모리 사용량 모두에서 오버헤드가 매우 낮습니다. 또한, 기존의 가비지 컬렉션 방식에서 볼 수 있는 일시 정지 현상도 발생하지 않습니다.

    하지만, 수명 주기 안전성을 유지하기 위해 런타임 오버헤드가 전혀 없어야 하는 경우도 있습니다. 그래서 Swift는 복사 불가능한 타입도 제공합니다. 이 타입들은 고유한 소유 자원에 대한 소유권을 정의합니다. 여기에는 상충 관계가 있는데, 복사 불가능한 타입은 더 제한적인 프로그래밍 모델을 따릅니다. 소유권에 대해 더 깊이 고려해야 합니다. 하지만 그 대가로 오버헤드가 전혀 발생하지 않습니다. 복사 불가능한 유형에 대해서는 강연 후반부에 다시 다루겠습니다.

    유형 안전성은 잘못된 유형으로 메모리에 접근하는 것을 방지해 줍니다. 다시 말해, C 언어에서는 여러 가지 방법으로 유형 안전성 문제를 쉽게 유발할 수 있습니다. 예를 들어, 메모리 내의 이 셀에 정수 값을 기록한다고 가정해 봅시다. 나중에 유니온 멤버를 통해 접근하거나 명시적 형 변환을 사용하여 이를 파일로 다시 읽어들이면, 타입 혼동을 일으킬 수 있습니다.

    Swift는 C의 유니온 및 형 변환 기능과 동등한 기능을 제공하지만, 이 두 가지 모두 설계상 안전하게 작동하도록 되어 있습니다. Swift의 열거형(enum)은 판별형(discriminated)입니다. 판별형 유니온은 현재 어떤 옵션이 활성화되어 있는지를 인코딩한다는 의미입니다. 여기에는 리소스 열거형이 있습니다. 이 열거형은 파일을 저장하거나, 리소스에 접근하기 위한 정수 식별자를 저장할 수 있습니다. switch 문을 사용하여 패턴 매칭을 수행하면, 이러한 특성 덕분에 잘못된 멤버에 접근하는 일이 절대 발생하지 않습니다. 이것은 쌍을 이루는 접근 방식입니다. Swift의 형 변환도 이와 유사합니다. 여기 있는 Swift 코드는 클래스 하나와 하위 클래스 하나를 정의합니다. 해당 하위 클래스는 메서드 하나를 재정의하고, 자체적인 다른 메서드를 추가합니다. 꽤 전형적인 객체 지향 프로그래밍입니다. 여기서는 다운캐스트를 시도해 보겠습니다. 여기서 `as` 구문은 주어진 객체를 `my` 하위 클래스로 다운캐스팅합니다. 이 구문은 옵셔널 값을 반환하는데, 이 값은 `my` 하위 클래스의 인스턴스를 포함하거나, 다운캐스팅이 실패하면 빈 값이 됩니다. 따라서 하위 클래스의 인스턴스라면 코드는 `if` 문 본문 내에서 실행됩니다. 이 경우, 안전한 다른 메서드를 호출할 수 있습니다. 다운캐스팅이 실패하면 옵셔널은 빈 값이 됩니다. if 문 본문은 실행되지 않습니다. 따라서 이는 형 변환으로 인한 잠재적인 메모리 안전성 문제를 제거할 뿐만 아니라, 잘못된 형 변환으로 인한 프로그래밍 오류를 방지하는 데도 도움이 됩니다.

    초기화 안전성(Initialization Safety)도 꽤 직관적인 개념입니다. 이는 메모리가 다시 초기화되기 전에 읽히는 것을 방지해 줍니다. C 언어는 이를 요구하지 않습니다. 따라서 지역 변수를 생성한 후, 초기화되기 전에 바로 사용할 수 있습니다. Swift에서 똑같은 작업을 시도하면 Swift 컴파일러가 이를 거부합니다. 변수에 값을 할당하거나 초기화하지 않았음을 감지하여 컴파일 시점에 오류를 발생시킵니다. 따라서 메모리 안전성 문제가 발생할 수 없습니다.

    마지막이자 가장 까다로운 것은 스레드 안전성입니다. 스레드 안전성을 위반한다는 것은 프로그램 내 어느 곳에서든 데이터 경합이 발생하면 메모리 안전성 문제가 발생할 수 있음을 의미합니다. 그리고 이는 언어가 다른 모든 측면에서 안전하더라도 발생할 수 있습니다.

    이 예제에는 공유 가능한 가변 리소스가 있습니다. `replaceResource` 함수는 해당 리소스의 값을 변경하며, 여기에는 리소스의 타입을 이 식별자로 설정하는 작업도 포함됩니다.

    이 작업이 하나의 스레드에서 수행되고 있다고 가정해 봅시다. 이제 다른 스레드에는 동일한 공유 리소스를 조작하는 `useResource` 함수가 있습니다. 여기서 데이터 경합이 발생한다면, 한 스레드가 이를 파일로 인식하는 동시에 다른 스레드가 정수 값으로 덮어쓰는 상황이 발생할 수 있습니다. 실제 파일 인스턴스를 정수 값으로 덮어쓰는 것이죠. Swift 6의 동시성 모델은 공유 가능한 가변 상태에 대한 동시 액세스가 발생하지 않도록 보장함으로써 이러한 데이터 경합을 방지합니다.

    이와 더불어, Swift는 상위 수준에서 데이터 경합을 유발하지 않는 안전한 동시성 처리 방법을 제공합니다. 그 중 하나가 액터(actor)입니다. 액터는 상태를 캡슐화하고 동시 수정으로부터 보호하는 타입입니다. 여기서의 resource 변수는 이제 액터의 상태에 속하므로, Swift는 해당 상태에 접근할 수 있는 use resource 및 replace resource가 절대 동시에 실행되지 않도록 보장합니다. 이는 Swift의 async-await 모델을 통해 일관되게 적용됩니다. 따라서 액터의 메서드 호출은 항상 비동기적입니다. 호출자는 해당 호출을 안전하게 수행하기 위해 액터가 다른 스레드에서 코드 실행을 완료할 때까지 기다려야 할 수도 있기 때문입니다.

    Swift는 또한 데이터 경합 방지를 위한 저수준 기본 요소도 제공합니다. 여기서의 뮤텍스(mutex) 타입은 한 번에 단일 스레드만 접근할 수 있는 객체를 나타냅니다. 뮤텍스에 저장된 데이터에 대한 모든 액세스는 해당 데이터에 대한 일시적인 액세스 권한을 제공하는 이 width lock 함수를 통해 이루어집니다. 클로저 내부에서 뮤텍스는 단 하나의 스레드만이 동시에 해당 클로저를 실행할 수 있도록 보장합니다.

    따라서 Swift에서는 메모리 안전성이 언어 설계에 깊이 녹아들어 있습니다. 이 점의 이점은 직접 경험해 보기 전까지는 전달하기 정말 어렵습니다. 메모리 안전 언어를 사용할 때, 단순히 ‘걱정할 필요가 없다’는 것만이 전부가 아니기 때문입니다. ‘내가 메모리 안전성 취약점을 유발하고 있지는 않은가?’, ‘혹시 그런 취약점이 있는지 찾아봐야 하지 않을까?’ 이런 고민을 아예 하지 않게 됩니다. 따라서 코드의 정확성과 메모리 안전성과는 무관한 다른 문제들에 모든 주의를 집중할 수 있습니다.

    C 계열 언어에서 넘어오신 분이라면, 이 메모리 안전성이 성능 저하를 초래하는 것은 아닌지 궁금해하실 겁니다.

    대략적으로 말하자면, Swift는 네이티브로 컴파일되는 언어입니다. Swift는 메모리 안전성 모델을 위한 효율적인 구현을 제공하며, 많은 프로그램의 경우 이 정도면 충분합니다.

    하지만 안전하지 않은 C 코드의 성능에 맞먹어야 하는 매우 저수준 코드의 경우, Swift 6.2에서는 이를 돕기 위해 몇 가지 안전한 추상화 기능을 도입했습니다.

    C 언어로 작성된 예제를 살펴보겠습니다. 이것은 런 길이 인코딩(RLE)을 사용하는 이미지 디코더입니다. 루프 내에서 입력 버퍼에서 한 번에 4바이트씩 읽어오고 있습니다. 여기에는 카운트와 픽셀 데이터가 포함되어 있으며, 그 결과를 출력 버퍼를 통해 기록하고 있습니다. 이 과정에서 수동으로 경계 검사를 수행하고 있음을 확인할 수 있습니다. 또한 이 C 코드에는 실제로 검증되지 않은 많은 가정들이 포함되어 있습니다. 예를 들어, 카운트 매개변수가 해당 버퍼의 크기를 정확하게 나타낸다고 가정하고 있습니다. 입력 및 출력 버퍼가 이상한 방식으로 중첩되지 않을 것이며, 이 프로그램이 실행되는 동안 다른 스레드에 의해 수정되거나 해제되지 않을 것이라고 가정하고 있습니다. 다음은 동일한 코드를 Swift로 변환한 것입니다. 여기서 입력 버퍼는 데이터와 개수를 모두 캡슐화한 배열입니다. 따라서 사용자는 이를 알 수 있습니다. 또한 멀티스레드 프로그램에서도 이 함수가 실행되는 동안 데이터가 사라지거나 수정되지 않도록 보장합니다.

    이제 배열 접근 시 경계 검사가 이루어지므로, 오류가 발생하면 메모리 안전성 취약점으로 이어지지 않고 런타임에 오류가 발생합니다. 하지만 성능에 대해 이야기하겠다고 했었죠. 그럼 이 부분을 살펴보겠습니다. 루프 내에서 올바른 경계 검사가 이루어지면, 컴파일러는 안전함이 입증된 경우 경계 검사를 완전히 제거하여 최적화할 수 있습니다.

    Swift의 ‘쓰기 시 복사(copy-on-write)’ 배열은 구현 과정에서 참조 카운팅을 사용합니다. 다시 말해, 컴파일러는 종종 모든 참조 카운팅 처리를 최적화하여 제거할 수 있으며, 이 예제에서도 실제로 그렇게 합니다. 하지만 이 append 연산은 배열에 새로운 요소를 추가하게 됩니다. 배열에 이미 충분한 공간이 없다면, 더 많은 저장 공간을 가진 새로운 메모리를 할당한 다음 기존 요소를 모두 그곳으로 복사해야 합니다. 이는 이전에 사용했던 C 구현보다 더 느릴 것입니다. 그것은 단순히 포인터를 통해 픽셀을 출력하는 방식이었습니다.

    여기에는 두 번째 문제가 있는데, 바로 이 설계로 인해 클라이언트가 스스로 배열을 가져야만 한다는 점입니다. 클라이언트 측에서 자체적으로 배열을 도입하도록 요구할 수 있습니다. 예를 들어 보겠습니다. 배열에 데이터가 있지만, 이 배열은 실제 출력 이미지의 너비와 높이로 구성된 간단한 헤더와, 그 뒤에 실제로 디코딩하고자 하는 이미지 데이터로 이루어져 있습니다. 여기서 주요 문제는, 우리가 개발 중인 디코딩 함수를 호출하기 위해서만도 이 런 길이 인코딩된 픽셀 데이터 전체를 복사해야 한다는 점입니다. 디코더가 전체 픽셀 배열을 읽어야 하기 때문에, 이는 불필요한 힙 할당과 모든 이미지 데이터의 복사를 의미하며, 이는 우리가 절대 원하지 않는 일입니다.

    여기에는 또 다른 관련 문제가 있는데, 바로 디코더가 결과 배열을 매번 힙에 할당하게 된다는 점입니다. 이 특정 이미지 디코딩 작업 자체는 그렇게 해도 괜찮을 수 있습니다. 하지만 픽셀을 특정 위치, 예를 들어 이미 할당된 고정 크기의 프레임 버퍼에 넣기를 원하는 다른 호출자가 있을 수도 있습니다. 그렇게 하려면 결과를 또다시 복사해야 합니다. 따라서 이러한 종류의 문제는 저수준이고 성능에 매우 민감한 코드베이스에서 자주 발생할 수 있습니다. erase slice나 제네릭 컬렉션을 사용하여 부분적으로 해결할 수 있지만, 이를 다루기는 다소 어려울 수 있습니다.

    이때 바로 새로운 ‘span’ 유형 계열이 등장합니다. span은 연속된 메모리에 대한 안전하고 오버헤드가 적은 접근을 제공합니다. Swift에서 unsafe 버퍼 포인터 유형을 사용해 본 적이 있다면, span을 그 안전 버전의 대응물로 생각하면 됩니다.

    span의 핵심 개념은, span이 소유하지 않은 연속된 메모리를 참조한다는 점입니다. 이는 메모리에서 정확히 그렇게 표현되기 때문에, 포인터에 길이를 더한 것과 같다고 생각하면 됩니다. 스팬은 완벽한 메모리 안전성을 제공합니다. 컴파일러가 검사하는 방식으로 수명 안전성을 보장하며, 런타임 오버헤드는 전혀 발생하지 않습니다. 또한 모든 액세스를 검사하여 경계 안전성을 보장합니다. 하지만 가장 중요한 점은, 스팬이 참조하는 저장 공간을 소유하지 않는다는 것입니다. 대신, 저장 공간을 소유하는 다양한 유형과 상호 운용됩니다. ‘쓰기 시 복사(copy-on-write)’ 배열이나 고정 길이 인라인 배열을 참조하는 span을 얻을 수 있습니다. 또한 unsafe 포인터 유형과도 상호 운용되는데, 이는 unsafe 언어나 span이 도입되기 전의 코드와 상호 작용할 때 중요합니다.

    span 유형 계열은 Swift 6.2에서 도입되었습니다. 하지만 우리는 span이 매우 중요하다고 판단하여, 이 유형들을 Apple 운영 체제의 훨씬 이전 버전까지 역호환되도록 만들었습니다. 이를 통해 배포 대상을 높이지 않고도 지금 바로 이 유형들을 도입할 수 있습니다.

    자, 이제 런 길이 디코더의 입력에 span을 적용해 볼 차례입니다.

    이 작업에는 큰 수고가 필요하지 않았습니다. 매개변수 타입을 배열에서 span으로 변경하기만 하면 됩니다. span은 배열과 동일한 요소 접근 API를 제공하기 때문에 이 방식이 작동합니다. 또한 이 코드는 데이터를 단순히 읽어내기만 할 뿐, 데이터를 수정하려고 하지 않습니다. 복사본을 반환하지도 않습니다. 사실 이 함수에서는 그 외에는 변경할 필요가 없었습니다. 이제 변경된 점은 호출자가 배열 대신 span을 제공해야 한다는 API 계약입니다. 그럼 호출자 코드로 돌아가서 어떻게 해야 하는지 살펴보겠습니다.

    우선, 모든 배열에는 span을 생성하는 span 속성이 있습니다. 따라서 코드를 다음과 같이 작성하여 컴파일할 수 있습니다. 끝부분에 span을 추가하기만 하면 됩니다. 이 방법은 작동합니다. 하지만 여전히 새로운 배열을 생성하고 있기 때문에 성능이 향상되지는 않습니다. span을 전달하기 전에 모든 데이터를 복사해 두면 더 나은 성능을 얻을 수 있습니다. 따라서 대신, 원래 배열에서 span을 가져온 다음, 필요한 부분만 잘라내어 decode 함수로 전달하도록 하겠습니다. 읽기 코드는 자신과 관련된 span만 받지만, 불필요한 할당이나 복사가 발생하지 않으면서도 전체적으로 메모리 안전성을 유지합니다.

    이 예제를 보면 span이 단순히 배열을 그대로 대체하는 것처럼 보일 수 있습니다. 하지만 그렇지 않습니다. span은 다른 누군가가 소유한 저장 공간에 대한 참조입니다. 따라서 런타임 오버헤드 없이 안전하게 사용하기 위해, span 유형 자체에는 몇 가지 필수적인 제한 사항이 있습니다.

    따라서 span은 우리가 ‘비탈출형(non-escapable) 유형’이라고 부르는 것입니다. span은 위에서 보여준 이 물결표(~)를 사용한 비탈출형 구문으로 작성됩니다. 이 같은 구문을 사용하여 span과 같은 동작을 하는 자신만의 비탈출 가능 타입을 정의할 수 있습니다. 이제 비탈출 가능 타입을 사용하면 앞서 했던 것처럼 언제든지 함수의 인수로 전달할 수 있습니다.

    하지만 함수 외부로 span을 반환할 수는 없습니다. 비탈출 가능 타입의 값은 그 수명이 매개변수 중 하나에 묶여 있을 때만 반환할 수 있기 때문입니다.

    이것이 무엇을 의미하는지 알아보기 위해 첫 번째 `run` 함수의 본문을 자세히 살펴보겠습니다. 여기 코드는 첫 번째 요소와 일치하는 요소들의 연속 구간을 찾는 것입니다. 그리고 내부의 루프는 첫 번째 불일치를 발견하면 종료됩니다. 여기서 중요한 부분은 그 논리가 아니라 마지막의 반환 문입니다. 자, 여기서 우리는 무엇을 반환할까요? 우리는 받은 data 매개변수에서 직접 추출한 span을 반환합니다. 그저 해당 부분만 추출한 것입니다. 그리고 이는 문제가 되지 않습니다. 즉, 우리에게 span을 제공한 호출자가 결과 span을 충분히 오랫동안 유지해 줄 것이기 때문입니다.

    하지만, 만약 코드가 data의 복사본을 새로운 배열에 생성했다고 가정해 봅시다. 이 배열은 지역 변수에 저장되고, 그 복사본에서 span을 반환하려고 시도합니다. 이 경우 컴파일러는 오류를 발생시킬 것입니다.

    그 이유는 새로 생성된 이 배열이 지역 변수에 저장되어 있기 때문입니다. 바로 그 지역 변수가 배열을 유지시켜 주는 것입니다. 호출자에게 지역 변수에 대한 스팬을 반환할 수는 없습니다. 함수를 벗어나는 즉시 지역 변수가 사라지기 때문입니다. 따라서 C 언어로 생각해보면, 제가 언급한 이러한 제한 사항이 익숙하게 느껴질 수도 있습니다. 로컬 변수의 주소를 가져온다고 상상해 보세요. 그 포인터가 어딘가에 저장되었다가, 함수가 반환되고 로컬 변수가 사라진 후에 사용되는 일이 없도록 매우 주의해야 합니다. Swift는 이러한 제한 사항을 언어의 일부로 포함하고 있으므로, 메모리 안전성을 해치는 실수를 저지르지 않게 됩니다.

    Span 자체는 메모리에 대한 읽기 전용 액세스만 제공합니다. 또한 읽기뿐만 아니라 쓰기도 가능한 변형 가능한 액세스를 제공하는 `mutable span`이라는 또 다른 유형이 있습니다. 앞서 언급한 연속적인 컬렉션 중 어느 것이든 `mutable span` 속성을 사용하여 `immutable span`을 해당 저장 공간에 할당한 다음, 예상한 대로 인덱싱을 통해 요소를 수정할 수 있습니다.

    여기서 안전 모델의 핵심은 `mutable span`이 기본 메모리에 대한 배타적 액세스를 요구한다는 점입니다. 즉, 특정 메모리 블록을 가리키는 불변 스팬이 있다면, 다른 누구도 그 메모리에 접근할 수 없습니다. 쓰기뿐만 아니라 읽기도 불가능합니다.

    따라서 코드에서 동일한 배열 내부에 접근하는 두 번째 스팬을 생성한 후 변형을 시도하면, 컴파일러는 활성 변형이 진행 중인 동안 해당 저장소에 대한 모든 접근을 차단하기 위해 여기서 오류를 발생시킵니다.

    이 배타성 모델은 Swift의 핵심입니다. 사실 Swift가 탄생한 초기부터 존재해 왔습니다. 이 모델은 변형 메서드나 inout 매개변수 등을 사용할 때 메모리 안전성을 보장해 줍니다. 대부분의 Swift 프로그래머는 이 모델이 존재한다는 사실조차 모릅니다. 실제로 메모리 안전성 위반을 유발하는 상황에 직면하는 경우가 매우 드물기 때문이죠. 하지만 메모리 안전성을 보장하기 위한 안전 장치로 항상 존재합니다. 자, 이제 런 길이 디코딩 함수에서 힙에 할당된 배열을 반환하는 대신 불변 스팬을 도입할 차례입니다. 이 작업은 스팬을 다룰 때보다 조금 더 많은 노력이 필요할 것입니다. 하지만 다시 한 번, 함수 시그니처부터 살펴보겠습니다. 여기서 핵심은 호출자에게 반환될 새로운 저장 공간을 생성하는 대신, 호출자가 이 출력 매개변수를 통해 결과를 어디에 저장할지 알려준다는 점입니다. 이 매개변수가 out으로 전달되는 이유는, 가변 스팬을 수정할 때 결과가 바로 그곳으로 저장되기 때문입니다. 또한, 출력 배열에 추가하는 대신, 픽셀 데이터가 이제 이 out 매개변수를 통해 최종 저장 위치에 직접 기록된다는 점에 유의하세요. 이는 이 함수 내에서 더 이상 메모리 할당이 필요하지 않음을 의미합니다. C에서 그랬던 것처럼, 읽기 및 쓰기 버퍼를 설정하는 것은 전적으로 호출자의 몫입니다. 호출자에 대해 말하자면, 사실 이전에는 초기 디코딩이 배열을 반환한다는 점에 의존하고 있었습니다. 그럼 다시 돌아가서 그 부분을 살펴보겠습니다. 이제 이 함수가 해야 할 일은 자체 픽셀 배열을 할당하는 것입니다. 그리고 그 배열에 가변 스팬을 전달하여, 결과 픽셀을 채울 코드를 읽어들일 것입니다.

    물론 이 호출자는 힙 할당을 선택하고 있지만, 다른 호출자는 완전히 다른 선택을 할 수도 있습니다.

    예를 들어 보겠습니다. 이 구조체는 320×240 크기의 픽셀 프레임 버퍼를 나타냅니다. 힙 메모리 할당을 피하기 위해 인라인 배열로 표현되어 있습니다.

    이미지 디코딩 작업은 다시 Inout을 사용하여 전달받은 프레임 버퍼에 대한 가변 참조를 받습니다. 그런 다음 해당 픽셀에 대한 불변 스팬을 얻어 디코딩 함수로 전달합니다.

    무슨 일이 일어나고 있는지 보시겠습니까? 디코더가 프레임에 직접 접근합니다. 아니요. 불필요한 복사도, 메모리 할당도 없습니다.

    span을 도입하는 것은 성능과 메모리 안전성 모두에 있어 큰 이점입니다. 여러분의 Swift 코드베이스에서도 이를 도입하시기를 권장합니다. span을 도입해야 할 부분을 찾고 계신다면, 성능 관점에서 두 가지 출발점이 있습니다. 배열이나 데이터형을 사용하는, 성능에 민감한 코드를 찾아보세요. 불필요한 복사나 힙 할당이 발견된다면, 메모리 안전성 관점에서 디코딩 작업에서 우리가 했던 것처럼 대신 `span`을 사용하세요. 먼저 코드에서 `unsafe` 버퍼 포인터 유형의 사용을 대체하는 것부터 시작하세요. 이름에서 알 수 있듯이, `unsafe` 포인터 유형은 메모리 안전성을 보장하지 않으므로 극히 드물게만 사용해야 합니다. span은 unsafe 포인터의 대부분의 용도에 대해 안전한 대안이지만, 이를 구현하려면 약간의 리팩토링이 필요할 수 있습니다. 이제 제 동료 펠릭스에게 마이크를 넘겨, 메모리 안전성을 저해하지 않으면서 Swift에서 unsafe 구문을 다루는 방법에 대해 더 자세히 설명해 드리겠습니다.

    고마워요, 더그.

    안녕하세요, 저는 펠릭스라고 합니다. 보안 엔지니어링 및 아키텍처 팀 소속입니다. 다음 안건은 unsafe 코드를 안전하게 사용하는 방법입니다.

    메모리 안전 언어는 프로그래밍의 미래입니다.

    더그가 설명했듯이, 스팬(span)과 같은 오버헤드가 낮은 기본형을 사용하면 안전한 코드는 매우 빠르며, 안전한 코드는 메모리 안전성 버그를 유발할 수 없습니다.

    동시에, 오늘날 비안전 코드는 어디에나 존재합니다. 말 그대로입니다.

    그리고 이전에는 사용하기 어려웠던 분야에서도 안전한 언어가 점차 확산되고 있지만, 여전히 수십억 줄에 달하는 비안전 코드를 고려해야 합니다. 이는 수십 년 동안 알려져 온 사실입니다. 안타깝게도, 이는 기존 방식에 뿌리를 둔 일반적인 엔지니어링 상식입니다. 재작성은 위험합니다. 새로운 구현은 버그를 유발하거나 재발시킬 수 있으며, 기존 구현이 안전하지 않은 구현과 동시에 계속 발전할 경우 서로 경쟁하게 됩니다.

    그렇기 때문에 작은 단위로 재작성을 진행하며 안전하지 않은 코드에서 점진적으로 마이그레이션할 계획을 세우는 것이 중요합니다. 이렇게 하면 위험을 관리하기가 훨씬 쉬워지고 성공 확률도 크게 높아집니다.

    그리고 이것이 오늘날 중요한 이유는 Swift가 C, C++, Objective-C로부터 점진적인 마이그레이션을 가능하게 하도록 설계된 고유한 기능을 갖추고 있기 때문입니다.

    이러한 도구 중 첫 번째는 엄격한 메모리 안전성입니다. 엄격한 메모리 안전성을 활성화하는 것보다 안전하지 않은 코드를 다루는 더 안전한 방법은 없습니다. 이는 매우 중요합니다.

    엄격한 메모리 안전성은 안전하지 않은 코드의 모든 사용처를 드러냅니다. 이는 매우 유용합니다. 왜냐하면... Swift는 종종 안전하지 않은 것의 이름에 ‘unsafe’라는 단어를 붙이긴 하지만, 일부 연산은 암묵적으로 안전하지 않기 때문입니다. 예를 들어, 이 코드에서 배열 복사 함수는 두 개의 배열 변수 x와 y를 선언하고, memcpy를 사용하여 한 배열을 다른 배열로 복사합니다.

    이 코드에서는 `memcpy`가 안전하지 않은 작업을 수행한다고 명시되어 있지는 않지만, `memcpy`는 C 함수이므로 안전하지 않은 포인터를 받아들입니다. `x`와 `y`는 암시적으로 안전하지 않은 포인터로 변환됩니다. 이로 인해 명확한 징후 없이 메모리 안전성 버그가 발생할 수 있습니다.

    엄격한 메모리 안전성을 활성화하면 컴파일러가 경고를 표시합니다. 이 경우와 마찬가지로, 표현식 내에서 안전하지 않은 구문을 사용하지만 `unsafe` 키워드로 표시되지 않은 모든 안전하지 않은 코드 사용에 대해서도 마찬가지입니다.

    이 문제는 호출 앞에 unsafe 키워드를 추가하면 해결됩니다.

    잠시 시간을 내어 unsafe 키워드에 대해 자세히 설명하겠습니다. 컴파일러 경고를 해결하는 것 외에도, 이 키워드에는 두 가지 주요 기능이 있습니다.

    첫째, 코드베이스를 감사할 때 보안 검토자에게 경고를 주는 역할을 합니다. unsafe 키워드는 잠재적으로 위험한 일이 발생하고 있음을 나타내는 지표입니다.

    둘째, 그리고 더 중요한 점은, 컴파일러가 검증할 수 없는 부분을 반드시 직접 확인해야 한다는 점을 상기시켜 준다는 것입니다.

    앞서 언급한 Memcpy 예제를 다시 살펴보겠습니다. 두 배열 모두 4바이트를 포함하고 있으므로 이 코드는 올바릅니다. 하지만 컴파일러는 이것이 호출의 전제 조건이라는 사실을 알지 못합니다. unsafe 키워드는 Memcpy가 올바르게 사용되었는지 확인해야 한다는 점을 상기시켜 줍니다.

    Xcode 프로젝트에서 이 기능을 활성화하려면, Swift 언어 옵션에서 ‘엄격한 메모리 안전성’ 설정을 찾으세요.

    Swift 패키지의 경우, 패키지 설명에 ‘엄격한 메모리 안전성’ 설정을 추가하면 이 기능이 활성화됩니다.

    이제 ‘엄격한 메모리 안전성’이 활성화되어 unsafe 코드가 표시되더라도, 여전히 unsafe 기능을 아예 사용하지 않는 것이 가장 좋습니다.

    unsafe 코드가 진정으로 필요한 경우는 단 두 가지뿐입니다. 첫째, unsafe 라이브러리와 상호 운용하기

    위함이고, 둘째는 안전한 기본형을 구현하기 위함입니다. 많은 경우, 실제로는 같은 작은 unsafe 보석의 두 가지 측면이 존재합니다.

    안전한 언어로 새 코드를 작성하는 데는 많은 이점이 있지만, 일부 작업의 경우 unsafe 구현이 여전히 최선의 도구일 수 있습니다.

    이는 안전하지 않은 라이브러리가 흔하며, 보안 문제를 제쳐두고 보더라도 여전히 가장 성숙한 상태일 수 있기 때문입니다.

    안전하지 않은 라이브러리를 안전한 언어로 재작성할 수 있는 경우, 그것이 항상 바람직하지만, 임의의 시간 내에 항상 가능한 것은 아닙니다. 안전한 재작성에는 시간과 전문 지식이 필요하며, 그 시간과 전문 지식이 확보될 때까지는 시간이 걸립니다. Swift는 해당 라이브러리가 안전하게 사용되도록 보장하는 데 도움을 줄 수 있습니다.

    더그(Doug)는 앞서 decode 함수를 소개했습니다. 이 함수가 쉽게 재작성할 수 없는 외부 C 라이브러리에 구현되어 있다고 가정해 봅시다.

    Swift가 헤더를 감지하면, 이 함수를 Swift의 unsafe 인터페이스로 노출합니다. 이 함수는 소스 포인터, 소스 크기, 대상 포인터, 대상 개수라는 동일한 매개변수를 받습니다. 여전히 unsafe 값을 사용하기 때문에 이상적인 방법은 아니지만, Swift에서 호출할 수는 있습니다. 하지만 엄격한 메모리 안전성이 활성화된 상태에서는 컴파일러가 동일한 unsafe 구문을 생성합니다. 경고.

    더그가 보여준 것처럼 unsafe 구현을 감싸는 안전한 래퍼를 작성하는 것이 더 좋습니다. 이 래퍼는 span을 입력으로 받고, mutable span을 출력으로 반환합니다. 이 래퍼 내에는 unsafe가 많이 언급될 것입니다. 이는 span에서 포인터를 추출하거나 해당 포인터를 사용하는 각 연산에 대해 unsafe로 표시해야 하기 때문입니다. 하지만 보안 감사는 쉽게 수행할 수 있습니다.

    이 코드는 각 span에서 unsafe 포인터를 추출하여, 해당 카운트와 함께 C 구현체로 전달합니다.

    Swift 인터페이스 사용자를 감사할 필요는 없습니다. span을 전달하기 때문이며, span은 안전하고 여기에는 오류가 없기 때문입니다. 하지만 만약 문제가 있다면, 이 구현 부분에 있을 것입니다. C 구현에 메모리 안전성 버그가 있을 수도 있습니다. 하지만 ‘red code’가 안전한 구현을 얻게 되면, 호출하는 Swift 모듈은 문제가 없음이 입증됩니다. 이 위험은 완전히 제거됩니다.

    이제 안전한 기본형을 구현하는 측면에서, 안전하지 않은 코드를 래핑할 때 발생할 수 있는 또 다른 문제는 외부 라이브러리가 ‘vending’을 할 수 있다는 점입니다. 수동으로 생성하고 소멸시켜야 하는 일종의 리소스 말입니다.

    다시 한 번 실제 디코딩 함수를 살펴보겠습니다. 단일한 상태 없는 디코딩 함수 대신, 이제 RL이 포함된 디코더 객체를 생성한 다음 RL destroy를 통해 이를 소멸시켜야 하도록 수정했습니다.

    RL 디코딩 함수는 이전과 대체로 동일하지만, 이제 첫 번째 인자로 디코더를 받습니다.

    이 패턴에는 명시적인 Dianette가 필요합니다. Copyable 구조체는 Dianette를 가질 수 없기 때문입니다. 이 기능은 항상 클래스를 통해 구현되어 왔습니다. 현재 안전하지 않은 리소스를 캡슐화하는 많은 코드가 다음과 같은 형태를 띠고 있습니다. 초기화자는 RL의 init 함수를 호출하고, 그 후 RL의 destroy 함수를 호출합니다. 그리고 디코딩 함수는 이전과 거의 동일하게 작동할 것입니다.

    이 방식은 작동하지만, 잠재적인 효율성만큼은 아닙니다.

    첫째, 인스턴스는 동적으로 할당됩니다. 수명이 긴 객체의 경우 보통 문제가 되지 않지만, 인스턴스를 반복적으로 생성하고 소멸시키면 피할 수 있는 malloc 및 free 호출이 발생합니다. 게다가 동적 할당을 사용하여 또 다른 동적 할당을 감싸는 것은 훨씬 더 많은 비용이 듭니다. 둘째, 인스턴스는 참조 카운팅이 적용됩니다. 이를 통해 복잡한 메모리 관리 상황에서도 안전성을 유지할 수 있지만, 객체의 수명이 매우 짧더라도 참조 카운트 처리로 인한 오버헤드를 감수해야 할 수 있습니다.

    마지막으로, 클래스 인스턴스의 모든 필드는 런타임에 세밀한 배타성 검사를 거치며, 이를 통해 구조체(struct)에서 수행할 수 있는 것보다 더 유연한 별칭(aliasing) 연산을 가능하게 합니다. 하지만 다시 말해, 연산이 단순한 타입의 경우 아무런 이점을 얻지 못하면서도 여전히 비용을 지불하게 될 수 있습니다. 클래스 인스턴스는 매우 다재다능하지만, 필요 이상으로 부피가 클 수 있습니다. 이것들은 사소한 오버헤드 비용이지만, 검증을 전혀 수행하지 않는 안전하지 않은 구현과 비교하면 빠르게 누적됩니다.

    Swift 6부터는 이러한 사용 사례에 복사 불가능한 구조체(non-copyable struct)를 사용할 수 있습니다. 트럭에는 그런 동적 할당이 필요하지 않습니다. 따라서 오버헤드의 한 가지 원인이 사라진 셈입니다. 또한 트럭은 대부분 컴파일 시점에 검증 가능한 대략적인 배타성 검사를 수행합니다. 따라서 프로그램에서 이러한 검사가 애초에 적게 발생하며, 최적화를 통해 제거하기도 더 쉽습니다. 하지만 디코더 함수에서 참조 타입의 유연성 덕분에, 복사 불가능한 구조체를

    공유하는 것은 클래스나 복사 가능한 구조체보다 훨씬 더 제약이 많습니다.

    동일한 디코더에 대한 여러 참조를 가지고, 그중 어느 것을 통해서든 decode 메서드를 호출하는 데는 아무런 문제가 없습니다.

    하지만 복사 불가능한 구조체(struct)로 동일한 작업을 수행하면 오류가 발생합니다. 컴파일러는 객체 수준에서 즉시 오류를 감지할 것입니다. a가 B로 이동된 후 다시 사용되기 때문입니다.

    요약하자면, 단순한 객체 관리의 경우 복사 불가능한 구조체가 클래스에 비해 성능 면에서 많은 이점을 제공합니다. 유연성이 떨어지기 때문에 항상 사용할 수 있는 것은 아닙니다. 더 구체적이지만, 그 대가로 더 예측 가능한 성능을 얻을 수 있습니다.

    마지막으로 이야기하고 싶은 것은 C에서 Swift를 호출하는 것입니다.

    현재 모든 주요 플랫폼은 안전하지 않은 C 또는 C++ 코어를 가지고 있으며, 모든 안전한 언어는 어떤 식으로든 그 코어를 호출해야 합니다. C와의 혼합은 안전한 언어의 정상적이고 당연한 기능입니다. 그럼에도 불구하고, 이는 종종 어렵습니다.

    이 작업이 어려울 수 있는

    한 가지 이유는 언어마다 기대치가 다르기 때문이며, 한 언어가 다른 언어를 호출할 때 양쪽 언어의 기대치가 모두 충족되어야 하기 때문입니다. 예를 들어, 가비지 컬렉션을 사용하는 언어에서 C로 객체 포인터를 전달할 경우, 객체가 존재하는 동안 포인터가 이동하지 않도록 가비지 컬렉터와의 특별한 협력이 필요할 수 있습니다. C에서의 참조. 또 다른 이유는 대부분의 언어가 서로를 잘 이해하지 못하기 때문입니다. 예를 들어, C를 호출할 수 있는 많은 언어에서 상호 운용성(interop)을 위해서는 글루코스가 필요합니다. 이는 대상 언어의 컴파일러가 C 헤더 파일을 읽을 수 없기 때문에 C 헤더 파일을 설명하는 역할을 합니다. 이러한 코드를 작성하는 것은 지루한 작업입니다. 하지만 다행히도 Swift는 안전하지 않은 언어와의 상호 운용성을 염두에 두고 설계되었습니다.

    Swift는 전체 C 컴파일러를 내장함으로써 개발자에게서 이러한 복잡성을 대부분 제거합니다.

    Swift는 브리징 헤더에서 파싱을 통해 인라인 함수를 포함하여 헤더 파일의 임의의 선언을 해석하고 사용할 수 있게 합니다. 프로젝트는 브리징 헤더를 통해 임의의 헤더를 포함할 수 있으며,

    Swift는 자체 헤더를 생성하여 C 컴파일러가 해당 선언을 사용할 수 있게 합니다.

    이제 더그가 초기에 보여준 디코딩 예제를 다시 화면에 띄워보겠습니다. 더그는 `span`을 사용한 구현이 가장 유연한 선택지임을 보여주었습니다. 이 구현은 완전히 다른 방식으로 픽셀을 반환하는 다른 두 함수의 고유한 백엔드로 사용되었습니다. 첫 번째는 배열을 반환하고, 두 번째는 모드 X 프레임 구조체에 참조 방식으로 출력을 반환합니다.

    Swift 6.3부터는 span 구현체가 더그가 처음에 보여준 것과 같은 C 함수의 백엔드 역할도 할 수 있습니다.

    이것은 더그가 아직 소개하지 않은 구현체입니다. Swift 구현체로 대체하기 위해 이 코드를 삭제할 예정이니, 마지막으로 한 번 자세히 살펴보시기 바랍니다.

    이를 위해서는 먼저 대상 C 함수와 호환되는 프로토타입을 가진 Swift 함수가 필요합니다.

    여기에는 입력 포인터, 계정, 출력 포인터를 인수로 받아, 입력 포인터와 출력 포인터에 걸쳐 스팬을 생성하는 준비된 코드가 있습니다. 그리고 이 코드는 일반적인 읽기 코드를 호출하는 Swift 백엔드를 사용합니다. 다음으로, 이 함수를 노출해야 하는데, 이를 수행하는 두 가지 방법이 있습니다. 첫 번째 방법은 함수에 `at c` 속성을 추가하는 것입니다.

    'at C'를 사용할 경우, Swift는 해당 타입을 적절한 대응 C 타입으로 변환합니다. 예를 들어, Swift의 Int32 타입은 int32t로 변환되며, 기본 정수 타입은 예상되는 대응 C 타입으로 변환됩니다(char는 char, short는 short, end는 int 등). 단, int는 pointer div t로 변환된다는 점에 유의하십시오. 이것이 구현 시 코드가 size t 대신 pointer div t를 받아들이고 반환하는 이유입니다. 구현할 함수가 이미 브리징 헤더에 존재하는 경우, 생성된 헤더에 선언을 출력하는 대신 at C 구현 조합을 사용하는 두 번째 옵션이 있습니다. 이는 Objective-C 메서드를 at 구현과 함께 사용할 때와 유사합니다. 이렇게 하면 컴파일러가 브리징 헤더에서 decode 함수에 대한 기존 선언을 찾도록 요청합니다.

    이 방법을 사용할 때, 컴파일러는 Swift 프로토타입이 C 프로토타입과 일치하는지 확인하고, 호환되지 않으면 오류를 발생시킵니다.

    단순히 `at C`만 사용할 경우 Swift는 인자 유형을 직접 지정해야 하지만, `at implementation at C`를 사용하면 다른 변환에도 유연하게 대응할 수 있습니다. 예를 들어, C 프로토타입이 `size T`를 사용하더라도 Swift는 `Uint` 대신 `int`를 받는 구현도 허용합니다.

    자, 이제 방금 무슨 일이 있었는지 되짚어 볼 좋은 기회입니다. 제 오른쪽에는 사진 속 반려동물을 식별하는 ‘cats and dogs’라는 C 함수가 있습니다.

    이 함수는 이미지 구조체 포인터와 반려동물 기록(Pet) 버퍼 포인터를 매개변수로 받습니다. 이미지를 압축 해제하기 위해 픽셀 메모리를 관리하며, decode 함수를 호출하여 이미지를 압축 해제합니다. 마지막으로, `identifyCatsAndDogs`를 호출하여 사진 속 고양이와 개가 있는 위치를 찾아냅니다.

    이전에는 앞서 dog가 보여준 것처럼 오른쪽의 C 구현체 `decode`를 호출했을 것입니다. 하지만 Swift 코드를 전혀 수정하지 않고도 C 구현체를 사용함으로써, `catsAndDogs` 함수는 이제 안전한 `decode` 구현체를 사용하게 되었으며,

    이는 Swift에서 완벽하게 작동했습니다. 이는 Swift 컴파일러가 decode에 대해 clang이 인식하는 것과 동일한 C 함수 프로토타입을 가진 디코딩 구현체와 매칭할 수 있기 때문입니다.

    이것이 C와 Swift 간 이주에 필요한 전부이며, 이러한 구현체들은 C 코드 베이스를 Swift로 마이그레이션하고, C 호출자가 사용해야 할 때 안전한 언어로 새로운 기능을 작성하기 위한 훌륭한 도구입니다. 이제 span, move-only 타입 및 C와 Swift 간의 인터럽트에 대해 배웠으니, 다음은 여러분이 해야 할 일입니다. 먼저, 코드에서 엄격한 메모리 안전성을 적용하세요.

    이를 통해 코드베이스 내의 안전하지 않은 작업을 파악할 수 있게 됩니다. 다음으로, span을 사용하여 연속된 메모리에 안전하게 접근하세요. 안전하지 않은 버퍼 포인터 사용을 대체하는 것부터 시작하세요. 그런 다음, 가능한 경우 안전하지 않은 리소스를 move-only 타입 클래스로 캡슐화하여 안전한 인터페이스를 생성하고, 그렇지 않은 경우에는 다른 방법을 사용하세요. 마지막으로, Swift의 상호 운용성 기능을 활용하여 안전하지 않은 코드를 점진적으로 Swift로 마이그레이션하기 시작하세요. 경청해 주셔서 감사합니다. 메모리 안전성 문제는 오늘날 보안 버그의 가장 큰 원인이며, 여러분과 함께 더 안전한 세상을 만들어 나가기를 기대합니다.

Developer 바닥글

  • 비디오
  • Meet with Apple
  • Swift로 보안에 민감한 코드 작성하기
  • 메뉴 열기 메뉴 닫기
    • iOS
    • iPadOS
    • macOS
    • tvOS
    • visionOS
    • watchOS
    • App Store
    메뉴 열기 메뉴 닫기
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • Icon Composer
    • SF Symbols
    메뉴 열기 메뉴 닫기
    • 손쉬운 사용
    • 액세서리
    • AI 및 머신 러닝
    • Apple Intelligence
    • 오디오 및 비디오
    • 증강 현실
    • 비즈니스
    • 디자인
    • 배포
    • 교육
    • 게임
    • 건강 및 피트니스
    • 앱 내 구입
    • 현지화
    • 지도 및 위치
    • 보안
    • Safari 및 웹
    메뉴 열기 메뉴 닫기
    • 문서
    • 다운로드
    • 샘플 코드
    • 비디오
    • 문서 아카이브
    메뉴 열기 메뉴 닫기
    • 도움말 및 문서
    • 문의하기
    • 포럼
    • 피드백 및 버그 리포트
    • 시스템 상태
    메뉴 열기 메뉴 닫기
    • Apple Developer
    • App Store Connect
    • 인증서, ID 및 프로파일
    • 피드백 지원
    메뉴 열기 메뉴 닫기
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program
    • Mini Apps Partner Program
    • News Partner Program
    • Video Partner Program
    • Security Bounty Program
    • Security Research Device Program
    메뉴 열기 메뉴 닫기
    • Apple과의 만남
    • Apple Developer Centers
    • App Store 어워드
    • Apple 디자인 어워드
    • Apple Developer Academy
    • WWDC
    최신 뉴스 읽기 Apple Developer 앱 받기 bilibili, LinkedIn, WeChat, YouTube에서 팔로우하기
    Copyright © 2026 Apple Inc. 모든 권리 보유.
    약관 개인정보 처리방침 계약 및 지침