모바일 애플리케이션 개발에서 EXC_BAD_ACCESS (SIGBUS) 오류는 가장 난해한 유형 중 하나다. 특히 Realm과 같은 로컬 데이터베이스 프레임워크를 사용할 때 이 오류가 발생하면 단순한 코드 오류를 넘어, 운영체제의 보안 정책이나 파일 시스템 수준의 이슈까지 염두에 둬야 한다.
최근 iOS 18.5 환경에서 TestFlight 배포 중인 앱에서 다음과 같은 crash log가 보고되었다:
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Subtype: FS pagein error: 1 Operation not permitted
VM Region Info: 0x103c80020 is in 0x103c80000-0x103c88000
Termination Reason: SIGNAL 10 Bus error: 10
Triggered by Thread: 0
이 오류는 단순한 널 포인터 접근이나 해제된 메모리 접근이 아니다. 오히려 메모리에 매핑된 파일 영역(mmap)에서 운영체제 커널이 해당 페이지를 로딩(page-in)하려 했으나 보안 정책에 따라 접근을 거부했을 때 발생한다. 이를 이해하려면 iOS의 APFS와 Realm의 내부 동작 방식에 대한 이해가 필요하다.
Realm과 mmap 기반 메모리 접근
Realm은 성능 최적화를 위해 데이터베이스 파일을 메모리에 매핑(mmap)하여 필요한 페이지만 메모리에 올리는 방식을 사용한다. 이는 전체 데이터를 로딩하는 기존 SQLite 방식보다 훨씬 빠르고 효율적이다. 그러나 이 방식은 운영체제가 해당 파일의 특정 페이지를 물리 메모리로 불러올 수 있어야 한다는 가정에 기반한다.
만약 해당 페이지가 손상되었거나, 운영체제가 “보안상 불허된 보호 클래스로 지정된 파일”로 인식한다면, page-in 요청은 거부되며 SIGBUS (Bus Error) 가 발생한다.
APFS와 파일 보호 클래스의 충돌
iOS는 기본적으로 앱의 데이터를 APFS(Apple File System)에 저장하며, 파일별로 NSFileProtection 클래스를 부여할 수 있다. 문제는 Realm 파일이 다음 조건을 만족할 경우다:
.NSFileProtectionComplete: 사용자가 잠금 해제 상태일 때만 접근 가능- 앱이 백그라운드에 들어갔거나, 실행 도중 잠금 상태에 빠졌을 때 Realm 접근 시도
- 또는 앱이 실행 중이지만 백그라운드 큐에서 오래된 Realm 객체를 참조한 경우
이와 같은 조건에서 파일의 특정 페이지는 mmap되어 있어도, 실제 접근 시 커널이 “접근 권한 없음(Operation not permitted)”을 반환한다. 그 결과는 SIGBUS 크래시다.
흔히 발생하는 사용 시나리오
이러한 크래시는 일반적으로 다음과 같은 환경에서 발생한다:
- Realm 객체를 비동기 큐에서 장시간 보관한 후 접근
Realm 인스턴스는 생성된 쓰레드와 런루프에 바인딩되며, 쓰레드 종료 후에도 객체를 참조하면 문제가 발생한다. - 앱이 suspend → resume 되는 사이에 DB 접근
앱이 백그라운드에 있을 때 OS가 Realm 파일에 대한 접근을 잠그고, resume 후 아직 잠금 해제되지 않은 상태에서 접근하면 크래시가 발생한다. - 파일 보호 설정이 기본값(NSFileProtectionComplete)일 때
앱이 실행 도중 사용자 잠금 상태로 들어가면, DB 파일은 읽기 금지 상태가 된다.
대응 전략
1. 파일 보호 설정 점검 및 변경
Realm 구성 시, 명시적으로 보호 클래스를 설정해야 한다:
var config = Realm.Configuration()
config.fileURL = ... // 기존 Realm 파일 경로
config.fileProtection = .none // 또는 .completeUntilFirstUserAuthentication
Realm.Configuration.defaultConfiguration = config
.none은 테스트 환경에서만 권장되며, 실제 배포에서는 .completeUntilFirstUserAuthentication을 사용하는 것이 보안과 접근성 모두를 만족시킬 수 있다.
2. Realm 객체의 생명주기 관리
- Realm 인스턴스는 한 쓰레드 내에서 생성 및 소멸되어야 하며, 쓰레드 간 공유는 절대 금물이다.
autoreleasepool을 사용해 Realm 사용 범위를 명확히 분리한다:
DispatchQueue.global().async {
autoreleasepool {
let realm = try! Realm()
let results = realm.objects(MyModel.self).filter("...")
// 결과 처리
}
}
3. 백그라운드 접근 차단
앱이 background, inactive 상태일 때 Realm 접근이 일어나지 않도록 아래와 같이 분기 처리한다:
guard UIApplication.shared.applicationState == .active else { return }
또는 scenePhase를 활용하여 SwiftUI에서 처리 가능하다.
4. DB 파일의 무결성 점검 및 복구 전략
- 앱 실행 시
fileExists(atPath:)로 확인 후, 파일이 유효하지 않다면 삭제 및 새 Realm 생성 - Realm이 제공하는
migrationBlock이나deleteRealmIfMigrationNeeded옵션도 활용 가능
config.deleteRealmIfMigrationNeeded = true // 단, 실제 서비스에서는 위험
결론
EXC_BAD_ACCESS (SIGBUS)는 메모리 접근 오류인 동시에 운영체제 보안 정책과 충돌했음을 의미한다. Realm의 경우, mmap 기반 구조와 iOS의 파일 보호 체계(APFS + NSFileProtection) 사이에서 발생하는 미묘한 충돌이 그 원인이다.
이러한 오류를 방지하려면, Realm 파일의 보호 설정을 명확히 하고, 객체 생명주기와 쓰레드 구성을 꼼꼼히 관리해야 한다. 특히 앱의 라이프사이클과 Realm 접근 타이밍이 겹치는 경우엔 더욱 신중한 설계가 요구된다.
실제 사용자 환경에서 신뢰성 높은 앱을 만들기 위해서는 단순한 코드 오류를 넘어서, 시스템 레벨에서 일어나는 동작까지 정밀하게 고려해야 한다. Realm은 강력하지만, 그만큼 정확한 이해가 요구되는 프레임워크다.
답글 남기기