기본적을 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
을 접근해 사용한다. 메모리 충돌은 메모리에 값을 할당하고 메모리 값을 접근하는 것을 동시에 수행할 때 발생한다.
예를 들어 만약 특정 물건을 구매하고 구매 총 금액을 확인하는 경우
메모리 접근이 충돌할 수 있는 상황의 특성은 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 파라미터를 잘못 사용할 때 발생할 수 있다.
var stepSize = 1
func increment(_ number: inout Int) {
number += stepSize
}
increment(&stepSize)
// Error: conflicting accesses to stepSize
increment
의 파라미터로 inout Int
의 number
를 사용한다. 그리고 함수 내부에서 인자로 사용한 number
를 변경한다. 이 경우 인자로 number
를 넣고, 또 number
를 읽어 그 number
에 stepSize
를 추가해 다시 할당하는 쓰기와 읽기가 아래 그림과 같이 동시에 발생해 접근 충돌이 일어난다.
이 문제를 해결하기 위한 한가지 방법은 stepSize
의 복사본을 명시적으로 사용하는 것이다.
아래 코드와 같이 stepSize
를 복사한 copyOfStepSize
를 사용하면 하나의 메모리를 읽고 쓰는 행위를 동시에 하지 않게 돼 접근 충돌을 피할 수 있다.
// Make an explicit copy.
var copyOfStepSize = stepSize
increment(©OfStepSize)
// 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
타입의 teammate
는 inout
파라미터로 지정하고 동작은 현재 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
이 경우 oscar
와 maria
둘 다 다른 구조체 인스턴스이기 때문에 체력을 공유해도 아래와 같이 아무 문제가 없다.
하지만 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
}
구조체에서 프로퍼티를 접근하는데 오버레핑 접근으로부터 안전한 상황은 다음과 같다.
만약 컴파일러가 접근이 안전하다고 판단하지 못하면 접근이 불가능하다.
Swift는 잘 모르지만 신기하네요... mutating이라는 키워드는 언제 쓰이는 걸까요?