🔥 메모리에 대한 접근 충돌 이해하기

474자
7분

코드에서 변수의 값을 설정하거나 함수에 인수를 전달할 때, 메모리에 대한 접근이 일어납니다. 다음 코드를 살펴볼까요?

// one이 저장된 메모리에 대한 쓰기 접근
var one = 1
 
// one이 저장된 메모리에 대한 읽기 접근
print("We're number \(one)!")
swift

위 코드에는 읽기 접근과 쓰기 접근이 모두 포함되어 있어요.

메모리에 대한 접근 충돌은 코드의 서로 다른 부분이 동시에 같은 메모리 위치에 접근하려고 할 때 발생할 수 있습니다. 같은 시점에 메모리의 한 위치에 여러 번 접근하면 예측할 수 없거나 일관성 없는 동작이 나타날 수 있어요. Swift에서는 값을 수정하는 과정이 여러 줄의 코드에 걸쳐 있을 수 있기 때문에, 값 자체의 수정 중간에 그 값에 접근하려는 시도가 가능합니다.

종이에 적힌 예산을 업데이트하는 과정을 생각해보면 비슷한 문제를 볼 수 있어요. 예산을 업데이트하는 것은 두 단계로 이루어집니다. 먼저 품목의 이름과 가격을 추가하고, 그 다음 현재 목록에 있는 품목을 반영하도록 총액을 변경하죠. 아래 그림처럼 업데이트 전후에는 예산에서 어떤 정보든 읽어도 정확한 답을 얻을 수 있습니다.

lecture image

하지만 품목을 추가하는 동안에는 새로 추가된 품목을 반영하도록 총액이 업데이트되지 않았기 때문에 예산이 일시적으로 무효한 상태가 됩니다. 이 과정에서 총액을 읽으면 잘못된 정보를 얻게 되겠죠.

이 예제는 메모리 접근 충돌을 해결할 때 마주할 수 있는 어려움도 보여줍니다. 충돌을 해결하는 방법이 여러 가지일 수 있고, 그에 따라 다른 답이 나오는데 어떤 답이 맞는지 명확하지 않은 경우가 있어요. 이 예제에서는 원래 총액을 원하는지, 아니면 업데이트된 총액을 원하는지에 따라 $5나 $320 둘 다 정답이 될 수 있습니다. 접근 충돌을 해결하기 전에, 코드가 의도한 바가 무엇인지 파악해야 해요.

메모리 접근의 특성

메모리 접근 충돌의 맥락에서 고려해야 할 메모리 접근의 특성은 세 가지가 있습니다. 접근이 읽기인지 쓰기인지, 접근 기간, 그리고 접근되는 메모리 위치예요. 구체적으로, 다음 조건을 모두 충족하는 두 접근이 있다면 충돌이 발생합니다.

  • 적어도 하나는 쓰기 접근이거나 비원자적 접근(nonatomic access)이다.
  • 둘은 같은 메모리 위치에 접근한다.
  • 둘의 접근 기간이 겹친다.

읽기 접근과 쓰기 접근의 차이는 대개 명확합니다. 쓰기 접근은 메모리 위치를 변경하지만, 읽기 접근은 그렇지 않아요. 메모리 위치란 접근되는 대상, 예를 들어 변수, 상수, 속성 등을 말해요. 메모리 접근 기간은 순간적이거나 장기적일 수 있습니다.

어떤 연산이 C의 원자적 연산(atomic operation)만 사용한다면 그 연산은 '원자적(atomic)'이고, 그렇지 않다면 '비원자적(nonatomic)'입니다. 원자적 함수의 목록은 stdatomic(3) 매뉴얼 페이지를 참고하세요.

접근이 시작된 후 끝나기 전에 다른 코드가 실행될 수 없다면 그 접근은 '순간적(instantaneous)'입니다. 본질적으로 두 개의 순간적 접근은 동시에 일어날 수 없어요. 대부분의 메모리 접근은 순간적입니다. 아래 코드의 모든 읽기 접근과 쓰기 접근이 그 예죠.

func oneMore(than number: Int) -> Int {
    return number + 1
}
 
var myNumber = 1
myNumber = oneMore(than: myNumber)
print(myNumber)
// Prints "2"
swift

하지만 '장기적(long-term) 접근'이라 불리는, 다른 코드의 실행에 걸쳐 있는 메모리 접근 방식도 몇 가지 있습니다. 순간적 접근과 장기적 접근의 차이는 장기적 접근이 시작된 후 끝나기 전에 다른 코드가 실행될 수 있다는 점인데, 이를 '겹침(overlap)'이라고 해요. 장기적 접근은 다른 장기적 접근 및 순간적 접근과 겹칠 수 있습니다.

겹치는 접근은 주로 함수나 메서드에서 in-out 매개변수를 사용하거나 구조체의 변경 메서드(mutating method)를 사용하는 코드에서 나타납니다. 장기적 접근을 사용하는 구체적인 Swift 코드의 종류는 아래 섹션에서 다루겠습니다.