study record

[Swift] consume, consuming, borrowing 본문

Swift

[Swift] consume, consuming, borrowing

asong 2025. 3. 3. 00:53

1️⃣ SE-0366 consume operator to end the lifetime of a variable binding

값을 복사하거나 참조를 넘기는 형태가 아닌, 값의 소유권을 이전

키워드 consume

  • Swift5.9에서 적용
  • 값의 소유권을 이전하여 값을 복사하거나 참조를 전달하는 방법을 사용하지 않도록 하는 성능 최적화 방법

ex) consume 키워드를 프로퍼티 앞에 사용하면, 해당 소유권을 이전하여 consume 키워드를 쓴 프로퍼티는 앞으로 사용 불가 (컴파일 에러)

useX(x)// do some stuff with local variable x// Ends lifetime of x, y's lifetime begins.let y = consume x// [1]

useY(y)// do some stuff with local variable y
useX(x)// error, x's lifetime was ended at [1]// Ends lifetime of y, destroying the current value._ = consume y// [2]
useX(x)// error, x's lifetime was ended at [1]
useY(y)// error, y's lifetime was ended at [2]
  • 프로퍼티 뿐만이 아닌 함수의 파라미터에 consume 적용이 가능
    • 파라미터에 consume이 있게되면 사용하는 쪽에서 이미 consume된 프로퍼티를 전달하려고 할때 컴파일 에러가 발생
func doStuffUniquely(with value: consume [Int]) {
// If we received the last remaining reference to `value`, we'd like// to be able to efficiently update it without incurring more copies.var newValue = consume value
  newValue.append(42)

  process(newValue)
}

func test() {
  var x: [Int] = getArray()
  x.append(5)

  doStuffUniquely(with: consume x)

// ERROR: x used after being consumed
  doStuffInvalidly(with: x)

  x = []
  doMoreStuff(with: &x)
}
  • consume 키워드와 consuming이라는 키워드가 존재
    • consuming키워드는 함수 파라미터에서 값을 받을 때 사용
    • 아래처럼 doSomethin에서 value값에 새로 값을 할당해도 컴파일 에러가 발생하지 않는데, CoW의 개념처럼 동작하는 것
    • 파라미터에 값이 넘어갈때 value = a 이렇게 동작할 때 value는 a의 값을 참조하다가 value값에 새로운 값이 어싸인이 되면(value = [1]) 그때 copy하고 새로 값을 할당하는 것
var a = [1,2,3]
doSomething(with: a)

func doSomething(with value: consuming [Int]) {
    value = [1]
}

 

Detailed design - 값 재할당

If the binding is a var, the analysis additionally allows for code to conditionally reinitialize the var and thus use it in positions that are dominated by the reinitialization. Consider the following example:

if condition {
  _ = consume x
  // I can't use x anymore here!
  useX(x) // !! ERROR! Use after consume.
  x = "SSS" O
  x.p = "" X
  x = newValue
  // But now that I have re-assigned into x a new value, I can use the var
  // again.
  useX(x) // OK
} else {
  // I can still use x here, since it wasn't consumed on this path!
  useX(x) // OK
}
// Since I reinitialized x along the `if` branch, and it was never consumed
// from on the `else` branch, I can use it here too.
useX(x) // OK

프로퍼티에 값을 할당하는 것은 안 되고,

아예 init을 하는 것은 가능하다.

 

키워드 consume

consume은 컨텍스트에 따라 의미가 달라지는 키워드(context-sensitive keyword) 로,

특정한 지역 변수(let 또는 var)나 함수 매개변수의 수명을 종료시키는 역할을 합니다.

즉, consume이 사용된 이후 해당 변수를 다시 사용하려 하면 컴파일 오류(diagnostic) 가 발생하여 코드의 안전성을 보장합니다.

이 연산자는 특히 값 소유권(ownership)을 전달해야 하는 코드에서 성능과 정확성을 보장하는 데 도움을 주며, 이를 개발자가 명확히 인지할 수 있도록 합니다.

성능 최적화 및 안전성 확보

→ 값의 복사가 아닌 이동(move)을 통해 성능 향상

 

Motivation

함수 호출 이후 이전 값이 필요하지 않는 경우

