본문 바로가기
iOS

[iOS] Simultaneous accesses 오류

by pearhyunjin 2024. 9. 8.

 

Simultaneous accesses ~ but modification requires exclusive access.

Struct 타입을 만들어 이용하는 과정에서 위와 같은 경고가 발생하며 작동이 멈췄다. 원인과 해결 방법에 대해 알아보고자 한다.

 


 

해당 오류는 Swift의 메모리 관리 시스템이 객체에 대한 동시 접근(Simultaneous Access)을 감지할 때 발생하는 문제이다. 즉, 동시에 두 개 이상의 부분에서 값을 읽거나 수정하려고 할 때 발생하며, 특히 값 타입인 struct에서 자주 볼 수 있다. 특히 mutating 메서드가 실행 중일 때, 동일한 객체에 다른 읽기 또는 쓰기 접근이 동시에 일어나면 충돌이 발생할 수 있다고 한다.

 

아래 내용은 Swift 문서의 Memory Safe 부분에 나와있는 내용이다.

 

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 out-of-bounds errors. Swift also makes sure that multiple accesses to the same area of memory don’t conflict, by requiring code that modifies a location in memory to have exclusive access to that memory.

 

"기본적으로 Swift는 코드에서 안전하지 않은 동작이 발생하는 것을 방지합니다. 예를 들어 Swift는 변수가 사용되기 전에 초기화되고, 메모리가 할당 해제된 후에는 액세스되지 않으며, 배열 인덱스가 범위를 벗어난 오류인지 확인합니다. Swift는 또한 메모리의 동일한 영역에 대한 여러 액세스가 충돌하지 않도록 보장하는데, 메모리의 위치를 ​​수정하는 코드가 해당 메모리에 대한 배타적 액세스를 갖도록 요구하기 때문입니다."

 

즉, Swift에서는 값 타입의 수정이 항상 배타적 접근(Exclusive Access)이도록 요구한다. 간단히 말하자면 한 시점에 오직 하나의 코드만 값을 수정할 수 있어야 한다는 의미인데, 그렇지 않은 경우 프로그램이 값을 동시에 읽고 수정하려고 하면서 충돌 발생 가능성이 생기고, Swift가 이를 방지하기 위해 오류를 발생시킨다고 한다. 그 오류가 내가 경험한 "Simultaneous accesses ~ but modification requires exclusive access." 인 것이다.

 

먼저 배타적 접근이 무엇인지를 알아보고, 해결 방식에 대해 다뤄보려고 한다.

 

배타적 접근(Exclusive Access) 이란?

Swift에서는 독점 메모리 접근 규칙을 통해 변수의 변경(write)이 일어날 때, 다른 곳에서 동시에 그 변수에 대한 읽기(read)나 또 다른 변경이 이루어지는 것을 방지한다. 이는 주로 다중 스레드 환경에서 데이터 레이스(Data Race)를 방지하기 위한 메커니즘이라고 한다.

 

그렇다면, 데이터 레이스는 무엇일까?

 

다중 스레드 환경에서는 여러 스레드가 동시에 하나의 변수에 접근하는 경우가 자주 발생할 수 있다. 이때, 한 스레드가 그 변수를 수정하는 도중에 다른 스레드가 그 변수를 읽거나 수정하려고 할 때 발생하는 것이 데이터 레이스라고 한다. 데이터 레이스가 발생하면 예기치 않은 동작이 발생하거나, 프로그램이 충돌할 수 있다.

Swift에서 이 데이터 레이스가 발생하는 것을 방지하기 위해 값 타입인 구조체에서 변경이 일어나는 동안 해당 값에 대한 독점 접근을 보장하려고 하고, 그로 인해 "Simultaneous accesses..."와 같은 오류를 발생 시켜 경고해주는 것이다.

 

 

해결 방법 ?

구조체에서 클래스 변경

Swift의 구조체는 값 타입이기 때문에, 구조체 내부의 속성을 변경할 때마다 새로운 복사본을 생성하는 동작이 발생할 수 있다. 클래스를 이용하면 메모리 상에서 하나의 인스턴스를 여러 곳에서 동시에 수정하는 것이 가능해진다. 값 타입인 구조체에서 참조 타입인 클래스로 변경하여 데이터가 변경될 때에도 항상 동일한 참조를 사용하게 하여 동시 접근 문제를 해결할 수 있다.

 

+) 참조 타입의 유연성
클래스는 참조 타입이므로, 동일한 인스턴스를 여러 곳에서 쉽게 공유할 수 있다. 이를 통해 데이터가 한 곳에서 수정되면 그 참조를 가지고 있는 모든 곳에서 즉시 업데이트된 값을 참조할 수 있다. 이는 복사 비용이 없다는 것을 의미하며, 큰 데이터를 다룰 때 메모리 효율성을 높일 수 있는 좋은 방식이 된다.

 

+) 간결한 코드
클래스 기반의 코드는 비교적 간단하다. 구조체와 달리 mutating 키워드나 복사 동작을 염두에 두지 않아도 되므로, 코드가 덜 복잡해질 수 있다.

 

+) 멀티 스레드 환경에서 동시성 처리 필요 없음
값 타입과 달리, 클래스는 동일한 인스턴스에 대해 참조를 공유하므로, 기본적으로 Swift의 독점 메모리 접근 규칙이 덜 엄격하게 적용되기 때문에 동시 접근 문제를 따로 처리할 필요가 적다.

 

