티스토리 뷰

 

 

오늘은 Swift Ownership에 대해서 공부해보고자 한다.

Swift Ownership이 등장하게 된 배경에는 값 타입, 참조타입, 얕은복사, 깊은 복사, Copy on Write 라는 개념이 함께 등장하게 되는데 왜 나오게 된 건지, 뭘 하고자 하는건지 알아보고자 한다.

(제가 공부하기 위해 작성한 것이므로 틀린 정보가 있다면 알려주세요!)

 

 

📚 들어가기 전

Swift에 대해 처음 공부하고자 하면 구조체는 값 타입, 클래스는 참조타입 이라는 말을 영어단어 암기 하듯이 반복적으로 들어봤을 것이다.

 

값 타입과 참조타입의 동작 원리를 아래 예제로 간단하게 살펴보자!

struct Player {
    var icon: String
}

func test() {
    let player1 = Player(icon: "📍")
    var player2 = player1
    player2.icon = "😬"
    assert(player1.icon == "📍"
}

 

구조체는 값타입이기 때문에 player1의 복사본을 만들어 변수를 복사하면 그 content가 복사되는 방식이다. 그래서 player2의 값을 변경하면 player1로부터 독립적인 player를 변경하게 된다. 

 

swift는 구조체 값타입, 배열 구조체, 배열 컬렉션 타입과 같은 값 타입에 Copy On Write가 최적화되어 있다. 

 

📍Copy On Write란? 

배열이나 딕셔너리, Set 처럼 CollectionType에서 복사가 이루어질 때, 실제로 복사를 하지 않고 원본을 공유하다가 원본 또는 복사한 곳에서 수정이 일어나게 될 경우 실제로 복사를 하는 작업을 의미한다.
값에 대한 수정이 발생하지 않는다면 같은 메모리를 공유하고 있기 때문에 성능을 향상 시킬 수 있다.

 

즉 값타입은 깊은 복사 방식(Deep Copy)의 성격을 가진다. 객체와 그 내부의 모든 데이터를 완전히 복제하는 방식을 의미하며 위 예제 코드 처럼 복사본은 원본과 독립적인 메모리 공간을 가지므로 한쪽을 수정해도 다른 쪽에 영향을 주지 않는다.

 

원본과 복사본이 완전히 독립적이므로 값 타입인 구조체와 열거형이 기본적으로 깊은 복사를 수행하는 방식이며 이를 통해 swift는 Copy On Write 최적화를 통해 성능을 높인다.

 

 

class PlayerClass {
    var icon: String
    init(_ icon: String) { self.icon = icon }
}

func test() {
    let player1 = Player(icon: "📍")
    var player2 = player1
    player2.icon = "😬"
    assert(player1.icon == "📍"
}

 

클래스는 참조타입이기 때문에 객체가 별도로 할당되어 데이터를 저장하는 방식이다. 즉 player2가 player1을 복사할 때 객체 자체가 아닌 참조가 복사되는 방식이다. 

 

이는 얕은 복사(Shallow Copy)의 성격을 띈다. 얕은 복사는 복사된 객체와 원본 객체가 동일한 내부 데이터(참조)를 공유하는 방식이며 내부 객체는 새로 생성되지 않고 원본 객체의 참조를 그대로 가르킨다. 

 

따라서 player1이 변경되면 원본 객체의 내부 데이터가 변경되는 것이기 때문에 복사본인 player2도 영향을 받는다. 

클래스에서도 init을 통해 깊은 복사처럼 동작하게 할 수 있지만 개발자가 직접 깊은 복사를 구현해야 하고 이를 위해서는 클래스 인스턴스 (참조타입)에 대해 깊은 복사를 구현할 수 있도록 도와주는 프로토콜인 NSCopying 프로토콜을 사용해야 한다는 단점이 있다.

 

 

Swift에서는 ARC로 클래스 메모리를 관리해 왔지만, Copy On Write의 값 타입도 참조 카운팅·유일성 검사 오버헤드와 암묵적 복사 비용이 있었다. Swift는 이를 명시적으로 제어하기 위해 Ownership을 도입했고, 그 결과로 복사가 불가능한 구조체가 deinit을 가질 수 있게 되었다.

 

Copyable

값을 암시적으로 또는 명시적으로 복사할 수 있는 형식을 의미하며 이 프로토콜을 준수한다는 것은 해당 타입의 값을 복사할 수 있음을 의미한다.

 

 

 

swift 5.9+에서부터 모든값 타입은 명시적으로 선언하지 않아도 copyable 특성을 가지도록 한다. Copyable 을 채택하겠다고 개발자가 직접 선언해주지 않아도 swift에서 내부적으로 값 타입(struct, enum, etc)에서 내부적으로 copyable 프로토콜을 채택하고 있다.

이는 값 타입의 복사 가능 여부를 명시적으로 제어하기 위해 사용된다.

 

WWDC 세션에 의하면 Swift에서는 모든 개발자가 복사를 필요로 한다고 가정하여 모든 값 타입에 Copyable 프로토콜을 채택하도록 한다고 한다.

 

반면 특정 상황에서는 복사가 존재하면 안되는 경우도 존재한다. 예를 들어 은행 이체 시스템이 있다고 가정했을떄 같은 이체를 복사본이 여럿 있는 상태에서 두 번 실행하거나 취소를 빠뜨리는 경우가 발생할 수 있다. 

 

이때는 복사를 진행하면 안되는 경우이며 이때 ~Copyable을 채택하여 사용해야 한다.

 

 

~Copyable

컴파일러가 복사 동작을 방지하도록 보장한다. 이런 타입을 NonCopyable Type 또는 Move-Only Type이라고 부른다.

 

NonCopyable이라고 부르며 표시는 ~Copyable로 한다. ~Copyable 제약을 사용하면 해당 타입이 복사를 불가능하도록 설정한다는 의미이며 주로 성능 최적화나 데이터 무결성을 보장하여 복사로 인한 의도치 않은 중복을 막기 위해 사용한다.

 

위에서 예시를 든 것 처럼 은행 이체라던가 대용량 데이터나 고유한 리소스 관리 처럼 구조체가 복사되면 안되는 경우에 ~Copyable을 명시하여 복사를 방지한다.

 

struct Dog {
    let name: String
    let age: Int
}
...
class OwnershipViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let dog = Dog(name: "Amy", age: 1)
        let new = dog // 복사가 아닌 소유권 이동
         
        print(new.name)          // OK
        print(dog.name)          // 컴파일 에러: 'dog' used after consume
        
        fetchInfo()
    }
    ...
 }

 