따라서 x의 소유권(ownership)을 안전하게 함수로 이동(forward)시킬 수 있다면, 다음과 같은 이점이 가질 수 있다.

  1. 불필요한 retain/release 호출 방지→ 기존의 배열 버퍼(array buffer)를 복사하지 않고 재사용할 수 있음
  2. 불필요한 Copy-on-Write 방지→ 함수 내부에서 value를 var newValue로 복사할 때 새로운 복사본을 만들지 않고 기존 버퍼를 수정할 수 있음

 

consume 연산자의 역할과 동작 방식

consume 연산자는 현재 바인딩(binding)의 값을 소비(consume)하여 해당 값이 이후 코드에서 다시 사용되지 않도록 보장하는 기능을 합니다.

  • consume이 적용될 수 있는 대상
    • let 또는 var 지역 변수 (local let, local var)
    • 함수 매개변수 (function parameter)
    • 탈출되지 않은(escaped 되지 않은) 값
    • 프로퍼티 래퍼(property wrapper)나 get/set/read/modify 등의 접근자가 적용되지 않은 값
  • consume이 하는 일
    1. 특정 변수(바인딩)의 현재 값을 소비(consume)
    2. 이후에 그 값을 다시 사용하려 하면 컴파일 오류(diagnostic error)를 발생

이렇게 하면 소유권을 안전하게 이전(forwarding) 하고 불필요한 retain/release 및 Copy-on-Write(COW) 복사를 줄일 수 있음.

 

 

consuming과 consume 쓰기

📌 consuming 키워드란?

consuming 키워드는 Swift의 소유권(ownership) 시스템에서 매개변수를 함수로 전달할 때, 해당 값을 소유하고 수정할 수 있도록 하는 키워드이다.

  • 기본적으로 Swift는 함수에 값을 전달할 때 copy-on-write를 사용하여 불필요한 복사를 줄인다.
  • 하지만, consuming을 사용하면 명시적으로 값의 마지막 소유권(last ownership)을 가져와서, 추가 복사 없이 직접 수정할 수 있다.
  • 즉, 매개변수(value)를 함수 내부에서 "유일한 소유권"으로 가져와, 새로운 복사를 방지하는 역할을 한다.

 

📌 함수 안에서 consume을 사용해야 하는 이유

 consuming을 사용하면 함수 매개변수(value)는 해당 함수에서 최종적으로 소유하는 값이 되지만,

 Swift의 소유권 시스템에서는 consuming으로 받은 값을 직접 변경할 수 없고, 반드시 consume을 사용하여 소유권을 가져와야 한다.

 

consume의 역할

  • consuming 매개변수는 기본적으로 읽기 전용처럼 동작한다.
  • 함수 내부에서 값을 수정하려면, consume을 사용하여 해당 값의 소유권을 가져와야 한다.
  • 이렇게 하면 복사를 방지하고, 직접 메모리에서 값을 변경할 수 있다.
doStuffUniquely(consume x)
func doStuffUniquely(with value: consuming [Int]) {
  // `value`는 `consuming`으로 소유권을 받았지만, 여전히 읽기 전용 상태
  // 즉, 직접 수정할 수 없음!

  var newValue = consume value  // ✅ `value`의 소유권을 가져옴
  newValue.append(42)           // ✅ 복사 없이 직접 값 변경 가능!

  process(newValue)             // ✅ 값이 변경된 상태에서 다른 함수에 전달
}

func doStuffUniquely(with value: consuming [Int]) {
  value.append(42)  // ❌ ERROR: consuming parameter 'value' may only be consumed once
}

func doStuff(with value: [Int]) {
    var newValue = consume value // ❌ ERROR: 'consume' can only be used with 'consuming' parameters or local variables
    newValue.append(42)
}
//  ✅ 해결 방법: consuming을 추가하거나, var로 변수를 선언한 후 사용해야 함.

 

+ inout 매개변수

함수의 매개변수를 변경 가능하게 만들어, 함수 내부에서 수정한 값이 함수 호출자의 원본 변수에도 영향을 주도록 할 때 사용됩니다. 일반적인 Swift 함수에서는 매개변수가 기본적으로 값(call by value)으로 전달되지만, inout을 사용하면 참조(call by reference) 방식으로 전달됩니다.

inout 매개변수를 사용하는 함수 호출 시, 원본 변수 앞에 &를 붙여서 참조로 전달해야 합니다.

 

 

