Swift - 메모리 안정성

임성빈·2022년 4월 22일
1

Swift

목록 보기
24/26
post-thumbnail
post-custom-banner

메모리 안정성


기본적을 Swift는 코드가 비정상적으로 동작하는 것을 막는다. 예를 들면, 변수가 사용되기 전에 초기화 된다던가, 메모리에서 해제된 값을 접근하는 것을 막는다던가, 배열의 인덱스 한계를 넘는지 확인하는 것들이 있다.

Swift는 또한 메모리 내의 위치를 수정하는 코드를 요구하여 동일한 메모리 영역에 대한 다중 액세스가 충돌하지 않도록 한다. Swift는 메모리를 자동으로 관리하기 때문에 대부분의 경우 메모리에 액세스할 생각을 전혀 하지 않아도 된다. 그러나 잠재적인 충돌이 발생할 수 있는 위치를 이해하는 것이 메모리에 대한 액세스 권한이 충돌하는 코드를 작성하는 것을 피할 수 있으므로 중요하다. 코드에 충돌이 있으면 컴파일 시간 또는 런타임 오류가 발생한다.


메모리 접근 충돌의 이해

코드에서 메모리 접근은 아래 예와 같이 변수에 값을 할당하거나 접근할 때 발생한다.

// 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)!")

위 코드는 one 이라는 변수에 1을 할당하고 print 하기 위해 one 을 접근해 사용한다. 메모리 충돌은 메모리에 값을 할당하고 메모리 값을 접근하는 것을 동시에 수행할 때 발생한다.

예를 들어 만약 특정 물건을 구매하고 구매 총 금액을 확인하는 경우

  • Before의 기본 상태에서 Total에 접근해 값을 가져 왔다면 Total은 5$가 된다.
  • 만약 TV와 T-shirt를 구매하는 동안(During) Total에 접근해 값을 가져왔다면 Total은 5$가 된다.
  • 하지만 실제 제대로 된 값은 Afer의 320$이어야 할 것이다.

메모리 접근의 특성

메모리 접근이 충돌할 수 있는 상황의 특성은 3가지 경우가 있다.
1. 메모리를 읽거나 쓰는 경우
2. 접근 지속시간 그리고 메모리가 접근되는 위치
3. 메모리가 접근되는 위치

구체적으로 메모리 충돌은 다음 3가지 조건 중 2가지를 만족하면 발생한다.

  • 최소 하나의 쓰기 접근 상황
  • 메모리의 같은 위치를 접근할 때
  • 접근 지속시간이 겹칠 때

읽기 접근과 쓰기 접근의 차이는 분명하다. 쓰기 접근은 메모리의 위치를 변경하지만 읽기는 그렇지 않다. 메모리의 위치는 무엇을 참조하고 있는지 나타낸다.

메모리 접근의 지속시간은 즉각적인 접근과 장기적인 접근으로 구분할 수 있다. 즉각적인 접근은 코드에서 메모리 접근이 시작되고 끝나기 전에 그 메모리에 대한 다른 접근이 시작될 수 없을 때를 의미한다.

아래 즉각적인 접근의 예제이다.

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

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

위 코드에서는 메모리 접근의 충돌이 발생하지 않는다.


In-Out 매개 변수에 대한 접근 충돌

메모리 충돌은 in-out 파라미터를 잘못 사용할 때 발생할 수 있다.

var stepSize = 1

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

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

increment 의 파라미터로 inout Intnumber 를 사용한다. 그리고 함수 내부에서 인자로 사용한 number 를 변경한다. 이 경우 인자로 number 를 넣고, 또 number 를 읽어 그 numberstepSize 를 추가해 다시 할당하는 쓰기와 읽기가 아래 그림과 같이 동시에 발생해 접근 충돌이 일어난다.

이 문제를 해결하기 위한 한가지 방법은 stepSize 의 복사본을 명시적으로 사용하는 것이다.

아래 코드와 같이 stepSize 를 복사한 copyOfStepSize 를 사용하면 하나의 메모리를 읽고 쓰는 행위를 동시에 하지 않게 돼 접근 충돌을 피할 수 있다.

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

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

또 다른 in-out 파라미터의 장기 접근으로 인한 충돌은 다음과 같은 상황에서 일어날 수 있ㄲ다. balance 라는 함수에 inout 파라미터 2개를 입력하는데 그 입력한 파라미터를 갖고 읽고 쓰는 것과 관련한 연산을 한다.

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(&playerOneScore, &playerTwoScore) 와 같이 인자를 넣으면 읽기와 쓰기를 동시에 하게 돼서 접근 충돌이 발생한다.


메소드에서 자신에 대한 접근 충돌

메소드 안에서 self에 접근할 때 충돌이 발생할 수도 있고 구조체의 변환 메소드는 메소드 호출 기간 동안 자신에 대한 쓰기 접근 권한을 가진다.

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

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

이 구조체를 확장해 Player 간에 체력을 공유하는 shareHealth 함수를 익스텐션에서 선언한다. Player 타입의 teammateinout 파라미터로 지정하고 동작은 현재 Player 와 입력한 teamamte Player 간에 balance 함수를 실행한다. 여기서 사용하는 인자는 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

이 경우 oscarmaria 둘 다 다른 구조체 인스턴스이기 때문에 체력을 공유해도 아래와 같이 아무 문제가 없다.

하지만 oscar 를 자신과 같은 인스턴스인 oscar 와 체력을 공유한다고 실행하면 체력을 읽어오고 읽어온 체력을 변경하는 동작을 한 메모리 위치에서 동시에 수행하게 돼서 충돌이 발생한다.

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


프로퍼티에 대한 접근 충돌

프로퍼티 접근에도 충돌이 발생할 수 있다.

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

읽기 쓰기를 동시에 수행하는 balance 를 실행할 때 충돌 에러가 발생한다.

Player 의 경우에도 전역변수로 선언돼 있어서 balance 수행시 충돌 에러가 발생한다.

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

하지만 지역변수에서 balance 를 수행하는 경우 충돌 에러가 발생하지 않는다.

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

구조체에서 프로퍼티를 접근하는데 오버레핑 접근으로부터 안전한 상황은 다음과 같다.

  • 구조체 인스턴스에서 저장 프로퍼티에만 접근하고 계산된 프로퍼티 혹은 클래스 프로퍼티에 접근하지 않을 때
  • 구조체가 전역변수가 아닌 지역변수일 때
  • 구조체가 어떤 클로저로부터도 캡쳐하지 않거나 nonescaping 클로저에서만 획득된 경우

만약 컴파일러가 접근이 안전하다고 판단하지 못하면 접근이 불가능하다.

profile
iOS 앱개발
post-custom-banner

1개의 댓글

comment-user-thumbnail
2022년 5월 11일

Swift는 잘 모르지만 신기하네요... mutating이라는 키워드는 언제 쓰이는 걸까요?

답글 달기