티스토리 뷰

 

Memory Safety — The Swift Programming Language (Swift 5.5)

Memory Safety By default, Swift prevents unsafe behavior from happening in your code. For example, Swift ensures that variables are initialized before they’re used, memory isn’t accessed after it’s been deallocated, and array indices are checked for

docs.swift.org

 

 

 

 

Memory Safety

기본저긍로 Swifr는 코드에서 안전하지 않은 동작을 예방합니다. 예를 들어 Swift는 변수는 사용되기 전에 초기화되며,  메모리는 할당해제 된 후 접근할 수 없고 배열 인덱스에서 잘못된 오류가 있는지 확인합니다.

 

Swift는 같은 메모리에 대한 여러개의 접근이 충돌을 일으키지 않게 합니다.  메모리 내에서 위치를 수정하는 코드가 해당 메모리를 독점적으로 접근하도록 요구해서 말입니다.

Swift는 메모리를 자동으로 관리하기 때문에 대부분의 경우 메모리 접근에 대해 생각할 필요가 없습니다. 그러나 어디서 잠재적 충돌이 일어날지 이해하고 있는 것은 중요합니다. 그래야 메모리 충돌을 일으키는 코드작성을 피할 수 있기 때문입니다. 코드가 충돌을 포함한다면 컴파일 에러나 런타임 에러가 발생할 것입니다.

 

 

 

 

Understanding Conflicting Access to Memory (메모리 접근에 대한 충돌 이해)

코드에서 변수 값을 설정하거나 함수에 인수를 전달할 때 메모리에 접근합니다.

아래 코드는 읽기 접근(read access) 와 쓰기 접근(write access)를 포함합니다.

// A write access to the memory where one is stored.
var one = 1

// A read access from the memory where one is stored.
print("We're number \(one)!")

메모리 접근에 대한 충돌은 코드의 다른 곳에서 같은 시간에 같은 메모리에 접근하려고 시도하면 발생할 수 있습니다. 같은 시간에 메모리에 대한 여러 개의 접근은 예측할 수 없거나 일관성 없는 동작을 만들어 냅니다. Swift에서는 코드의 여러줄에 걸쳐 있는 값을 수정하기 위한 방법들이 있습니다. 자체 수정 중에 값에 대한 접근을 시도할 수 있습니다.

 

종이에 적혀있는 가계부를 정리하는 것과 비슷한 문제라고 생각하면 됩니다. 가계부를 업데이트하는 것은 두 가지 단계를 걸칩니다. 첫째로 항목의 이름과 가격을 적습니다. 그 다음 현재 리스트에 있는 항목을 기반으로 총액을 수정합니다. 업데이트 전 후로 가계부에서 모든 정보를 읽을 수 있고, 정확한 답을 얻을 수 있습니다. 아래 그림처럼 말입니다.

출처 : Swift 공식 문서

순간이지만 가계부에 항목을 추가하는 동안 유효하지 않은 상태입니다. 새로 추가된 항목을 반영해서 총액이 업데이트되지 않았기 때문입니다. 항목을 추가하면서 총액을 읽는 것은 부정확한 정보를 전달합니다.

 

이 예제에서 해결해야할 또 다른 과제는 메모리 접근에 대한 충돌을 수정할 떄 있습니다. 다른 답을 만드는 충돌을 수정하기 위해 여러가지 방식이 있습니다. 어떤 답이 옳은 지는 항상 분명하지 않습니다. 예제에서 사용자가 기존의 총액을 원하는지 업데이트 된 총액을 원하는지에 따라 답이 달라집니다. 접근 충돌을 수정할 수 있기 전에 어떤 의도를 가지는지 결정해야 합니다.

 

동시적이고 멀티 쓰레드를 사용하는 코드를 작성했다면, 메모리 접근에 대한 충돌은 아마 친숙한 문제 일 것입니다. 그러나 여기서에서 다루는 충돌 접근은 싱글 스레드로 국합됩니다.

싱글 스레드 내에서 메모리에 대한 액세스 권한이 충돌하는 경우, Swift는 컴파일-타일 에러나 런타임 에러가 발생하도록 보장합니다.

 

 

 

 

Characteristics of Memory Access (메모리 액세스(=접근)의 특성)