Dog에 ~Copyable을 붙이면 let new = dog는 복사가 아니라 소유권 이동으로 처리되어 오류가 발생하게 된다.

 

뷰 생명주기 관점에서 보면 new라는 변수에 dog를 복사하는 거 자체가 아무런 문제가 발생하지 않지만 그 이후에 struct 내부에서 deinit이 이미 처리된 상황인데 그 이후에 dog의 값에 접근하게 되면 에러가 나게 된다. 

 

이 경우를 소유권 이동이 허용되는 타입임을 나타낸다.

📍소유권 이동이란?

값의 소유권이 한 변수에서 다른 곳으로 넘어가고, 원래 변수는 더 이상 사용할 수 없게 되는 것이다. ~Copyable 타입은 복사가 불가능하고 이동만 가능해서 Move-Only Types이라고도 부른다.
주로 구조체와 열거형에 적용된다. 클래스는 이미 참조기반으로 동작하므로 ~Copyable을 사용하지 않기 때문에 적용되지 않는다. 

값의 복사로 인한 오버헤드를 줄이고 데이터 무결성을 유지하며 고유한 리소스 관리나 성능을 최적화하는데 이점이 있다.

📌 ~Copyable이 가지는 이점

- 강제화를 하는 것이기 때문에 private, final와 같이 최적화적인 측면에서 고려해볼 수 있다.
- 데이터 무결성을 보장하기 때문에 의도치 않은 중복을 방지한다.
- 고유한 리소스를 관리한다.
- 구조체에 deinit을 선언할 수 있다.

 

정리해보자면 ~Copyable 프로토콜을 채택한다는 것은 빌드하기 전에 컴파일러에서 이 타입은 복사가 불가능한 타입이다라고 강제할 수 있는 수단이 된다.

 

 

 

소유권 종류에는 4가지가 존재한다. 하나씩 알아가보도록 하자.

consume

Move-only type은 복사를 금지하고 소유권에 대한 이동만 허용하는 타입이다. (~Copyable 제약이 적용된 타입)

 

앞서 코드에서 let new = dog에서 이미 소유권이 넘어간 상태이다. 따라서 이미 소유권이 넘어갔는데 print로 dog의 값에 접근을 하려고 하니까 에러가 났던건데 consume은 이런 상황을 해결해준다.

class OwnershipViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let dog = Dog(name: "Amy", age: 1)
        
        print(dog.name)
        
        // 복사하지 못하게 하는 ~Copyable을 하게 되면 오류남, 이 코드 자체가 이미 복사를 하는 과정이니까 충돌
        let new = consume dog 
         
        print(new.name)
        
        fetchInfo()
    }
    ...
  }​

 

consume은 소유권을 명시적으로 이동시키기 위해 사용되는 연산자이며 값이 한 변수에서 다른 변수로 이동하며 원래 변수는 사용 불가능한 상태가 된다. 

 

따라서 consume을 사용하면 소유권을 명시적으로 이동하도록 하여 print로 복사된 값인 new에 접근을 했을때 에러가 나지 않게 되며 컴파일러가 소유권 이동을 명확히 추적할 수 있도록 한다.

 

consuming

consume과 비슷한 개념으로 함수의 매개변수 선언에서 사용되는 소유권 지정자

 

~Copyable 매개변수의 경우에도 어떤 값을 복사하는지 모르기 때문에 함수가 해당값에 대해 어떤 소유권을 갖는지 선언해야 한다.

 

func introduce(user: consuming Person) {
    print(user.name, user.age)
}

 

consume과 거의 동일한 개념으로 함수의 매개변수에서 사용할 때는 consuming으로 사용한다.

 