consume 후에 다시 할당할 수 있는 케이스

consume buffer는 buffer의 소유권을 완전히 소비(consumption)하는 작업이야. 즉, 이후에는 원래 buffer를 사용할 수 없어. 하지만 중요한 점은, consume을 사용했다고 해서 buffer라는 변수가 완전히 사라지는 게 아니라, 비어 있는 상태(uninitialized)가 된다는 거야.

  • uninitialized 상태 → 변수가 초기화되지 않은 상태.
  • deinitialized 상태 → 객체가 해제(deinit)되어 메모리가 반환된 상태.

consume을 통해 소유권이 이전된 후, 해당 변수를 직접 다시 사용하면 컴파일 오류가 발생하지만, 새로운 값을 할당하면 변수를 재초기화할 수 있다.

func f(_ buffer: inout Buffer) {
  let b = consume buffer
  b.deinitialize()
  // ... write code ...
  // We re-initialized buffer before end of function so the checker is satisfied
  buffer = getNewInstance()
}

 

consume은 값 타입, 참조타입

consume은 값 타입(Struct)에도 사용할 수 있어! 하지만 Swift의 소유권(ownership) 시스템이 참조 타입(Class)보다 값 타입(Struct)에서 다르게 동작한다.

  • 구조체 (값 타입) - 값 타입이지만 consume을 사용하면 소유권을 직접 이동할 수 있음. 복사 없이 최적화 가능.
  • 클래스 - 참조 타입이므로, consume을 사용하면 기존 변수를 사용할 수 없게 됨

 

ownership 소유권이 뭘까?

Swift의 소유권(ownership) 개념은 메모리 관리와 최적화를 위해 도입된 개념으로, 변수나 객체가 어디에서 생성되고, 사용되며, 언제 해제되는지를 명확하게 정의하는 역할을 한다.

Swift는 기본적으로 ARC(Automatic Reference Counting)를 사용하여 메모리를 관리하지만, 불필요한 복사(copy)와 성능 저하를 방지하기 위해 소유권 시스템을 도입했다.

  1. 불필요한 복사 방지 → 성능 최적화
    • 값 타입을 복사할 필요 없이 consume을 사용하면 메모리와 성능을 최적화할 수 있음.
  2. 메모리 안정성 보장
    • 참조 타입(Class)에서는 ARC를 통해 자동으로 메모리를 관리하지만,잘못된 접근(순환 참조 등)이 발생하면 메모리 누수가 생길 수 있음.
    • 값 타입(Struct)에서는 불필요한 복사를 방지하는 것이 중요함.
  3. Swift가 소유권을 자동으로 체크
    • 소유권이 사라진 변수를 잘못 사용하면, Swift가 컴파일러 단계에서 잡아줌 → 안정성 향상!

 

2️⃣ SE-0377 borrowing and consuming parameter ownership modifiers

Introduction

우리는 함수가 불변(immutable) 매개변수를 받을 때 사용할 새로운 소유권(ownership) 수정자 borrowing  consuming을 제안합니다. 이를 통해 개발자는 함수가 매개변수를 받을 때 어떤 소유권 규칙을 사용할지를 명시적으로 선택할 수 있습니다.

이러한 수정자를 매개변수에 적용하면, 해당 매개변수는 더 이상 암묵적으로 복사될 수 없으며, 만약 복사가 필요하다면 새로운 copy x 연산자를 사용하여 명시적으로 복사를 수행해야 합니다.

이 기능을 도입하면 불필요한 ARC 호출이나 복사의 횟수를 줄여 성능을 최적화할 수 있으며, noncopyable(복사 불가능) 타입에서 함수가 값을 소비(consume)할지 여부를 명확히 지정할 수 있는 중요한 기능적 기반을 제공합니다.

 

Motivation

함수 호출 시, 호출자(caller)가 피호출자(callee)에게 객체를 전달하는 방식에는 두 가지 주요한 규칙이 있습니다.

1️⃣ Callee가 매개변수를 빌릴(Borrow) 수 있음

  • 호출자(caller)는 객체가 함수가 실행되는 동안 살아 있을 것을 보장합니다.
  • 피호출자(callee)는 이 객체를 해제할 필요가 없으며, 필요할 경우 추가적으로 retain한 후 balance를 맞춰야 합니다.

