BOID

[swift] 스위프트에서 메모리충돌 & 대처 - HoonIOS 본문

swift 시작기

[swift] 스위프트에서 메모리충돌 & 대처 - HoonIOS

HoonIOS 2021. 4. 11. 13:34

안녕하세요 HoonIOS입니다. :)

( 우선 포스팅을 하기 전에 여기에 있는 예시 및 조건은 다중스레드가 아닌 단일 스레드에서 컴파일, 실행이 된다고 가정을 하는 것입니다. )

 

스위프트는 상당히 안전을 중요하게 생각하는 언어입니다.

그중에서 메모리의 안전한 접근에 위험을 줄이도록 장치를 두었는데요, 그중에 대표적인 것이 변수를 사용하기 전에 initalize를 하고 해제된 메모리에 접근을 할 수 없도록 하는 것이 있습니다.

 

사실 스위프트는 컴파일러에서 메모리를 자동으로 관리해서 굳이 신경을 쓸 필요가 없는데요. 그 이유는 메모리 접근 충돌이 생길만한 코드를 미리 알려주는 방식으로 관리가 되고 있습니다.

 

메모리 접근 충돌에 대해

 

* 메모리에 접근을 하는 방법 3가지

  1. 개발자가 변수에 값을 할당
  2. 함수의 전달인자로 변수의 값을 전달
  3. 함수를 변수에 지정

- 이렇게 다양한 방법을 통해 메모리에 접근을 하게 됩니다.

 

- 메모리 접근 충돌은 서로 다른 코드에서 동시에 같은 위치의 메모리를 접근을 하려고 할 때 발생을 합니다.

 

* 예

 

메모리 접근 충돌의 예시

- 위와 같이  변수 A, 변수 B가 인스턴스 내부에 값을 가지고 있고 A와 B를 더하는 함수와 두 변수를 0으로 초기화하는 함수를 가지고 있다고 하자, 이것은 상관이 없지만 만약 더하는 함수와 0으로 최기화를 해주는 함수가 동시에 일어나면 어떻게 될까? 바로 위에서 말한 것처럼 메모리 접근 충돌이 발생하게 됩니다.

 

그럼 이제 메모리 접근 충돌을 일으키는 접근의 특성에 대해 알아보겠습니다.

 

메모리 접근 충돌을 일으키는 접근의 특성

 

메모리 접근에는 세 가지 특성이 있습니다.

만약 이조건에서 메모리 접근이 두 군데 이상의 코드에서 동시에 발생하면 메모리 접근 충돌이 발생합니다.

  1. 최소한 한곳에서 쓰기 접근이 가능
  2. 같은 메모리 위치에 접근
  3. 접근 타이밍이 겹친다.

- 순서대로 코드를 실행하고 메모리에 접근하는 것이 순간적이라면 다른 코드에서 같은 메모리 위치에 동시에 접근하는 일은 없습니다.

 

* 예시를 봐보겠습니다.

 

 

* 코드 설명

  • 순차적으로 name 변수에 값을 할당해주고 plusOne 함수를 통해 name 함수를 순간적으로 접근해 name 변수에 값을 추가해준 다음 name을 print 했습니다.
  • 이렇게 동시에 접근하는 게 없고 순간적이라면 다른 코드에서 메모리 위치에 동시에 접근하는 일은 없습니다.

- 장기적으로 메모리 접근이라는 접근 방식도 있습니다.

- 장기적 메모리는 해당 메모리가 소멸되기 전에 다른 코드에서 메모리 접근을 할 수 있어 겹치지 않게 조심을 해야 합니다.

- 접근 타이밍이 겹치게 되는 대표적 상황은 함수나 메서드에서 inout을 사용한 입출력 매개변수를 사용하거나 구조체에서 mutating키워드를 사용하는 가변 매개변수를 사용하는 경우입니다.

- 코드에서 메모리 접근 충돌을 예측할 수 있는 경우 컴파일러에서 오류로 취급을 해버려 컴파일을 하지 않습니다.

 

