왕논의 연구실

swift의 소유권 개념 본문

iOS/Swift

swift의 소유권 개념

ywangnon 2025. 4. 28. 23:08
반응형

1. 왜 ‘소유권 (ownership)’이 필요할까?

Swift 의 값 - 복사(copy) 기반 모델은 편리하지만,

  • 고유-자원(파일 디스크립터, 락, GPU 버퍼 등)은 “한 번에 하나만” 소유되어야 안전합니다.
  • 불필요한 ARC retain/release · 메모리 복사는 성능을 떨어뜨립니다.

이를 해결하기 위해 Swift 5.9~5.10에서 “값의 소유권 을 코드에 표현” 하는 세 가지 키워드가 도입되었습니다.

키워드 의미 대표 위치
~Copyable (비복사/noncopyable) “이 값은 복사 불가, 이동-전용(move-only)” 타입 선언
borrowing “잠깐 빌려쓰기”∙읽기 전용, 끝나면 호출자 소유권 유지 파라미터·메서드·클로저
consuming “소유권을 넘겨받아 소비”∙끝나면 호출자에서 더 못 씀 파라미터·메서드·클로저

2. ~Copyable (Non-Copyable) — 값 자체가 “유일”해야 할 때

// 파일 디스크립터를 구조체 하나로 감싸고 싶다
struct FileDescriptor: ~Copyable {
    private var rawFD: Int32

    init(open path: String) throws {
        rawFD = open(path, O_RDONLY)
    }

    func read(byteCount n: Int) -> [UInt8] { /* … */ }

    deinit { close(rawFD) }   // 고유 자원이므로 deinit 허용
}

‼️ 제약


3. borrowing — “읽기만, 복사 안 하고 빌려쓰기”

// fd를 잠깐 읽기만 한다 – 함수가 끝나면 caller가 그대로 소유
func printHeader(_ fd: borrowing FileDescriptor) {
    let header = fd.read(byteCount: 128)
    print(header)
}   // 여기까지 fd는 여전히 유효

4. consuming — “소유권을 받아서 내가 파괴하거나 넘긴다”

// fd를 ‘소모’하여 파일 전체를 읽고, fd는 여기서 생이 끝난다
func slurpFile(_ fd: consuming FileDescriptor) -> [UInt8] {
    var bytes: [UInt8] = []
    while let chunk = try? fd.read(byteCount: 4096) {
        bytes += chunk
    }
    // fd.deinit 자동 호출 → 파일 닫힘
    return bytes
}

// 호출자 쪽
let fd = try FileDescriptor(open: "/tmp/foo")
let data = slurpFile(fd)     // ✅ 이후 fd 사용 불가 → 컴파일 오류

5. borrowing vs consuming vs inout

목적 내부에서 수정? 호출자 관점 복사 발생
borrowing 그대로 유지 ❌ (금지)
consuming ✅ 가능 더 이상 접근 불가 ❌ (이동)
inout ✅ 가능 같은 변수에 반영 ✅ 필요 시 복사

6. 값 복사가 꼭 필요할 땐 copy / consume

func duplicate(_ s: borrowing String) -> (String, String) {
    (copy s, copy s)          // 명시적 복사
}

let newFD = consume fd        // 소유권 이동 후 fd는 무효
``` ([swift-evolution/proposals/0377-parameter-ownership-modifiers.md at main · swiftlang/swift-evolution · GitHub](https://github.com/apple/swift-evolution/blob/main/proposals/0377-parameter-ownership-modifiers.md))

---

### 7. 언제 써야 할까?

| 시나리오 | 권장 키워드 |
|----------|-------------|
| 파일·소켓·락·GPU 버퍼 등 **고유 자원** 캡슐화 | `~Copyable` |
| “읽기 전용으로 잠깐 사용” (로그 출력, 해시 계산) | `borrowing` |
| “리소스를 _닫거나_ 컨테이너에 옮겨 넣어야” 하는 API | `consuming` |
| 기존 값 수정 후 그대로 돌려주기 | `inout` 여전히 적합 |

---

### 8. 실무 팁

1. **라이브러리 공개 API**에는 `borrowing / consuming`을 미리 명시해서 ABI 안정성을 확보하세요. (바뀌면 바이너리 호환성 깨짐) ([swift-evolution/proposals/0377-parameter-ownership-modifiers.md at main · swiftlang/swift-evolution · GitHub](https://github.com/apple/swift-evolution/blob/main/proposals/0377-parameter-ownership-modifiers.md))  
2. `Sendable` 프로토콜은 noncopyable 타입도 채택할 수 있어 **동시성 모델과 호환**됩니다. ([swift-evolution/proposals/0390-noncopyable-structs-and-enums.md at main · swiftlang/swift-evolution · GitHub](https://github.com/apple/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md))  
3. Xcode 15.3 이상에서 _Build Setting → Experimental Features → Move-only Types_ 를 켜면 실험 기능을 바로 시험할 수 있습니다(Swift 5.10+ 기본 활성).  
4. 컴파일 오류 메시지  
   * “`value used after consume`” ➜ `consuming` 후 재사용 시도  
   * “`copy of noncopyable value used`” ➜ noncopyable 값을 묵시적으로 복사

---

### 9. 한눈에 보는 예제 Playground
```swift
//: Move-only & ownership demo (Swift 5.10+)

struct Token: ~Copyable {
    let id: UUID
    deinit { print("Token deallocated:", id) }
}

func inspect(_ t: borrowing Token) { print("Inspect", t.id) }

func finish(_ t: consuming Token) {
    print("Consume", t.id)
}

do {
    let t = Token(id: .init())
    inspect(t)        // OK
    finish(t)         // t 소유권 이동
    // inspect(t)      // ❌ compile error – value consumed
}
// scope ends – Token already deallocated by finish()

요약

  • ~Copyable = “값 자체가 유일해야 하니 복사 금지”
  • borrowing = “잠깐 빌려서 읽기만”
  • consuming = “내가 받아서 처리하고 파괴(또는 이동)”

이 세 가지로 “값이 어디서 태어나고, 언제 파괴되는지”를 코드 수준에서 명확히 표현할 수 있어, 성능·안전성 둘 다 한층 끌어올릴 수 있습니다.

반응형