접근 충돌의 문맥에서 고려하기 위한 3 가지 메모리 접근 특성이 있습니다. 액세스가 읽기나 쓰기인지, 액세스 시간, 액세스 중인 메모리 위치. 특히 다음 조건을 모두 충족하는 두가지 액세스가 있을 경우 충돌이 발생합니다.

  • 적어도 하나는 쓰기(write) 액세스나 nonatomic access 입니다
  • 메모리의 같은 위치에 액세스합니다
  • 시간이 겹칩니다

읽기 액세스와 쓰기 액세스의 차이는 대부분 명확합니다. 쓰기 액세스는 메모리 위치를 변경하고, 읽기 액세스는 그렇지 않습니다. 메모리 위치는 무엇이 액세스되는지를 참조합니다. (변수, 상수, 프로퍼티) 

메모리 액세스 시간은 순간(instantaneous)이거나 긴 시간(long term) 입니다.

 

C atomic 연산자만을 사용한다면 연산은 atomic 입니다. 그렇지 않다면 nonatomic 입니다. 

 

액세스가 시작된 후 종료되기 전에 다른 코드를 실행할 수 없는 경우, 액세스는 instantaneous 입니다. 기본적으로 두 개의 순간적인 액세스는 같은 시간에 일어날 수 없습니다. 대부분의 메모리 접근은 instantaneous 입니다.

아래 코드의 읽기 액세스와 쓰기 액세스는 모두 instantaneous 입니다.

func oneMore(than number: Int) -> Int {
    return number + 1
}

var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"

그러나 long-term 액세스라고 메모리에 액세스하는 여러가지 방법이 있습니다. 다른 코드의 실행에 걸쳐서 작동합니다. instantaneous 액세스와 long-term 액세스의 차이는 long-term 액세스가 시작된 후 종료되기 전에 다른 코드를 실행할 수 있습니다. 이를 overlap 이라고 합니다. long-term 액세스를 다른 long-term 액세스와 instantaneous 액세스에 overlap 할 수 있습니다.

 

Overlapping액세스는 주로 함수와 메소드의 in-out 파라미터나 구조체의 중복(mutating) 메소드를 사용하는 코드에 주로 나타납니다. long-term 액세스를 사용하는 Swift 코드의 특정 유형은 아래에서 설명합니다.

 

 

 

 

Conflicting Access to In-Out Parameters (In-Out 파라미터에 대한 액세스 충돌)

함수는 자신의 모든 in-out 파라미터에 long-term 쓰기 액세스를 가집니다. in-out 파라미터에 대한 쓰기 액세스는 모든 non-in-out 파라미터가 평가되고 해당 함수 호출의 전체 기간 동안 지속된 후에 시작합니다. 만약 여러개의 in-out 파라미터가 있다면 쓰기 액세스는 파라미터가 나타나는 순서대로 시작됩니다.

 

이러한 long-term 쓰기 액세스의 한 가지 결과는 in-out으로 전달된 원본 변수에 접근할 수 없다는 것입니다. 범위 지정 규칙(scoping rule)과 액세스 컨트롤(access control)이 허용하지 않더라도 말입니다. 원본에 대한 모든 액세스로 인해 충돌이 발생합니다.

 

var stepSize = 1

func increment(_ number: inout Int) {
    number += stepSize
}

increment(&stepSize)
// Error: conflicting accesses to stepSize

위 코드에서 stepSize는 전역 변수 입니다. 일반적으로 increment(_:)에 의해 접근 가능합니다.

그러나 stepSize의 읽기 액세스는 number에 대한 쓰기 액세스와 겹칩니다(overlap). 아래 그림과 같이, numer와 stepSize 모두 메모리에서 동일한 위치를 참조합니다. 읽기 액세스와 쓰기 액세스는 같은 메모리를 참조하고 중복됩니다. 이에 따라 충돌이 발생합니다.

출처 : Swift 공식 문서

이 충돌을 해결하는 방법은 stepSize의 명시적(explicit) 복사본을 만드는 것입니다.

// Make an explicit copy.
var copyOfStepSize = stepSize
increment(&copyOfStepSize)

// Update the original.
stepSize = copyOfStepSize
// stepSize is now 2

increment(_:)를 호출하기 전에 stepSize의 복사본을 만들 때, copyOfStepSize의 값이 현재 stepSize에 의해 증가한다는 것이 명확합니다. 읽기 액세스는 쓰기 액세스가 시작하기 전에 끝나 충돌이 발생하지 않습니다.

 