입출력 매개변수(inout)에서의 메모리 접근 충돌

 

* inout이란?

- 함수의 파라미터로 받은 값을 변경할 수 있도록 해주는 기능입니다.

 예시

var a = 1
var b = 2

func ex(a: Int, b: inout Int) {
	a = 2 
    b = 3 
}

ex(a: a, b: &b)

print(a) // 1
print(b) // 3

- &을 통해서 변수의 메모리 주소 값을 전달해 변경을 하는 방식입니다.

 

- 입출력 매개변수(inout)를 갖는 함수는 모두 장기적 메모리 접근을 합니다. 다시 말해 함수의 실행과 동시에 입출력 매개변수의 쓰기 접근이 시작되고 함수가 종료될 때까지 유지가 됩니다.

- 입출력 매개변수를 이용해 장기적 메모리 접근 중에는 매개변수로 전달하는 변수는 다른 접근이 제한이 됩니다.

 

* 예제를 통해 알아보겠습니다.

* 코드 설명

  • one의 변수가 increment 함수의 매개변수로 전달되면 컴파일 에러가 발생합니다. 이유는 바로 이미 함수 안에서 one을 사용하고 있어 plus도 one의 메모리를 one도 one의 메모리를 접근하고 있어 메모리 충돌이 발생합니다.

그럼 메모리 접근 충돌을 해결하면 어떻게 해야 될까요?

 

바로바로

.

.

.

새로운 변수에 one변수의 참조를 넣어주고 그 변수를 함수 안에서 접근시키면 메모리 충돌을 해결할 수 있습니다.

 

* 코드 설명

  • oneDeta에 one의 변수를 넣어줬기 때문에 같은 메모리 접근을 하는 게 아닙니다. 따라서 위의 문제였던 one의 값을 넣어 매개변수 plus와 oneDeata는 다른 메모리를 참조하고 있기 때문에 메모리 충돌을 해결할 수 있습니다.

 

이번에는 두 개의 입출력 매개변수에 같은 변수를 넣어주면 메모리 충돌이 발생을 합니다.

 

그 예시를 한번 봐보겠습니다.

 

* 코드 설명 

  • 첫 번째 halfFunc의 함수의 매개변수에 다른 변숫값을 참조값으로 넣어주면 다른 메모리이므로 메모리 충돌이 일어나지 않습니다.
  • 두 번째 halfFunc의 함수의 매개변수에 같은 변숫값을 참조값으로 넣어주면 같은 메모리를 동시에 입출력 매개변수로 전달하기 때문에 메모리 충돌이 발생합니다.

 

메서드 내부에서 self 접근의 충돌

- 구조체의 가변 메서드는 메서드 실행 중에 self로 쓰기 접근이 가능합니다.

 

야곰의 프로그래밍에 있는 예시를 보고 설명을 하겠습니다.

 

이 예시는 게임 캐릭터를 구조체로 구현했는데 캐릭터가 상처를 입으면 체력이 닳고 체력을 다시 회복하는 메서드와 체력을 다른 캐릭터와 공유하는 메서드를 만들어보겠습니다.

 

* 코드 설명

  • mutating 함수를 한번 살펴보겠습니다.
    - restoreHealth( ) 메서드는 self를 통해 자신에게 장기적으로 쓰기 접근이 가능합니다. 인스턴스의 다른 프로퍼티를 동시에 접근하는 코드가 없습니다.
    ( self.health는 자신의 인스턴스 GamePlayer.maxHealth는 새로운 인스턴스의 프로퍼티 이므로 )
    - shareHealth( ) 메서드는 다른 캐릭터의 인스턴스를 inout을 통해 받기 때문에 메모리 접근 충돌이 발생할 수 있습니다.

그럼. shareHealth에서 충돌이 일어나는 경우와 일어나지 않는 경우를 살펴보겠습니다.

 