하지만 매개변수가 아닌 메서드 앞에서도 사용할 수 있다. 

consuming func introduce(user: Person) {
    print(user.name, user.age)
}

 

메서드 선언 앞에 consuming을 붙이면 그 메서드가 호출될 때 호출 대상인 self의 소유권을 메서드가 넘겨받아 소비한다는 의미로 해석된다. 즉 ~Copyable 타입의 메서드가 호출되면 해당 객체가 이동되어 더이상 원래 컨텍스트에서 사용할 수 없게 된다.

 

borrowing

함수 매개변수에서 사용되며 값의 소유권을 유지한 채 읽기 전용 접근을 제공한다.

 

borrowing은 move-only type의 값을 복사하거나 소유권을 이동시키지 않으며 borrowing을 사용하면 함수가 값을 빌려 읽기 전용으로 접근할 수 있으며 호출 후 원래 변수는 여전히 유효하도록 한다. 

 

쉽게 말하면 값을 가져가는 것이 아니라 잠깐 빌려서 사용하는 방식이다.

func introduce(user: borrowing Person) {
    print(user.name, user.age)
}

 

 

마찬가지로 borrowing도 파라미터에 전달해서 사용하게 된다. 즉 print와 같이 값을 변경하거나 소비하지 않는 경우 borrowing을 사용할 수 있다.

 

~Copyable 타입은 복사할 수 없으므로 매개변수의 소유권 규약(borrowing, consuming, inout)을 반드시 명시해야 한다. WWDC24 영상의 FloppyDisk 예제도 같은 이유로, 디스크를 포맷하는 함수에서 소유권을 명시하는 과정을 보여준다. 

 

consuming과 마찬가지로 메서드 선언 앞에 borrowing을 붙일 수 있다. 

선언 self 접근 호출 후 원본
borrowing func 읽기만 가능, 소비와 수정 불가 유효
mutating func 독점 접근으로 수정 가능 유효
consuming func 소유권을 가져감, 내부에서 수정 가능 사용 불가

 

함수 앞에 선언될 수 있는 것들에 대한 차이를 비교해보았다.

 

borrowing은 self를 빌려 읽기만 하는 메서드이기 때문에 consuming과는 달리 호출 후에도 원래 변수가 유효하다는 차이점이 있다.

Copyable 타입은 별도 표기가 없으면 대부분 borrowing으로 동작한다.

 

 

deinit

 

클래스의 deinit은 그 객체를 가리키는 모든 참조가 사라져야 실행되는 방식이다.

 

참조가 몇 개 존재하고 언제 사라지는지는 코드 전체의 흐름에 달려 있어서, deinit이 정확히 언제 실행될지 코드만 보고 보장하기 어렵다. ~Copyable 타입은 복사본이 존재할 수 없어서 소유자가 항상 하나이고, 그 소유자의 수명이 끝나는 시점(스코프 종료 또는 consume)에 deinit이 실행된다.

struct Person: ~Copyable {
    let name: String
    let age: Int
    
    deinit {
        print("Deinit", name)
    }
}

 

따라서 deinit을 통해 이 시점에 실행할 정리 작업을 정확하게 정의할 수 있다.

클래스의 deinit처럼 정리 작업을 맡기되, 클래스와 달리 힙에 객체를 할당하지 않는다는 특성을 가지고 있다.

 

마무리

Swift Ownership에 대해서 알아봤다. 해당 내용을 공부하기 전에는 공식 외우듯이 값 타입 (struct, array, enum...)은 깊은 복사가 이루어진다. 참조타입은 클래스이고 얕은 복사가 이루어진다.라고만 알고있고 그 안에 있는 원리를 이해하지 못했는데 이번 기회에 swift ownership을 공부하면서 값타입과 참조타입의 원리와 확장개념까지 이해하게 되었다.

 

하지만 해당 개념을 공부하면서 ~Copyable 타입을 직접 정의해볼 수 있는건 주로 자원 관리 측면이나 라이브러리 수준에서 사용해볼 수 있을 거 같아서 일반적인 개발에서 해당 개념들을 도입해서 사용해보긴 어려울 거 같다는 생각이 들었다. 그래도 아는 만큼 보인다고. . 공부해놓으면 접목시키는건 쉬우니까 . .!!!


참고

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

 

Consume noncopyable types in Swift - WWDC24 - Videos - Apple Developer

Get started with noncopyable types in Swift. Discover what copying means in Swift, when you might want to use a noncopyable type, and how...

developer.apple.com

 

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

https://developer.apple.com/documentation/Swift/Copyable
https://developer.apple.com/videos/play/wwdc2025/274/?time=712

 

'iOS > Swift' 카테고리의 다른 글

[Swift] Privacy Manifest  (0) 2026.09.24
[Swift] App Sandbox  (0) 2026.09.15
[Swift] Swift Concurrency(2) - GCD vs Swift Concurrency  (1) 2026.04.22
[Swift] Swift Concurrency(1) - 동시성  (1) 2026.04.21
[swift] Swift Concurrency - nonisolated  (0) 2025.12.16
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/10   »
일 월 화 수 목 금 토
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함