in-out 파라미터에 대한 long-term 쓰기 액세스의 또 다른 결과는 "같은 함수의 여러 개의 in-out 파라미터에 대한 인수로 단일 변수를 전달"하면 충돌이 발생한다는 것이다.

func balance(_ x: inout Int, _ y: inout Int) {
    let sum = x + y
    x = sum / 2
    y = sum - x
}
var playerOneScore = 42
var playerTwoScore = 30
balance(&playerOneScore, &playerTwoScore)  // OK
balance(&playerOneScore, &playerOneScore)
// Error: conflicting accesses to playerOneScore

코드의 balance(_:_:) 함수는 두 파라미터를 수정하여 전체 값을 두 파라미터로 균등하게 나눕니다. playerOneScoe와 playerTwoScore를 함께 인수로 호출해도 충돌이 발생하지 안흡니다. 두 개의 쓰기 액세스가 중복되지만, 메모리 위치가 다른 곳을 접근합니다.

반대로 playerOneScoe를 두 개의 파라미터에 모두 전달하면 충돌이 발생합니다.  두 개의 쓰기 액세스를 동시에 같은 메모리 위치에서 수행하려고 하므로 충돌이 발생합니다.

 

연산자가 함수이기 때문에, 연산자도 in-out 파라미터에 대해 long-term 액세스를 가집니다.

예를 들어 balance(_:_:) 가 <^>라는 이름의 연산자 함수라면, playerOneScore <^> playerOneScore 는 balance(&playerOneScore, &playerOneScore)와 똑같은 충돌이 발생합니다.

 

 

 

 

 

Conflicting Access to self in Methods (메소드 내부의 self에 대한 액세스 충돌)

구조체의 중복 메소드는 메소드 호출기간 동안 self 에 대해 쓰기 액세스(write access)를 가집니다.

 

struct Player {
    var name: String
    var health: Int
    var energy: Int

    static let maxHealth = 10
    mutating func restoreHealth() {
        health = Player.maxHealth
    }
}

예제의 restoreHealth() 메소드 내부에서 self 에 대한 쓰기 액세스는 메소드가 시작할 때부터 리턴할 때까지 지속됩니다. 이 경우에는 restoreHealth() 안에 Player 인스턴스의 프로퍼티에 중복해서 접근하는 코드가 없습니다.

 

아래 예제의 shareHealth(with:) 메소드는 또 다른 Player 인스턴스를 in-out 파라미터로써 가집니다. 이에 따라 중복 접근에 대한 가능성이 생깁니다.

extension Player {
    mutating func shareHealth(with teammate: inout Player) {
        balance(&teammate.health, &health)
    }
}

var oscar = Player(name: "Oscar", health: 10, energy: 10)
var maria = Player(name: "Maria", health: 5, energy: 10)
oscar.shareHealth(with: &maria)  // OK

Oscar의 health를 Maria에게 나눠주기 위해 shareHealth(with:) 를 호출하는 것은 충돌을 발생시키지 않습니다.

메소드 호출동안 oscar 에 대한 쓰기 액세스가 있습니다. oscar는 중복 메소드 내부의 self 값이기 때문입니다.  

같은 시간동안 maria에 대한 쓰기 액세스가 있습니다. maria는 in-out 파라미터로 전달되기 때문입니다.

둘 다 다른 메모리 위치에 접근합니다. 쓰기 액세스 시간이 중복되더라도 충돌이 발생하지 않습니다.

 

다른 메모리 위치에 접근한다. 출처 : Swfit 공식문서

 

 

그러나 만약 shareHealth(wifth:)의 인수로 oscar를 전달하면 충돌이 발생합니다.

oscar.shareHealth(with: &oscar)
// Error: conflicting accesses to oscar

중복 메소드는 메소드 호출 시간동안 self에 대한 읽기 액세스를 필요로 합니다. 같은 시간동안 in-out 파라미터는 teammate 에 대한 읽기 액세스를 필요로 합니다.

이 메소드에서 self와 teammate 모두 메모리의 같은 위치를 참조합니다. 두 쓰기 액세스는 같은 메모리를 참조하고 중복됩니다. 이에 따라 충돌이 발생합니다.

같은 메모리에 접근한다. 출처 : Swift 공식 문서

 

 

 

 

 

Conflicting Access to Properties (프로퍼티에 대한 액세스 충돌)