-) 참조 타입의 부작용
참조 타입은 모든 곳에서 같은 인스턴스를 참조하기 때문에, 명시적으로 데이터를 복사하지 않으면 한 곳에서 변경된 값이 다른 모든 참조하는 곳에서 영향을 미친다. 이로 인해, 의도하지 않은 동작이 발생할 수 있고, 버그 추적이 어려울 수 있다.

 

-) 메모리 관리 문제
클래스는 ARC(Automatic Reference Counting)를 사용해 메모리를 관리한다. 이 과정에서 순환 참조나 메모리 누수가 발생할 가능성이 있습니다. 만약 클래스 인스턴스가 다른 클래스 인스턴스를 강하게 참조하는 구조라면, 이를 관리하는 데 추가적인 노력이 필요할 수 있습니다.

 

-) Thread-safety 보장 없음

클래스 자체가 동시 접근 문제를 완전히 해결해 주는 것은 아니다. 여러 스레드에서 클래스의 값을 동시에 수정하려고 하면 여전히 데이터 레이스가 발생할 수 있으며, 이를 위해서는 별도의 동기화 메커니즘이 필요할 수도 있다.

 

비동기 접근 제어

만약 구조체를 유지하고 싶다면, 해당 타입에 대한 접근을 비동기적으로 처리해 동시 접근을 방지할 수 있다. DispatchQueue를 이용해 쓰기 접근을 비동기적으로 제어해 모든 접근을 직렬화(순차적으로 처리) 함으로써 가능해진다. 추가로, NSLock 이용하는 것 또한 가능하다.

 

* NSLock이란?

기본적인 동기화 메커니즘 중 하나로, 동기화된 코드 블록에서 하나의 스레드만 접근할 수 있도록 잠금을 걸어 여러 스레드가 동시에 변수나 리소스에 접근하는 문제를 해결할 수 있다. 즉, 스레드 간의 경쟁 상태(race condition)를 방지하는 데 사용된다.

  • lock(): 현재 스레드가 잠금을 획득할 때까지 대기. 잠금을 획득한 후 코드 실행.
  • unlock(): 잠금을 해제. 잠금을 해제하면 다른 대기 중인 스레드가 잠금을 획득할 수 있음.
  • tryLock(): 잠금을 시도. 잠금이 이미 걸려 있으면 false를 반환, 그렇지 않으면 true를 반환해 잠금을 획득.

 

+) 철저한 동시성 제어
DispatchQueue를 사용하면 구조체나 클래스의 값 수정 및 접근을 안전하게 동기화할 수 있다.

 

+) 값 타입(구조체)의 장점 유지
값 타입의 불변성과 복사 특성을 유지하면서도, 동시성 문제를 해결할 수 있다. 값 타입의 복사와 함께 데이터를 독립적으로 처리할 수 있는 특성을 활용할 수 있다.

 

+) 명확한 메모리 관리
값 타입은 참조 타입과 달리 ARC 관리에 의존하지 않기 때문에, 참조 카운팅 문제(순환 참조나 메모리 누수)를 걱정할 필요가 없다. 값이 명확하게 복사되고 관리되므로, 메모리 관리가 상대적으로 단순하다.

 

-) 코드 복잡성 증가
DispatchQueue를 사용하는 방식은 코드의 복잡성을 증가시킬 수 있다. 특히, 동기화가 필요한 부분마다 큐에 접근하거나, 비동기 처리를 명시적으로 관리해야 하므로, 코드가 더 길어지고 읽기 어려워질 수 있다.

 

-) 성능 오버헤드
직렬 큐를 사용하면 동시 접근 문제는 해결되지만, 성능 오버헤드가 발생할 수 있다. 모든 작업을 큐를 통해 직렬로 처리하기 때문에, 병렬 처리의 이점을 활용하지 못할 수 있다. 특히, 큐가 긴 대기열을 갖게 되면 프로그램의 성능에 악영향을 줄 수 있다.

 

-) 불필요한 동기화 가능성
때로는 동기화가 불필요한 부분에서도 DispatchQueue를 사용하게 될 수 있다. 이러한 경우, 과도한 동기화는 성능을 저하시킬 수 있으며, 불필요한 복잡성을 초래할 수 있다.

 

 

그렇다면, 각 방식들 중 어떤 방식이 좀 더 적합한 해결 방식일까?

 

클래스 기반 방식은 참조 타입을 활용한 데이터 공유와 간결한 코드가 필요한 경우 유리하다고 볼 수 있다. 다만, 참조 타입으로 인해 발생할 수 있는 부작용과 메모리 관리 문제를 신경 쓸 필요가 여전히 있다. 

DispatchQueue를 사용한 동기화 방식은 값 타입의 장점을 유지하면서, 동시성 문제를 해결하고 싶을 때 유리하다. 코드 복잡성은 증가할 수 있지만, 더 철저한 동시성 제어가 가능해지며 특히 멀티 스레드 환경에서 값 타입을 사용해야 하는 경우 적합하다.

 

프로젝트가 단일 스레드 환경이거나, 상대적으로 작은 데이터를 다룰 때 클래스 방식을 사용하고 이외에는 구조체 방식이 적합할 것 같다고 결론 지을 수 있을 것 같다..

 

 

 

 


* https://docs.swift.org/swift-book/documentation/the-swift-programming-language/memorysafety/

* https://www.swift.org/blog/swift-5-exclusivity/

* https://medium.com/@lucianoalmeida1/exploring-memory-safety-and-exclusive-access-in-swift-a8cd686b3288

'iOS' 카테고리의 다른 글

[iOS] Xcode - Logging Error  (0) 2024.07.14
[iOS] swiftUI 캘린더 직접 구현  (0) 2024.06.06