2️⃣ Callee가 매개변수를 소비(Consume)할 수 있음

  • 피호출자(callee)가 객체의 소유권을 가지며, 객체를 직접 해제하거나, 소유권을 다른 곳으로 넘기는 역할을 해야 합니다.
  • 만약 호출자(caller)가 객체의 소유권을 유지하고 싶다면, 추가적으로 retain을 수행하여 피호출자가 사용 후 소유권을 가질 수 있도록 해야 합니다.

값 타입의 경우:

  • "retain"  값을 독립적으로 복사(copy)
  • "release"  복사된 값을 소멸(destroy) 및 메모리 해제(deallocate)

Swift는 기본적으로 각 함수의 동작 방식에 따라 적절한 소유권 규칙을 자동으로 선택합니다.

  • 초기화(initializer) 및 프로퍼티 설정자(setter)
    • 보통 전달된 매개변수를 사용해 새로운 값을 생성하거나 업데이트하므로, 매개변수를 소비(consume)하는 방식이 더 효율적입니다.
  • 일반적인 함수
    • 보통 매개변수를 참조하여 읽기 작업을 수행하므로, 빌리는(borrowing) 방식이 더 적절합니다.

NonCopyable 타입은 이전의 소유권 규칙을 더 중요한 API 설계 요소로 만듭니다.

  • Borrowing을 사용하는 함수
    • 복사 불가능한 값을 임시로 사용하고, 이후에도 계속 사용할 수 있도록 보장함.
    • 예: 파일 핸들에서 데이터를 읽는 함수  borrow
  • Consuming을 사용하는 함수
    • 복사 불가능한 값을 완전히 소비하여, 이후에는 사용할 수 없도록 만듦.
    • 예: 파일 핸들을 닫는 함수  consume

 

protocol Ownership 서로 바꿔서 사용 가능?!

Swift의 프로토콜에서 borrowing과 consuming 매개변수 수정자 구현체(conformance)에서 변경할 수 있음.

즉, 프로토콜에서 정의한 소유권 규칙을 반드시 그대로 따를 필요는 없으며, 더 제한적인 방향으로 수정할 수 있음

값 타입에서만 적용됨…?!

protocol P {
  func foo(x: consuming Foo, y: borrowing Foo)
}

// These are valid conformances:

struct A: P {
  func foo(x: Foo, y: Foo)
}

struct B: P {
  func foo(x: borrowing Foo, y: consuming Foo)
}

struct C: P {
  func foo(x: consuming Foo, y: borrowing Foo)
}

 

borrowing

borrowing과 consuming은 이 소유권을 명확하게 제어할 수 있도록 도와주는 매개변수 수정자(parameter modifiers)

  • 값을 빌려서(read-only) 사용
  • 변경하지 않고 읽기 작업만 할 때
  • 값을 "빌려서" 사용함 → 함수 내부에서 값을 변경할 수 없음(읽기 전용).
  • 함수가 끝나면 원래 값은 그대로 유지됨.
  • Swift에서 기본적으로 함수 매개변수는 borrowing(읽기 전용) 방식으로 전달됨.
  • 소유권 ❌ 이전되지 않음
  • 복사가 수행되지 않음. 값을 빌려서 사용하는 것이므로.
    • 값 타입은 borrwing을 사용하면 복사 없이 참조로 전달. 항상은 아니고 최적화를 통해 결정.
    • 참조 타입은 동일하게 참조 방식으로 전달
class Foo {
    var value: Int
    init(value: Int) { self.value = value }
}

func printFoo(x: borrowing Foo) {
    print(x.value)  // ✅ 읽기 가능
    // x.value = 20  ❌ 변경 불가능! (borrowed 상태)
}

let foo = Foo(value: 10)
printFoo(x: foo)
print(foo.value)  // 10 (변경되지 않음)

 

consuming

  • 함수가 값을 소유(consume)하게 되며, 원래 변수는 더 이상 사용할 수 없음.
  • *값 타입(Struct)**에서 주로 사용되며, 메모리 복사를 방지하고 성능을 최적화할 수 있음.
  • 함수가 끝나면 값이 더 이상 존재하지 않거나, 다른 곳으로 이동할 수 있음.
  • 값 전달 방식
    • 참조타입 참조를 이동
    • 값타입은 복사되지 않고 소유권이 이동. 참조가 전달되는 것이 아니라 원본 값을 직접 이동시킨다.

 

 

