이번에 넥스터즈 면접 중 기술 면접을 진행하고 나서 내가 아직 많이 부족하다고 느낄 수 있었다.. Race Condition, Dead lock, Priority Inversion 등 멀티 스레드를 운용하면서 일어날 수 있는 문제점 등에 대해서 제대로 개념을 잡아두지 않아 답변에 어려움이 있었다..
처음엔 조금 속상했지만 그래도 이런 경험을 통해 이러한 키워드, 개념들을 잡고 갈 수 있는 기회가 될 수 있으니까 모르면 배우면 되지~ 하는 마음으로 단계별로 개념을 정리해보도록 하자.
먼저 오늘은 멀티쓰레드 환경에서 Race Condition에 대해서 알아보도록 하자!
Race Condition
그래서 레이스 컨디션이 뭘까 도대체 경쟁 상태라고 해석할 수 있는 이 상태는 두개 이상의 쓰레드나 프로세스가 공유 자원에 동시에 접근하면서 발생할 수 있는 문제를 말한다!
주로 개발하면서 결과가 실행 순서에 따라 달라질때 발생하는 문제로, iOS에서는 GCD 설정 즉 멀티쓰레딩 환경에서 발생하기 때문에 이 개념을 꼭 알고 있어야 한다.
간단한 예를 들어 쓰레드 A가 변수 count를 읽어서 1을 더하려고 하고, 동시에 쓰레드 B도 count를 읽어 1을 더하려고 할 때
두 쓰레드가 같은 변수 count를 동시에 읽는다면 업데이트된 값을 잃어버릴 가능성이 있다!!
Race Condition 예시
간단한 코드를 통해 알아보자
var count = 0
DispatchQueue.global().async {
for _ in 0..<1000 {
count += 1
}
}
DispatchQueue.global().async {
for _ in 0..<1000 {
count += 1
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("\\(count)") // 예상 값은: 2000이지만 실제로는 예측할 수 없다.
}
이 과정에서 동시에 두개의 쓰레드에서 count에 접근해 1씩 올리는 작업을 수행하고 있는데 동시에 접근하다보니 읽기 → 계산 → 쓰기 의 과정에서 오류가 생겨 원하는 답을 얻지 못하게 된다고 생각하면 이해가 좀 쉽다!
왜 이런 문제가 발생하냐면 count += 1 은 3단계로 이뤄진다.
- count 값을 메모리에서 읽는다
- 읽은 값에 1을 더한다.
- 결과를 메모리에 쓴다.
이러한 3단계를 통해 진행되는데 두개가 동시에 접근하게 되면 이 3단계 진행 과정이 섞이거나 덮어쓰여지는 문제가 발생한다.
그래서 결론적으로 count의 값이 1000씩 두번 더한 2000이 되는게 아니라 예측할 수 없는 안정적이지 않은 상태가 된다는 말!!!!
Race Condition 해결 방법
그렇다면 이러한 Race Condition을 방지하기 위해선 어떤 해결 방법이 있을지 알아보자. 사실 생각해보면 간단하게 해결할 수 있다. 동시에 접근해서 오류가 생기는 것이기 때문에 동시에 접근하지 않도록 수정하면 되는 것 🙂
동시에 접근하지 않도록 수정하는 방법에 대해서 알아보자.
Serial Queue 사용하기
Race Condition을 방지하기 위해 Serial Queue를 사용하게 되면 한번에 하나의 작업만 실행되기 때문에 위 문제를 방지할 수 있다.
let serialQueue = DispatchQueue(label: "com.example.serialQueue")
var count = 0
DispatchQueue.global().async {
for _ in 0..<1000 {
serialQueue.sync {
count += 1
}
}
}
DispatchQueue.global().async {
for _ in 0..<1000 {
serialQueue.sync {
count += 1
}
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("Final count: \\(count)") // 항상 2000
}
sync 를 활용해 custom한 시리얼 큐를 작동 시키면 한 작업이 끝날때까지 하나의 작업은 기다리는 상태로 놓여질 수 있기 때문에 원하는 답변을 구할 수 있게 된다.
NSLock 사용
두번째로 NSLock을 사용해 작업 접근을 잠그고, 끝나면 해제하는 방식을 사용해 동시에 작업이 실행되는 방식을 컨트롤 해 오류를 방지하는 방법이다.
let lock = NSLock()
var count = 0
DispatchQueue.global().async {
for _ in 0..<1000 {
lock.lock()
count += 1
lock.unlock()
}
}
DispatchQueue.global().async {
for _ in 0..<1000 {
lock.lock()
count += 1
lock.unlock()
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("Final count: \\(count)") // 항상 2000
}
sync를 사용해 시리얼큐로 작동하는 것이 아닌 비동기로 작동을 굳이 하고싶다면 해당 하는 작업을 시작하기 전에 lock()을 통해 잠근 뒤 작업이 끝나면 unlock()을 실행해 다음 작업이 접근할 수 있도록 설정하는 방식으로 동시에 접근하는 방향을 예방할 수 있다.
이제 그럼 UI를 업데이트하는 과정에서 어떤 문제가 발생할 수 있는지 알아보자.
UI 업데이트는 반드시 메인쓰레드에서 실행되어야 한다는 점은 다들 알고 있을 것이다. 이 과정에서 여러 쓰레드가 동시에 UI를 변경하려고 하면 충돌이 발생할 수 있다.
DispatchQueue.global().async {
myLabel.text = "Updated Text"
} // 충돌 가능
DispatchQueue.global().async {
DispatchQueue.main.async {
myLabel.text = "Updated Text"
}
} // 메인스레드 실행으로 오류 해결
Race Condition을 생각해야하는 이유
Race Condition은 중요 데이터가 덮어쓰여서 손실이 날 수 있고 테스트 환경에서는 잘 동작하지만 실제 환경에서는 예측할 수 없는 오류가 발생할 수 있다.
또한 이에 대한 디버깅에 어려움이 있기 떄문에 꼭 생각하면서 멀티스레드를 구성해야한다.
작은 목 데이터로 테스트를 진행하다가 가끔 실제 환경에서 알 수 없는 오류가 일어나는 경우가 있었는데 이런 경우 모든 순간이 다 Race Condition인 상태라서 발생했다고 볼 순 없지만 이 개념을 이해하고 있으면 빠르게 접근해 해당 오류 여부를 판단할 수 있는 능력을 길러야한다고 생각하면서 확실하게 개념을 잡은 것 같아 이제라도 해당 내용을 꼭 숙지하고 문제가 발생하면 해결할 수 있는 능력을 길러나가야겠다 🙂
이래서 어디든 면접을 볼 기회가 있다면 다 보는게 좋다고 하는 것 같다. 바라보는 시야가 넓어질 수 있는 기회가 있는 느낌..! 다음엔 Dead Lock에 대해서 알아볼 예정이다.
오늘은 여기까지!!
'◽️ Programming > iOS' 카테고리의 다른 글
Swift Format , Swift Lint 프로젝트에 적용하기 (0) | 2025.01.07 |
---|---|
Deadlock에 대해서 알아보자!! (0) | 2024.12.13 |
WKWebView를 사용해 웹페이지 가져오기 (1) | 2024.12.02 |
@AppStorage , UserDefaults의 특징 (0) | 2024.10.18 |
앱 내 데이터 JSON으로 변환하기 (1) | 2024.10.14 |