구조체, 튜플, 열거형과 같은 타입은 개별 구성(constituent) 값으로 구성됩니다. 구조체의 프로퍼티나 튜플의 요소(elements) 같이 말입니다. 이들은 값 타입(value types)이기 때문에 프로퍼티 중 하나에 대한 읽기 액세스 또는 쓰기 액세스가 전체 값에 대한 읽기 액세스 또는 쓰기 액세스를 필요로 합니다. (값타입은 값의 일부라도 돌연변이하면 전체 값이 돌연변이됩니다.) 예를 들면 튜플의 요소에 대한 중복된 쓰기 액세스는 충돌을 발생시킵니다.

 

var playerInformation = (health: 10, energy: 20)
balance(&playerInformation.health, &playerInformation.energy)
// Error: conflicting access to properties of playerInformation

위 예제에서 튜플의 요소에 대한 balance(_:_:) 호출은 충돌을 발생시킵니다. playerInformation에 대한 쓰기 액세스가 중복되기 때문입니다.  playerInformation.health 와 playerInformation.energy 모두 in-out 파라미터로 전달됩니다. 이는 balance(_:_:) 가 함수 호출동안 이 둘에 대한 쓰기 액세스를 필요로 함을 의미합니다.

두 경우 모두 튜플 요소에 대한 쓰기 액세스가 튜플 전체에 대한 쓰기 액세스를 필요로 합니다. 이는 playerInformation에 대한 두 개의 쓰기 액세스가 같은 시간동안 중복됨을 의미합니다. 이에 따라 충돌을 발생시킵니다.

 

아래 코드는 "전역 변수로 저장된 구조체의 프로퍼티에 대한 쓰기 액세스 중복"으로 인해 같은 에러가 발생함을 보여줍니다.

var holly = Player(name: "Holly", health: 10, energy: 10)
balance(&holly.health, &holly.energy)  // Error

 

실제로 구조체의 프로퍼티에 대한 액세스는 안전하게 중복됩니다.

만약 위 예제의 holly 변수가 전역변수에서 지역변수로 바뀐다면, 컴파일러는 구조체의 저장프로퍼티에 대한 중복된 액세스가 안전하다는 것을 증명할 수 있습니다.

func someFunction() {
    var oscar = Player(name: "Oscar", health: 10, energy: 10)
    balance(&oscar.health, &oscar.energy)  // OK
}

위 예제에서 oscar의 health 와 energy는 balance(_:_:) 에 in-out 파라미터로 전달됩니다. 컴파일러는 메모리 안정성이 유지된다는 것을 증명할 수 있습니다. 두 저장 프로퍼티가 어떤 방식으로든 상호작용하지 않기 때문입니다.

 

메모리 안전을 유지하기 위해 구조체의 프로퍼티에 대한 중복 액세스를 제한하는 것이 항상 필요한 것은 아닙니다. 메모리 안전은 바람직한 보증입니다. 하지만 독점적(exclusive) 액세스는메모리 안전보다 엄격한 요구사항입니다. 이는 일부 코드가 메모리에 대한 독점적 액세스를 위반하더라도, 메모리 안전이 유지된다는 의미입니다.

Swift는 " 만약 컴파일러가 메모리에 반독점적(nonexclusive) 액세스가 여전히 안전하다는 것을 증명할 수 있다면", 이 메모리-안전 코드를 허용합니다. 특히 아래와 같은 조건이 적용될 경우, 구조체의 프로퍼티에 대한 중복 액세스가 안전하다는 것을 증명할 수 있습니다.

  • 인스턴스의 저장프로퍼티(stored properties)에만 액세스한다. 연산 프로퍼티(computed properties) 나 클래스 프로퍼티에는 안된다
  • 구조체는 지역 변수(local variable)의 값이다. 전역변수(global variable)은 안된다
  • 구조체는 어떤 클로져에도 캡쳐(captrue) 되지 않거나 nonescaping 클로져에 의해서만 캡쳐되어야 한다.

 

만약 컴파일러가 액세스가 안전한지 증명할 수 없다면, 액세스를 허용하지 않습니다.

 

 

 

Swift는 보통 메모리를 자동적으로 관리해주기 떄문에 왠만해서는 메모리 안전도 유지된다.

같은 메모리에 대해 동시에 읽기 액세스(read access) 나 쓰기 액세스(wrtie access)를 하면 메모리 충돌이 발생한다.

이를 생각하면서 코드를 작성해야 한다.

 

 

 

 

 

Memory Safety. The End. 🤯

 

 

 

 

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/06   »
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
글 보관함