먹고 기도하고 코딩하라

[Swift] Realm accessed from incorrect thread (RLMException) 에러 해결하기 본문

앱/Swift

[Swift] Realm accessed from incorrect thread (RLMException) 에러 해결하기

사과먹는사람 2023. 2. 26. 22:14
728x90
728x90

 

2월 한 달동안 회사에서 데이터를 기기에 저장하는 작업을 진행했다. M 대리님이 하던 작업인데, 중간에 어떤 문제 때문에 꼬여서 중단됐던 걸 내가 맡아서 에러 고치고 기능 마저 구현하고 QA 티켓 처리하면서 마무리짓게 됐다. (곧 배포 예정! 예이~)

QA에서 나온 이슈 중에 하나가 클립을 다 재생하면 앱이 강제종료된다는 거였는데, 로그를 찾아보니 RLMException 때문에 죽고 있었다.

클립을 다 재생하면 위젯 데이터를 업데이트하는 함수를 호출하게 되어 있는데, 오프라인 모드일 때는 Realm에서 값을 읽어오는 작업을 한다. 여기서 exception이 일어났다.

 

 

원인

이 에러는 보통 Realm 객체를 생성한 스레드와 Realm에 접근하는 스레드가 다르기 때문에 발생한다. 

Realm은 thread-confined라서 Realm 객체를 다른 스레드/큐에서 접근 및 사용할 수 없다. 그런데 이것을 무시하고 다른 스레드에서 접근하려 하면 죽는 거다.

이와 관련해서 Realm 문서에는 스레딩에 관해 반드시 지켜야 할 3가지 규칙을 소개하고 있다.

  • Read 작업에 락걸지 말 것
    • Realm 데이터베이스의 MVCC 구조는 read 작업에 있어 락을 걸 필요가 없게 해준다. 읽어들이는 값은 절대 오염되거나 부분적으로 변경되는 일이 없다. 락이나 뮤텍스 없이 어떤 스레드에서든 자유롭게 읽어올 수 있다. 불필요한 락은 병목 현상을 부를 수 있다.
  • 백그라운드 스레드에서 write를 하고 싶다면 UI 스레드에서 write(CREATE, UPDATE, DELETE)하는 건 피할 것 
    • write 작업 역시 아무 스레드에서나 할 수 있지만, 한 번에 한 write만 할 수 있다. 결과적으로, write 작업의 경우에는 서로 write 작업을 블로킹한다. UI 스레드에서 write하면, 백그라운드 스레드에서의 write 작업이 끝날 때까지 앱이 잘 반응하지 않는 결과를 낳을 수 있다. Device Sync를 사용하고 있다면 UI 스레드를 사용하는 것은 피해라.
  • live objects, collections, realms를 다른 스레드에 전달하지 말 것
    • 위와 같은 것들은 thread-confined이다(*스레드 종속적이라고 이해하면 좋을 것 같다). 말인즉슨 이것들은 자신들이 생성된 스레드에서만 유효하다는 뜻이다. 다른 스레드로 전달할 수 없다. 하지만 Realm은 스레드 너머로 객체를 전달할 수 있는 몇 가지 방법을 제공하긴 한다.

이 에러는 3번째 규칙과 관련된 에러이다. 

 

Realm을 사용할 때 best practice는 어떤 스레드에서 새로운 오퍼레이션을 수행하고 싶을 때마다 그 스레드에 Realm 객체를 새로 만드는 것이라고 한다.

Realm은 내부적으로 스레드에 기반해서 Realm 객체를 캐싱하고 있기 때문에, Realm()을 하면 매번 새로운 Realm 객체가 생성되는 게 아니라 그냥 캐싱하고 있던 걸 return하고 있기 때문에 아주 약간의 오버헤드만 생긴다고 한다.

 

 

지금 사용 중인 코드에서는 아예 Realm 객체를 관리하고 여러 가지 연산을 제공하는 클래스를 하나 만들었고, 거기서 try! Realm() 해서 싱글턴으로 사용하고 있다. 이 작업을 메인 스레드에서 수행한다.

최근에 위젯을 추가하면서 위젯에 나타낼 데이터를 위해 Realm에 select 연산을 수행해서 데이터를 가져오는 작업을 추가하게 됐는데, 이걸 함수로 따로 빼놓고(accessRealmData라고 칭하자), WidgetCenter.shared.getCurrentConfigurations의 completion handler에서 호출하고 있었다.

그런데 이 completion handler 자체가 메인 스레드가 아닌 다른 스레드에서 돌다 보니, 클로저에서 호출하는 함수 역시 메인 스레드가 아닌 스레드에서 돌았다. Realm 생성 스레드와 접근 스레드가 불일치해서 앱이 죽는 거였다.

// 잘 될 때
Realm Select 연산을 수행하는 스레드 <_NSMainThread: 0x2815a0480>{number = 1, name = main}
WidgetCenter.getCurrentConfiguartions completion handler 스레드 <NSThread: 0x281ab2e80>{number = 15, name = (null)}
accessRealmData <_NSMainThread: 0x2815a0480>{number = 1, name = main}

// 종료될 때
Realm Select 연산을 수행하는 스레드 <_NSMainThread: 0x2817f4480>{number = 1, name = main}
WidgetCenter.getCurrentConfiguartions completion handler 스레드 <NSThread: 0x2817bc000>{number = 5, name = (null)}
accessRealmData <NSThread: 0x2817bc000>{number = 5, name = (null)}

브레이크포인트를 찍었을 때, Realm Select 연산 결과에 접근하는 스레드가 main이 아니면 100% 죽었다.

 

 

해결

원래 코드는 다음과 같았다.

WidgetCenter.shared.getCurrentConfigurations { results in 
    // code
    accessRealmData()
}

func accessRealmData() {
    // 뭔가 Realm Object 데이터에 접근함
}

accessRealmData가 Realm Select 연산 결과에 접근하는 스레드였으므로, 이걸 main 스레드에서 수행해야 앱을 살릴 수 있었다.

DispatchQueue.main.async로 감쌌다.

WidgetCenter.shared.getCurrentConfigurations { results in 
    // code
    DispatchQueue.main.async { 
        accessRealmData()
    }
}

func accessRealmData() {
    // 뭔가 Realm Object 데이터에 접근함
}

앱이 죽지 않고 잘 도는 것을 확인했고, 이슈는 closed 처리됐다.

 

그런데 엄밀히 따지자면 무조건 DispatchQueue.main.async로 감싸는 게 능사는 아니다.

우리 앱에서는 Realm을 싱글턴으로 만들어서 사용하고 있기 때문에 메인 스레드에서 동작하는 것이 보장되어야 했기 때문에 저렇게 쓴 거지, 문서에서는 realm에 접근해야 할 때마다 try? Realm()을 하는 방법을 더 권장하고 있다.

웬만한 이유가 있지 않고서야 realm에 접근해야 할 때마다 다시 불러오는 것이 더 좋은 방법이다.

 

 

 

References

 

 

728x90
반응형
Comments