Swift의 borrowing과 consuming에서 암묵적 복사(implicit copy) 제한

Swift에서 borrowing과 consuming을 사용하면, 해당 값이 함수 또는 클로저 내부에서 자동으로 복사되지 않도록 제한됨.

즉, 명시적으로 copy 연산자를 사용하지 않으면 암묵적인 복사가 허용되지 않음.

func foo(x: borrowing String) -> (String, String) {
    return (x, x) // ❌ 오류: `x`를 복사할 필요가 있음
}

func bar(x: consuming String) -> (String, String) {
    return (x, x) // ❌ 오류: `x`를 복사할 필요가 있음
}
func dup(_ x: borrowing String) -> (String, String) {
    return (copy x, copy x) // ✅ OK, 복사가 명시적으로 허용됨
}
func foo(x: borrowing String) {
    let y = x // ❌ 오류: `x`를 암묵적으로 복사할 수 없음
    bar(z: x) // ✅ OK, 함수에 전달하는 것은 허용됨
}

func bar(z: String) {
    let w = z // ✅ OK, `z`는 일반적인 복사 가능 값
}

 

Noncopyable과 borrowing, consuming 연관성

borrowing 에서  consuming 으로 변경하면, 그 수정자를 채택한 클라이언트 코드에서 복사가 필요한 위치가 달라질 수 있기 때문에 소스 코드가 깨질 가능성이 있습니다.

반면, consuming에서 borrowing으로 변경하는 것은 일반적으로 복사 가능한 타입에 대해 소스 코드 호환성을 유지할 수 있습니다.

하지만 해당 매개변수 타입이 복사 불가능(noncopyable)하다면, 두 방향 모두 소스 코드에 영향을 미치며, 변경이 소스 호환성을 깨뜨릴 수 있습니다.

 

 

Leaving consuming parameter bindings immutable inside the callee

consuming을 함수 내부에서 변경 가능해야 하는 이유✅ 함수가 소유권을 가지므로, 값을 변경해도 호출자에게 영향을 주지 않음.

consuming을 변경 불가능하게 하면 생기는 문제  consuming을 borrowing으로 바꿀 때 코드가 깨질 위험이 있음.
복사 불가능(Noncopyable) 타입과 consuming ✅ 호출자가 값을 더 이상 사용할 수 없으므로, 함수 내부에서 변경해도 혼란이 없음.
consuming을 borrowing으로 바꿀 경우 해결책 ✅ 지역 변수를 사용하여 기존 코드가 깨지지 않도록 수정 가능.

📌 즉, consuming 매개변수는 함수 내부에서 변경 가능해야 하며, 이를 변경 불가능하게 하면 소스 코드가 깨질 가능성이 높아진다. 

📌 복사 불가능한 타입에서는 어차피 호출자가 값을 더 이상 사용할 수 없으므로, 혼란이 발생하지 않는다.

 

 

Noncopyable 타입에서 borrowing과 consuming의 차이

  • borrowing(빌려서 사용) → 값을 빌려서 사용하며, 기존 값은 계속 사용할 수 있음.
  • consuming(소유 후 사용) → 값을 소비하여 사용하며, 기존 값은 사용할 수 없음(소멸됨).

📌 즉, borrowing을 사용하면 기존 값을 유지하면서 사용할 수 있지만, consuming을 사용하면 해당 값이 사라지기 때문에 이후에 다시 사용할 수 없다.

이러한 특성 때문에, Noncopyable 타입의 매개변수는 borrowing 또는 consuming을 명시적으로 지정해야 한다.

📌 복사 불가능(Noncopyable)한 타입의 매개변수는 반드시 borrowing 또는 consuming을 명시해야 한다!

  • 복사할 수 있는 타입(Copyable Types)에서는 borrowing과 consuming을 생략할 수 있지만, Noncopyable(복사 불가능) 타입은 기본적으로 암묵적 복사가 불가능하므로, 반드시 명시해야 한다.
  • 어떤 함수가 값을 소유(consume)하는지, 빌려서(borrow) 사용하는지 명확히 정의해야 안전한 API가 될 수 있다.

'Swift' 카테고리의 다른 글

[Swift] Noncopyable structs and enums  (0) 2025.03.03