BluetoothLeScanner는 Bluetooth Low Energy 기기를 스캔하기 위한 Android 클래스로, API 21(Android 5.0)부터 사용할 수 있습니다. BluetoothLeScanner는 BluetoothAdapter의 더 이상 사용되지 않는 startLeScan 메서드를 대체하여 스캔 구성(ScanSettings), 필터링(ScanFilter) 및 백그라운드 모드 지원(PendingIntent)을 제공하는 유연한 API를 제공합니다. 인스턴스는 BluetoothAdapter.getBluetoothLeScanner()를 통해 얻습니다. Android Developers, 2026에 따르면 BluetoothLeScanner는 세 가지 전원 모드를 지원하며 서비스 UUID, 기기 이름 또는 MAC 주소로 필터링하여 BLE 광고 패킷을 스캔할 수 있습니다.
핵심 사항
BluetoothLeScanner는 Android에서 BLE 스캐닝을 관리하는 시스템 클래스입니다. 간단한 LeScanCallback을 받는 BluetoothAdapter.startLeScan()과 달리 BluetoothLeScanner는 설정, 필터 및 고급 오류 처리를 제공하는 객체 지향 API를 제공합니다. 이 클래스는 BLE 4.2 지원과 함께 API 21(Android 5.0)에서 도입되었으며 모든 최신 Android 버전에서 BLE 스캐닝의 기본 방법으로 남아 있습니다.
BluetoothLeScanner 인스턴스 얻기는 BluetoothAdapter.getBluetoothLeScanner()를 통해 수행됩니다. Bluetooth 어댑터를 사용할 수 없는 경우(Bluetooth 비활성화 또는 기기가 BLE를 지원하지 않음) 메서드는 null을 반환합니다. 얻기 전에 PackageManager를 통해 BluetoothAdapter.isEnabled() 및 FEATURE_BLUETOOTH_LE의 존재를 확인하세요. 스캐너를 얻은 후에는 모든 스레드에서 스캐닝을 시작할 수 있습니다. Android는 내부 Bluetooth 스택 스레드에서 BLE 작업을 예약합니다.
// BluetoothLeScanner 얻기
class BLEScannerManager(context: Context) {
private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val adapter: BluetoothAdapter? = bluetoothManager.adapter
private var scanner: BluetoothLeScanner? = null
fun initScanner(): Boolean {
// BLE 가용성 확인
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
return false
}
// Bluetooth 활성화 확인
if (adapter?.isEnabled != true) {
return false
}
// 스캐너 얻기
scanner = adapter?.bluetoothLeScanner
return scanner != null
}
// 스캐너 가용성 확인
val isAvailable: Boolean
get() = scanner != null
// 필터 없이 기본 스캔 시작
fun startBasicScan() {
scanner?.startScan(object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
handleResult(result)
}
})
}
private fun handleResult(result: ScanResult) {
val device = result.device
print("Device: ${device.name ?: "Unnamed"}, RSSI: ${result.rssi}, address: ${device.address}")
}
}
BLEScannerManager 클래스는 BluetoothLeScanner의 안전한 획득 및 초기화를 보여줍니다. initScanner는 hasSystemFeature를 통한 BLE 가용성, 활성화된 Bluetooth 및 성공적인 스캐너 획득을 확인합니다. startBasicScan은 설정이나 필터 없이 스캐닝을 시작하여 범위 내의 모든 BLE 기기를 발견합니다. handleResult는 ScanResult(BluetoothDevice(이름, 주소), RSSI(신호 강도), scanRecord(광고 데이터))를 구문 분석합니다.
ScanSettings는 BLE 스캔 구성을 위한 클래스입니다. 주요 매개변수는 스캔 모드(scanMode)로, 전력 소비와 검색 지연 시간 간의 균형을 결정합니다. ScanSettings.Builder를 사용하면 scanMode, callbackType(CALLBACK_TYPE_ALL_MATCHES, CALLBACK_TYPE_FIRST_MATCH, CALLBACK_TYPE_MATCH_LOST), matchMode(MATCH_MODE_AGGRESSIVE, MATCH_MODE_STICKY), reportDelay(배치 전달 지연) 및 phy(PHY_LE_1M, PHY_LE_2M, PHY_LE_CODED)를 구성할 수 있습니다.
세 가지 스캔 모드: SCAN_MODE_LOW_POWER(0) — 낮은 전력 소비의 백그라운드 스캐닝, 몇 초의 검색 지연. SCAN_MODE_BALANCED(1) — 대부분의 시나리오에 대한 균형 모드. SCAN_MODE_LOW_LATENCY(2) — 최소 검색 지연(약 100ms), 최대 전력 소비. 활성 기기 검색에는 LOW_LATENCY를, 백그라운드 모니터링에는 LOW_POWER를 사용하세요.
reportDelay — 결과 배치 전달 전 지연 시간(밀리초). reportDelay = 0이면 결과가 발견 시 즉시 전송됩니다. > 0이면 Android가 결과를 축적하고 onBatchScanResults를 통해 배치를 전송합니다. 배치 전달은 콜백 호출과 전력 소비를 줄이며 낮은 우선순위의 백그라운드 스캐닝에 적합합니다.
// 다양한 시나리오에 대한 ScanSettings 구성
class ScanSettingsProvider {
// 1. 빠른 스캔(활성 검색)
fun lowLatencyScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.setReportDelay(0)
.setPhy(ScanSettings.PHY_LE_ALL_SUPPORTED)
.build()
}
// 2. 전력 효율적 스캔(백그라운드 모니터링)
fun lowPowerScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.setReportDelay(2000) // 2초마다 배치
.build()
}
// 3. BLE Long Range 스캔(Coded PHY)
fun longRangeScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setPhy(ScanSettings.PHY_LE_CODED)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.build()
}
// 4. 2M PHY에서만 스캔(BLE 5.0+)
fun highSpeedScan(): ScanSettings {
return ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setPhy(ScanSettings.PHY_LE_2M)
.build()
}
}
ScanSettingsProvider 클래스에는 일반적인 구성이 포함되어 있습니다. lowLatencyScan은 UI 스캐닝용(「지금 여기」 검색). lowPowerScan은 2초마다 배치와 callbackType FIRST_MATCH(첫 번째 발견 시에만 트리거)를 사용한 백그라운드 모니터링용. longRangeScan은 PHY_LE_CODED(BLE Long Range, 최대 1km)를 사용. highSpeedScan은 PHY_LE_2M(2Mbit/s, BLE 5.0+ 기기만).
ScanFilter는 BLE 스캔 결과를 필터링하는 클래스입니다. 필터가 없으면 BluetoothLeScanner는 범위 내의 모든 BLE 기기를 반환합니다. 밀집된 BLE 환경에서는 분당 수백 개의 패킷이 될 수 있습니다. ScanFilter는 결과를 원하는 기기로 좁혀 전력 소비와 앱 부하를 줄입니다. 필터는 Bluetooth 스택 수준에서 적용되어 부적합한 패킷이 앱에 도달하기 전에 폐기됩니다.
필터 유형: setServiceUuid — 서비스 UUID(전체 128비트 형식 필요). setDeviceName — 기기 이름의 부분 문자열(대소문자 구분, 정확한 부분 문자열 일치). setDeviceAddress — 정확한 MAC 주소. setManufacturerData — 제조업체 데이터(회사 ID + 마스크). 단일 스캔에 여러 필터를 설정할 수 있으며 기기는 모두와 일치해야 합니다(AND 논리). OR 논리의 경우 여러 스캔을 시작하세요.
// 다양한 시나리오에 대한 ScanFilter 생성
class ScanFilterFactory {
// 1. 서비스 UUID로 필터(심박수 모니터)
fun byHeartRateService(): ScanFilter {
return ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB")
)
.build()
}
// 2. 기기 이름으로 필터( "iBeacon*")
fun byDeviceName(): ScanFilter {
return ScanFilter.Builder()
.setDeviceName("Sensor")
.build()
}
// 3. MAC-( 기기)
fun byMacAddress(mac: String): ScanFilter {
return ScanFilter.Builder()
.setDeviceAddress(mac)
.build()
}
// 4. 결합 필터(UUID + )
fun combinedFilter(): List<ScanFilter> {
return listOf(
ScanFilter.Builder()
.setServiceUuid(
ParcelUuid.fromString("0000A001-0000-1000-8000-00805F9B34FB")
)
.setDeviceName("MyDevice")
.build()
)
}
// 5. 제조업체 데이터
fun byManufacturer(companyId: Int, data: ByteArray, mask: ByteArray): ScanFilter {
return ScanFilter.Builder()
.setManufacturerData(companyId, data, mask)
.build()
}
}
ScanFilterFactory 클래스는 모든 필터 유형을 보여줍니다. byHeartRateService는 심박수 서비스 0x180D가 있는 기기를 필터링합니다. byDeviceName은 이름에 「Sensor」가 포함된 기기를 찾습니다(Apple은 필터링에 고유한 이름을 권장). byMacAddress — 특정 기기의 정확한 검색. combinedFilter — UUID 및 이름별 AND 필터. byManufacturer — 제조업체 데이터별 필터(예: iBeacon의 경우 Apple 회사 ID 0x004C 사용).
ScanCallback은 BLE 스캔 결과를 수신하기 위한 추상 클래스입니다. 세 가지 메서드를 포함합니다: onScanResult — 단일 결과(콜백 유형, ScanResult), onBatchScanResults — reportDelay > 0용 배치 결과, onScanFailed — 오류 코드. 모든 메서드는 Android 메인 스레드에서 호출됩니다. onScanResult에서 긴 처리를 위해 coroutines 또는 HandlerThread를 사용하세요.
ScanResult에는 다음이 포함됩니다: BluetoothDevice device, int rssi(dBm 단위 신호 레벨), ScanRecord scanRecord(광고 데이터), long timestampNanos(시스템 부팅 이후 검색 시간). ScanRecord 제공: getServiceData() — UUID + 사용자 정의 데이터, getManufacturerSpecificData() — 제조업체 데이터, getAdvertiseFlags() — BLE 플래그. 콜백 유형은 다음을 나타냅니다: CALLBACK_TYPE_ALL_MATCHES — 필터와 일치, CALLBACK_TYPE_FIRST_MATCH — 첫 번째 검색, CALLBACK_TYPE_MATCH_LOST — 기기 손실.
onScanFailed 오류 코드: SCAN_FAILED_ALREADY_STARTED(1) — 스캔이 이미 시작됨, SCAN_FAILED_APPLICATION_REGISTRATION_FAILED(2) — Bluetooth 스택에 앱 등록 실패, SCAN_FAILED_INTERNAL_ERROR(3) — 내부 스택 오류, SCAN_FAILED_FEATURE_UNSUPPORTED(4) — 기기에서 BLE 스캐닝이 지원되지 않음.
// 전체 스캔 결과 및 오류 처리
class ScanResultHandler {
private val results = mutableListOf<ScanResult>()
val scanCallback = object : ScanCallback() {
// 1. 단일 결과
override fun onScanResult(callbackType: Int, result: ScanResult) {
// callbackType: 1 = ALL_MATCHES, 2 = FIRST_MATCH, 4 = MATCH_LOST
if (callbackType == ScanSettings.CALLBACK_TYPE_MATCH_LOST) {
onDeviceLost(result)
return
}
// 목록에 추가(주소로 중복 제거)
val existingIndex = results.indexOfFirst {
it.device.address == result.device.address
}
if (existingIndex >= 0) {
results[existingIndex] = result // RSSI 업데이트
} else {
results.add(result)
}
// 광고 패킷에서 데이터 추출
val record = result.scanRecord
val serviceData = record?.serviceData
val manufacturerData = record?.manufacturerSpecificData
print("Found: ${result.device.name ?: "N/A"}, RSSI: ${result.rssi}")
}
// 2. 배치 결과(reportDelay > 0)
override fun onBatchScanResults(results: MutableList<ScanResult>?) {
results?.let { batch ->
print("Batch: ${batch.size} devices")
}
}
// 3. 스캔 오류
override fun onScanFailed(errorCode: Int) {
val error = when (errorCode) {
ScanCallback.SCAN_FAILED_ALREADY_STARTED -> "Already scanning"
ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED -> "Registration failed"
ScanCallback.SCAN_FAILED_INTERNAL_ERROR -> "Internal error"
ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED -> "BLE not supported"
else -> "Unknown error: $errorCode"
}
print("Error: $error")
}
}
private fun onDeviceLost(result: ScanResult) {
results.removeAll { it.device.address == result.device.address }
print("Device lost: ${result.device.address}")
}
}
ScanResultHandler 클래스는 모든 BluetoothLeScanner 콜백 유형을 처리합니다. onScanResult는 MAC 주소로 중복 제거하여 기기 목록을 업데이트하고 이미 찾은 기기의 RSSI를 업데이트합니다. CALLBACK_TYPE_MATCH_LOST는 기기 손실(목록에서 제거)을 알립니다. onBatchScanResults는 reportDelay > 0용 배치 결과를 처리합니다. onScanFailed는 오류 코드를 사람이 읽을 수 있는 메시지로 매핑하며 BLE 스캐닝 디버깅에 중요합니다.
PendingIntent 스캐닝은 앱이 백그라운드에 있어도 작동하는 BLE 스캐닝을 위한 BluetoothLeScanner 메커니즘입니다(Android 8+ 제한 있음). ScanCallback 대신 PendingIntent를 사용하여 BLE 기기가 발견될 때 시스템 BroadcastReceiver에 Broadcast를 보냅니다. 이를 통해 앱이 메모리에 상주하지 않고도 BLE 기기 알림을 받을 수 있습니다(시스템은 브로드캐스트 수신 시 프로세스를 생성합니다).
백그라운드 스캐닝 제한: Android 8+(API 26)에서는 백그라운드 서비스가 제한됩니다. PendingIntent 스캐닝은 BLE 이벤트 수신 시 시스템이 시작할 수 있는 BroadcastReceiver를 통해 이 제한을 우회합니다. Android 10+(API 29)에서는 백그라운드 BLE 스캐닝이 제조업체의 전원 절약 정책(Xiaomi, Huawei, Samsung이 백그라운드 BLE 작업 차단)에 의해 추가로 제한됩니다. 중요한 BLE 시나리오의 경우 포그라운드 서비스 알림이 필요합니다.
// PendingIntent를 통한 백그라운드 BLE 스캐닝
class BackgroundBLEScanner(private val context: Context) {
private val scanner: BluetoothLeScanner? by lazy {
val adapter = BluetoothAdapter.getDefaultAdapter()
adapter?.bluetoothLeScanner
}
fun startBackgroundScan() {
// BroadcastReceiver용 PendingIntent 생성
val intent = Intent(context, BLEBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 백그라운드 스캔 설정
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
.setMatchMode(ScanSettings.MATCH_MODE_STICKY)
.build()
// 백그라운드 스캔 시작
scanner?.startScan(
null, // 필터
settings,
pendingIntent
)
}
fun stopBackgroundScan() {
val intent = Intent(context, BLEBroadcastReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
scanner?.stopScan(pendingIntent)
}
}
// BroadcastReceiver BLE-
class BLEBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// 스캔 결과 가져오기
val results = BluetoothLeScanner.getPendingIntentScanResults(intent)
results?.let { scanResults ->
for (result in scanResults) {
// 사용자에게 알림 보내기
showNotification(context, result.device.name ?: " ")
}
}
}
private fun showNotification(context: Context, name: String) {
val notification = Notification.Builder(context, "ble_channel")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("BLE devices")
.setContentText("Found: $name")
.setAutoCancel(true)
.build()
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
manager.notify(System.currentTimeMillis().toInt(), notification)
}
}
BackgroundBLEScanner 클래스는 PendingIntent를 통해 백그라운드 BLE 스캐닝을 시작합니다. startBackgroundScan은 BLE 기기 발견 시 BLEBroadcastReceiver에 Broadcast를 보내는 PendingIntent를 생성합니다. BroadcastReceiver는 getPendingIntentScanResults()를 통해 ScanResult를 추출하고 알림을 표시하거나 서버에 데이터를 보낼 수 있습니다. 이 접근 방식은 앱이 시스템에 의해 종료된 경우에도 작동합니다. Android는 브로드캐스트 수신 시 BroadcastReceiver를 다시 시작합니다.
전체 예제: BluetoothLeScanner를 ScanSettings, ScanFilter 및 ScanCallback과 함께 사용하여 Heart Rate Monitor 기기를 찾는 Kotlin의 BLE 스캐너. 스캐너는 RSSI 및 서비스 UUID와 함께 발견된 기기 목록을 표시하며 BluetoothGatt를 통해 연결할 수 있습니다.
// Kotlin에서 코루틴을 사용한 전체 BLE 스캐너
class DeviceScanner(private val context: Context) {
private val adapter: BluetoothAdapter? by lazy {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE)
as BluetoothManager
manager.adapter
}
private val scanner: BluetoothLeScanner? by lazy {
adapter?.bluetoothLeScanner
}
fun startScan(duration: Long = 10000): Flow<ScanResult> = callbackFlow {
// Bluetooth 상태 확인
if (adapter?.isEnabled != true) {
close(IllegalStateException("Bluetooth disabled"))
return@callbackFlow
}
// 스캔 구성
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val filters = listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
.build()
)
val callback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
trySend(result)
}
override fun onScanFailed(errorCode: Int) {
close(BLEException("Scan failed: $errorCode"))
}
}
// 스캐닝 시작
scanner?.startScan(filters, settings, callback)
// 시간 후 자동 중지
delay(duration)
scanner?.stopScan(callback)
close()
}.flowOn(Dispatchers.IO)
fun stop() {
scanner?.stopScan(object : ScanCallback() {})
}
}
class BLEException(message: String) : Exception(message)
DeviceScanner 클래스는 반응형 BLE 스캐닝을 위해 Kotlin Flow(callbackFlow)를 사용합니다. 스캐닝은 LOW_LATENCY 설정과 Heart Rate Service UUID 필터로 시작됩니다. 결과는 onScanResult를 통해 Flow로 방출됩니다. 지정된 기간(기본 10초) 후 자동 중지. FlowOn(Dispatchers.IO)는 BLE 작업을 백그라운드 스레드로 오프로드합니다. 이 접근 방식은 viewModelScope.launch 및 collect를 통해 MVVM 아키텍처에서 BLE 스캐닝을 가능하게 합니다.
자주 묻는 질문
BluetoothLeScanner는 BLE 스캐닝을 위한 Android 클래스(API 21+)입니다. BluetoothAdapter.getBluetoothLeScanner()를 통해 얻습니다. 세 가지 스캔 모드(LOW_POWER, BALANCED, LOW_LATENCY), UUID, 이름 및 MAC 주소로 필터링, 배치 결과 및 백그라운드 스캐닝을 위한 PendingIntent를 지원합니다. 더 이상 사용되지 않는 BluetoothAdapter.startLeScan() 메서드를 대체합니다.
SCAN_MODE_LOW_POWER — 5~10초 검색 지연이 있는 백그라운드 모드, 최소 전력 소비. SCAN_MODE_LOW_LATENCY — 약 100ms 지연이 있는 활성 모드, 최대 전력 소비. SCAN_MODE_BALANCED — 절충(~2초 지연). UI 스캐닝에는 LOW_LATENCY를, 백그라운드 모니터링에는 PendingIntent와 함께 LOW_POWER를 사용하세요.
이유: Bluetooth 비활성화(adapter.isEnabled 확인), 권한 누락(API 31+에서는 BLUETOOTH_SCAN, API 23~30에서는 ACCESS_FINE_LOCATION), scanner = null(어댑터 사용 불가), 기기가 범위 밖에 있거나 필터가 잘못됨. onScanFailed도 확인하세요. 오류 코드가 원인을 나타냅니다: SCAN_FAILED_ALREADY_STARTED(1) 또는 SCAN_FAILED_APPLICATION_REGISTRATION_FAILED(2).
startScan()의 PendingIntent 버전을 사용하세요. ScanCallback 대신 PendingIntent를 전달합니다. BLE 기기가 발견되면 Android는 BroadcastReceiver에 Broadcast를 보내며, 앱이 백그라운드에 있어도 시스템이 시작할 수 있습니다. Android 8+의 경우 BroadcastReceiver를 매니페스트에 추가하세요. Android 10+에서는 제조업체의 전원 절약 제한을 고려하세요.
BluetoothLeScanner는 발견 가능한 기기 수에 제한이 없습니다. 제한은 환경의 BLE 포화도에 따라 다릅니다. 사무실에는 20~50개의 활성 BLE 기기가, 쇼핑센터에는 수백 개가 있을 수 있습니다. 필터링에는 ScanFilter(UUID, 이름으로)를 사용하세요. 필터링 없이 결과를 비동기식으로 처리하세요. onScanResult는 초당 수십 번 호출될 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.