Swift: 소유권 키워드(~Copyable)

틀틀보·2026년 1월 17일

Swift Concurency

목록 보기
9/11
post-thumbnail
struct SecretMessage: ~Copyable {
    private(set) var content: String
    private(set) var isEncrypted: Bool = false
    
    init(content: String) {
        self.content = content
    }
    
    mutating func encrypt() {
        self.content = "[Encrypted] \(self.content)"
        self.isEncrypted = true
    }
}

protocol MessageProcessable {
    func inspect(_ message: borrowing SecretMessage) -> Bool
    func encryptMessage(_ message: inout SecretMessage)
    func archive(_ message: consuming SecretMessage)
}

final class SecureMessageHandler: MessageProcessable {
    func inspect(_ message: borrowing SecretMessage) -> Bool {
        print("🔍 Inspecting message: \(message.content)")
        return !message.content.isEmpty
    }
    
    func encryptMessage(_ message: inout SecretMessage) {
        print("🔒 Encrypting message...")
        message.encrypt()
    }
    
    func archive(_ message: consuming SecretMessage) {
        print("d Archiving message: \(message.content)")
    }
}

consuming

해당 값 타입 객체의 소유권을 함수에게 넘긴다는 의미

소유권을 함수에게 완전히 넘겨 기존 호출자는 해당 값을 사용할 수 없게 함.

~Copyable에서의 consuming

protocol MessageProcessable {
    // Consume: 메시지를 전송하거나 파기함 (소유권 종료)
    func archive(_ message: consuming SecretMessage)
}

final class SecureMessageHandler: MessageProcessable {
    // consuming: 소유권을 가져와서 생명주기를 끝내거나 소유권을 가짐.
    func archive(_ message: consuming SecretMessage) {
        print("d Archiving message: \(message.content)")
        // 함수가 종료되면 message는 메모리에서 해제
        // 여기서 해당 클래스 내 변수에 저장하면 해제 X 및 소유권 보유
    }
}
func performTest(processor: MessageProcessable) {
    var message = SecretMessage(content: "Top Secret Codes")
    
    // 소유권을 완전히 processor에게 넘김.
    processor.archive(message)
    
    // 소유권이 넘어갔으므로 에러 발생
    // 이제 message 변수에는 접근 불가
    // processor.inspect(message) // Error: 'message' used after consume
}
  • consuming 키워드로 정의된 매개변수에 객체를 넣으면 해당 함수에게 객체의 소유권이 넘어감.

  • 기존 해당 객체의 소유권을 지닌 변수는 재접근이 불가.

Copyable에서의 consuming

struct LargeImage {
    var data: [UInt8]
    var metadata: String
    
    init() {
        self.data = Array(repeating: 0, count: 10 * 1024 * 1024) 
        self.metadata = "High Res Photo"
    }
}
class ImageService: ImageProcessable {
    // 기존 소유자가 더 이상 이 값을 안 쓸 때, 소유권을 가지게 됨.
    func upload(_ image: consuming LargeImage) {
        print("☁️ Uploading data...")
    }
}

func performanceTest() {
    let service = ImageService()
    let myPhoto = LargeImage()
    
    
    // Consuming
    // Copyable이므로 복사 발생
    service.upload(myPhoto) 
    
    // 여전히 사용 가능
    print("📸 Still have photo: \(myPhoto.metadata)")
    
    // 마지막 사용 시점에 자동으로 최적화
    service.upload(myPhoto) // 여기서 myPhoto가 마지막으로 쓰인다면 최적화 수행 (소유권 넘기기)
}
  • 기존 소유자가 consuming으로 소유권을 넘겼으나, 후에 재사용되면 복사된 형태로 값을 넘김.

  • 마지막으로 재사용되면 컴파일러가 해당 값의 소유권을 마지막으로 consuming한 함수에게 넘김.

  • 즉, 여러 번 consuming 되면 마지막이 원본을 갖고, 앞의 함수들은 복사된 값을 가짐.

borrowing

소유권을 넘기지 않고 읽기 권한만 넘기기

호출된 함수에게 읽기 권한만을 주어 읽기만 가능

~Copyable에서의 borrowing

protocol MessageProcessable {
    // Borrow: 내용을 확인만 함 (소유권 유지)
    func inspect(_ message: borrowing SecretMessage) -> Bool
}

final class SecureMessageHandler: MessageProcessable {
    // borrowing: 원본에 영향을 주지 않고 읽기만 수행
    func inspect(_ message: borrowing SecretMessage) -> Bool {
        print("🔍 Inspecting message: \(message.content)")
        return !message.content.isEmpty
    }
}
func performTest(processor: MessageProcessable) {
    var message = SecretMessage(content: "Top Secret Codes")
    
    // 소유권은 여전히 'performTest' 함수에 있음
    let isValid = processor.inspect(message)
    assert(isValid, "Message should be valid")
  • 읽기 권한만 주고 소유권은 여전히 기존 소유자에게 있음.

Copyable에서의 borrowing

struct LargeImage {
    var data: [UInt8]
    var metadata: String
    
    init() {
        self.data = Array(repeating: 0, count: 10 * 1024 * 1024) 
        self.metadata = "High Res Photo"
    }
}
class ImageService: ImageProcessable {
	// borrowing을 쓰지 않은 함수에서는 값 복사 발생 가능성
    // 값을 복사하지 않고 포인터로 참조만 하여 오버헤드 0
    func analyze(_ image: borrowing LargeImage) {
        // image.metadata = "New" // Error: borrowing은 수정 불가
        print("🔍 Analyzing \(image.data.count) bytes without copying...")
    }
}

func performanceTest() {
    let service = ImageService()
    let myPhoto = LargeImage()
    
    // 복사 없이 메모리 주소만 참조해서 읽음
    service.analyze(myPhoto)
}
  • 기존 방식에서 값 타입을 넘기면 해당 값이 복사되어 함수로 넘겨가던 방식

  • borrowing 키워드로 값을 복사하지 않고 포인터로 참조만하여 읽기 가능

inout

소유권을 대여하여 해당 값을 수정

소유권을 잠시 가져 수정 권한을 얻는 것

~Copyable에서의 inout

protocol MessageProcessable {
    // Mutate (inout): 내용을 변경함 (소유권 대여 후 반환)
    func encryptMessage(_ message: inout SecretMessage)
}

final class SecureMessageHandler: MessageProcessable {
    // inout: 값을 변경할 수 있음
    func encryptMessage(_ message: inout SecretMessage) {
        print("🔒 Encrypting message...")
        message.encrypt()
    }
}
func performTest(processor: MessageProcessable) {
    var message = SecretMessage(content: "Top Secret Codes")
    
    // 소유권을 잠시 processor에게 빌려주고, 수정된 상태로 돌려받음
    processor.encryptMessage(&message)
  • 우리가 사용하던 inout 방식과 동일

  • 호출한 함수가 끝날 때까지 해당 값의 배타적 접근 보장 필요

  • 해당 값의 원본 그 자체를 가져와 수정하고 돌려주는 방식

Copyable에서의 inout

  • ~Copyable에서는 원본 자체를 가져와 수정했다면 Copyable은 복사본을 수정한 뒤에 복사본을 원본에 덮어쓰는 방식

참고
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0377-parameter-ownership-modifiers.md

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0427-noncopyable-generics.md

https://developer.apple.com/videos/play/wwdc2024/10170/

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/memorysafety/

https://developer.apple.com/videos/play/wwdc2023/10164/

profile
안녕하세요! iOS 개발자입니다!

0개의 댓글