* 코드 설명

  • 위 코드는 share Health가 호출이 됐을 때 두 개의 변수 모두 inout을 이용하기 때문에 둘 다 쓰기 접근을 하지만 충돌이 나지 않습니다. 그 이유는 서로 다른 변수( 서로 다른 메모리 위치)이기 때문에 접근 충돌이 일어나지 않습니다.
  • 즉 chara1, chara2의 변수는 서로 다른 메모리 위치이므로 접근 충돌이 일어나지 않습니다.

메모리 충돌이 일어나는 예시를 살펴보겠습니다.

chara1.shareHealth(with: &chara1)

* 코드 설명

  • 위 코드와 같이 전달받은 메모리 위치와 chara1의 인스턴스 메모리 위치가 같은 곳에 있으면 메모리 접근 충돌이 발생을 합니다.

프로퍼티 접근 중 충돌이 일어날 때

 

-  구조체, 열거형, 튜플은 값 타입입니다. 그래서 자신의 인스턴스 내부의 프로퍼티를 변경하는 것은 자신 스스로의 값을 변경한다는 의미로도 생각을 할 수 있습니다.

- 즉, 프로퍼티에 읽고 쓰기를 위한 접근만 가져오는 게 아니라 자신의 인스턴스 자신 전체에 대한 읽고 쓰기 접근권한도 필요하다는 것입니다.

 

앞에서 사용했던 예시를 한번 살펴보겠습니다.

* 코드 설명

  • balance함수의 두 매개변수 모두 inout으로 설정되어 있기 때문에 입출력 매개변수이므로 두 매개변수 모두 쓰기 접근이 가능합니다.
  • 첫 번째 매개변수를 보면 &chara1.health을 전달했으므로 chara1 자체의 쓰기 접근을 하고 있고 &chara1.energy도 마찬가지로 chara1 자체의 쓰기 접근을 해야 하므로 두 접근이 충돌이 날수밖에 없습니다.
    ( 즉 쓰기 접근을 하고 있고 동시에 접근을 하고 있기 때문입니다. )

chara1의 선언이 지역변수로 선언이 되었기 때문입니다. 그럼 만약 전역 변수로 선언했으면 어떻게 되는지 확인을 해보겠습니다.

* 코드 설명

  • chara1의 선언되는 곳을 shareHealth의 함수 내부에 전역 변수로 선언을 했습니다.
  • chara1의 balance함수가 실행이 되면서 chara1에 접근을 하려는 코드가 없습니다. 그 이유는 지역 변수이므로 다른 코드에서 이 코드로 접근을 할 수 없기 때문입니다.
  • balance함수에서 두 입출력 매개변수로 chara1을 전달해도 지역변수로 쓰이기 때문에 위에서 문제가 되던 순자적 실행시간만 겹칠 뿐 다른 문제가 되지 않습니다.
    ( 앞에서 말씀드린 메모리 접근 특성에서 2개 이상이 겹쳐야 문제가 된다고 하지만 하나만 겹치므로 문제가 되지 않습니다. )

 

* 다음 세 조건을 만족하면 구조체의 프로퍼티 메모리에 동시 접근해도 안전합니다. 이 세 가지를 만족하지 않으면 컴파일러가 접근을 제한합니다.

  1. 연산 프로퍼티나 클래스 프로러티가 아닌 인스턴스의 저장 프로퍼티에만 접근
  2. 전역변수가 아닌 지역변수일때
  3. 클로저에 의해 획득 되지 않거나 비탈출 클로저에 의해서만 획득될때

 

 

지금까지 스위프트에서 메모리 접근에 대한 것에 포스팅을 했습니다.

 

비록 컴파일러가 알아서 하지만 어느 부분에서 메모리 접근 충돌인지 알아야 고칠 수도 있을 거라고 공부하면서 생각을 했습니다.

 

그리고 뭔가 복잡한 부분과 추상적으로 생각해야 될 부분이 많다고 생각해 메모리 관리에 대해 좀 더 공부를 해야 될 거 같습니다. :)

반응형
Comments