[Android] BLE 연결과 연동의 차이

easyhooon·2026년 1월 14일

BLE

목록 보기
1/2
post-thumbnail

출처)
https://kr.mathworks.com/help/bluetooth/gs/bluetooth-protocol-stack.html

서두

개발 중인 앱 내에 혈압계와 혈당계와 같은 BLE 기기 연동 작업을 진행하던 도중, BLE기기와 앱을 '연결' 하는 코드와 '연동(페어링)' 하는 코드의 결이 너무 다르다고 느껴졌다.

BLE 연결을 판단하는 예시 코드

private val gattCallback = object : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
        // 연결 상태 변경을 ConnectionStateManager에 전달
        connectionStateManager.handleConnectionStateChange(gatt, status, newState)
    }
}

/**
 * 연결 상태 판단 (Connection State)
 * BluetoothGattCallback의 onConnectionStateChange에서 제공하는 newState로 판단 가능
 */
fun handleConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
    currentDevice = gatt.device

    when (newState) {
        BluetoothProfile.STATE_CONNECTED -> {
            _connectionState.value = BleConnectionStatus.Connected
            Timber.d("✅ BLE 연결됨: ${gatt.device.address}")

            // 연결 시 페어링 상태도 확인
            checkBondingState(gatt.device)
        }

        BluetoothProfile.STATE_CONNECTING -> {
            _connectionState.value = BleConnectionStatus.Connecting
            Timber.d("🔄 BLE 연결 중: ${gatt.device.address}")
        }

        BluetoothProfile.STATE_DISCONNECTING -> {
            _connectionState.value = BleConnectionStatus.Disconnecting
            Timber.d("🔄 BLE 연결 해제 중: ${gatt.device.address}")
        }

        BluetoothProfile.STATE_DISCONNECTED -> {
            _connectionState.value = BleConnectionStatus.Disconnected
            Timber.d("❌ BLE 연결 해제됨: ${gatt.device.address}")
        }
    }
}

관련 문서

BLE 연동(페어링)을 판단하는 예시 코드

/**
 * Android API에서 연동(페어링)은 `bond`라는 용어로 표현됨 
 * 페어링 상태 변경을 감지하는 BroadcastReceiver
 */ 
private val bondingStateReceiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent?.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED) {
            val device = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)
            } else {
                @Suppress("DEPRECATION")
                intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
            }
            val bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE)
            val previousBondState = intent.getIntExtra(
                BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE,
                BluetoothDevice.BOND_NONE
            )

            // 현재 연결 중인 디바이스의 페어링 상태 변경만 처리
            if (device?.address == currentDevice?.address) {
                handleBondStateChange(bondState, previousBondState)
            }
        }
    }
}

/** OS (BluetoothDevice)에서 제공하는 상태
 * BOND_NONE(초기) → BOND_BONDING(Pairing 중) → BOND_BONDED(Bonding 완료)          
 */
private fun handleBondStateChange(bondState: Int, previousBondState: Int) {
    when (bondState) {
        BluetoothDevice.BOND_NONE -> {
            _bondingState.value = BleBondingStatus.NotBonded
            Timber.d("🔓 페어링 해제됨: ${currentDevice?.address}")
        }
        
        BluetoothDevice.BOND_BONDING -> {
            _bondingState.value = BleBondingStatus.Bonding
            Timber.d("🔄 페어링 진행 중: ${currentDevice?.address}")
        }
        
        BluetoothDevice.BOND_BONDED -> {
            _bondingState.value = BleBondingStatus.Bonded
            Timber.d("🔐 페어링 완료: ${currentDevice?.address}")
        }        
    }
}

관련 문서

왜 연결은 BluetoothGatt?Callback?이라는 거로 판단하고, 연동은 BroadcastReceiver로 판단을 해야하는 걸까? 그게 그거 같은데 동일한 클래스로 판단하면 되는거 아닌가?

위와 같은 무지한 의문을 해결하기 위해, 연결과 연동의 차이를 BLE 프로토콜 스택의 레이어 관점에서 정리해보고자 글을 작성하였다.

본론

Bluetooth Classic vs BLE

우선 BLE는 Bluetooth Low Energy의 약어 이다.

Bluetooth
    │
    ├── Classic (BR/EDR)
    │     └── 고전력, 대용량, 지속적 연결
    │         예) 에어팟 오디오, 파일 전송
    │
    └── Low Energy (BLE)
          └── 저전력, 소용량, 간헐적 연결
              예) 혈압/혈당계, 스마트워치 알림

기본 블루투스(Bluetooth Classic)와 저전력 블루투스(BLE)의 차이점을 표로 정리하면 다음과 같다.

Bluetooth ClassicBLE (Bluetooth Low Energy)
도입1.0 (1999)4.0 (2010)
전력 소모높음낮음
데이터 전송량대용량 (스트리밍)소용량 (간헐적)
통신 방식지속 연결필요시 연결
용도오디오, 파일 전송, 통화생체 센서, 알림, IoT
페어링필수선택적
프로토콜SPP, A2DP, HFP 등GATT

이번 글에선 이 중에서 BLE의 대한 내용을 중점적으로 다루도록 하겠다.

BLE는 페어링이 선택적???

실제로 기기에 따라 Pairing 자체가 필요 없을 수 있다(No Security).

예를 들어 비콘, 온습도 센서처럼 민감하지 않은 데이터를 다루는 기기는 Pairing 없이 바로 연결/통신이 가능하다.

반면, 건강 데이터처럼 민감한 정보를 다루는 기기는 Pairing 과정이 필수이며, 보안 수준에 따라 그 방식이 다르다.

보안 레벨Pairing기기 예시
No Security별도의 과정 ❌비콘, 온습도 센서, 미밴드(일부)
Unauthenticated⚠️ Just Works(연결 확인 버튼 클릭)일부 혈압계, 체중계
Authenticated✅ PIN 입력혈당계, 스마트 도어락

BLE에 대해 잠깐 살펴보았으니, 이제 서두의 질문 "왜 연결은 BluetoothGattCallback, 연동은 BroadcastReceiver로 처리하는가?" 에 답할 차례다.

이를 위해 연결과 연동이 무엇인지, 그리고 BLE 프로토콜 스택 구조를 알아보도록 하겠다.

연결과 연동

연결(Pairing): 암호화 키 교환 과정. RAM에 임시 저장되며, 연결 끊으면 소멸
연동(Bonding): Pairing 정보를 Flash에 영구 저장. 재부팅해도 유지됨

첫 연동 시

스캔 → 연결 → Pairing → Bonding → 서비스 탐색 → 데이터 통신
              (인증)    (등록)

이후 재연결 시

스캔 → 연결 → 서비스 탐색 → 데이터 통신
       (자동 인증 - Bonding 정보 사용)

Bonding이 완료된 기기는 블루투스 설정 > "등록된 기기" 목록에 표시되며, 이후 재연결 시 Pairing 과정(PIN 입력 등)을 생략할 수 있다.

BLE 프로토콜 스택

위 프로토콜 레이어에서 연결과 연동의 해당되는 레이어는 아래와 같다.

┌─────────────────┐
│   Application   │  ← 데이터 통신 (파싱, UI)
├─────────────────┤
│      GATT       │  ← 데이터 통신 (Service/Characteristic)
├─────────────────┤
│      ATT        │  ← 데이터 통신 (Attribute 읽기/쓰기)
├─────────────────┤
│      SMP        │  ← 연동 (Pairing/Bonding)
├─────────────────┤
│     L2CAP       │  ← 연결
├─────────────────┤
│   Link Layer    │  ← 연결/스캔
├─────────────────┤
│   Physical      │  ← 스캔 (advertising)
└─────────────────┘
구분레이어설명
데이터 통신ATT, GATT, Application서비스 탐색, 데이터 R/W
연동SMPPairing/Bonding (보안)
연결L2CAP, Link Layer통신 채널 수립
스캔Physical, Link Layer기기 발견

GATT

GATT(Generic Attribute Profile) 는 BLE 프로토콜 스택에서 데이터 구조를 정의하는 레이어이다.

Service와 Characteristic 계층 구조로 데이터를 주고받는 방식을 표준화한다.

  • Service: 관련 기능을 묶은 컨테이너. 예) Blood Pressure Service (UUID: 0x1810)
  • Characteristic: 실제 데이터 단위. 예) Blood Pressure Measurement (0x2A35)
  • Descriptor: Characteristic의 설정값. 예) CCCD (Notification on/off)

구조

GATT Server (기기)
└── Service (UUID로 식별)
    └── Characteristic (실제 데이터)
        ├── Value (읽기/쓰기 대상)
        └── Descriptor (설정값, e.g. Notification 활성화)

BluetoothGattCallback은 GATT 레이어 아닌가?

API 이름과 프로토콜의 레이어는 다르다. BluetoothGattCallback은 여러 프로토콜 레이어의 이벤트를 하나의 콜백으로 묶어서 제공한다.

콜백레이어역할
onConnectionStateChangeL2CAP연결/해제 상태
onServicesDiscoveredGATT서비스 탐색 완료
onCharacteristicReadGATT데이터 읽기 완료
onCharacteristicWriteGATT데이터 쓰기 완료
onCharacteristicChangedGATTNotification 수신

즉, onConnectionStateChange()연결(L2CAP) 레이어이고, 나머지는 콜백들데이터 통신(GATT) 레이어다.

콜백 호출 순서

connectGatt()
    ↓
onConnectionStateChange(CONNECTED)   ← L2CAP 연결 수립
    ↓
discoverServices()
    ↓
onServicesDiscovered()               ← GATT 서비스 목록 획득
    ↓
getService(UUID).getCharacteristic(UUID)  ← 이제 접근 가능
    ↓
readCharacteristic()   → onCharacteristicRead()    (읽기)
writeCharacteristic()  → onCharacteristicWrite()   (쓰기)
setCharacteristicNotification() → onCharacteristicChanged() (알림)

연결(onConnectionStateChange)이 완료되어야 서비스 탐색이 가능하고, 서비스 탐색(onServicesDiscovered)이 완료되어야 Characteristic에 접근하여 데이터 통신이 가능하다.

SMP

SMP(Security Manager Protocol) 는 BLE에서 Pairing/Bonding을 담당하는 보안 레이어다.

앱은 createBond()OS(Android Bluetooth 스택)에게 요청만 하고, 실제 Pairing 다이얼로그 표시, PIN 검증, 키 저장은 OS가 처리한다. 그래서 앱은 BroadcastReceiver로 결과(BOND_BONDED, BOND_NONE 등)만 수신하는 구조다.

왜 OS가 처리하는가?

1. 시스템 전역 자원

연동(Bonding) 정보는 앱이 아닌 기기 단위로 저장되며, 다른 앱도 동일한 연동(Bonding) 정보를 사용한다.

2. 보안

암호화 키(LTK)는 민감 정보이므로 앱이 직접 접근 불가하며, OS가 이를 안전하게 관리한다.

LTK가 뭔지 추가적으로 궁금하거나, Bluetooth의 보안에 관련해선 아래 블로그 글을 확인해보면 도움이 될 듯하다.
BLUETOOTH 보안 : Legacy pairing vs LE Secure Connections
Bluetooth Low Energy (BLE) Security & Privacy: A 2025 Guide

3. UI 일관성

Pairing 다이얼로그(PIN 입력 등)는 OS가 시스템 UI로 표시하며, 앱에서 직접 구현(커스텀)할 수 없다.

결론

왜 연결과 연동은 다르게 처리하는가?

연결 (Connection)연동 (Bonding)
정의데이터를 주고받을 수 있는 상태기기 정보를 시스템에 등록하여 재연결 시 자동 인증 가능한 상태
레이어L2CAPSMP (Security Manager)
관리 주체앱 (BluetoothGatt)OS (BluetoothDevice)
범위앱 별 독립시스템 전역
지속성앱 종료 시 끊김재부팅해도 유지
영구 저장없음암호화 키 (LTK 등)

연결은 앱이 BluetoothGattCallback API의 connectGatt()를 통해 직접 연결을 수립하므로, 해당 Gatt 객체의 콜백으로 상태 변화 수신이 가능하고,

연동은 OS가 페어링 시스템 팝업을 표시, PIN 검증, 키 저장 등을 처리하며 앱은 이 과정에 직접 개입이 불가하기 때문에, 따라서 시스템 Broadcast 통신을 통해 결과를 수신 받는다.

정리

연결: 앱 ←→ 기기 (직접 통신)
      └── BluetoothGattCallback

연동: 앱 ← OS ←→ 기기 (OS가 중개)
      └── BroadcastReceiver (OS가 결과 알려줌)

API 비교

항목연결 (Connection)연동 (Bonding)
APIBluetoothGattCallbackBroadcastReceiver
등록connectGatt() 시 자동registerReceiver() 명시적 호출
감지콜백 메서드 오버라이드onReceive() 구현
상수BluetoothProfile.STATE_*BluetoothDevice.BOND_*
현재 상태getConnectionState()device.bondState
정리disconnect(), close()unregisterReceiver()

느낀점

기존에 앱을 유지보수하며 어렴풋이 알고 있던 내용들을 이번에 한번 제대로 한번 정리해보며 Android의 BLE와 조금 더 친해질 수 있었다.

다만, 이번 글에서는 BLE 프로토콜 스택에 일부분만 다뤘을 뿐이고, Bluetooth Classic은 아예 다루지 않았기 때문에, 이후 Blutooth Classic도 알아보며 BLE와의 차이점을 보다 선명하게 이해해봐야겠다.

P.S

혈압계/혈당계 두 BLE 기기를 연동하던 중, 이 두 개의 BLE 기기 사이에도 다른 점이 있는 것을 확인할 수 있었다.

혈압계혈당계
데이터 전송측정 즉시 1건 전송기기 내 다중 기록 저장 후 요청 시 전송
프로토콜Notification만Notification + RACP

혈압계는 측정하면 바로 Notification으로 데이터가 날아오지만, 혈당계는 기기에 여러 건의 측정 기록을 저장해두고 앱에서 요청해야 전송하는 구조였다.

RACP

RACP(Record Access Control Point) 는 혈당계처럼 기기 내 측정 기록을 저장하는, BLE 기기에서 다중 기록 관리를 위해 사용하는 프로토콜을 의미한다.

RACP 프로토콜에는 다음과 같은 명령 코드들이 존재한다.

Op Code

Code의미
0x01Report Stored Records (전체 기록 요청)
0x02Delete Stored Records (기록 삭제)
0x03Abort Operation (작업 중단)
0x04Report Number of Stored Records (기록 개수 요청)
0x05Number of Stored Records Response (기록 개수 응답)
0x06Response Code (결과 응답)

BLE 기기는 대체적으로 저사양에 메모리가 제한적이므로, 많은 데이터를 저장하기엔 한계가 존재한다.

따라서, 기존 측정했던 데이터를 서버로 동기화 하였다면, 0x02 Op Code를 통해 기록 삭제가 가능하기에 삭제하고, 다시 처음 지점부터 최근 측정 기록까지 저장하는 방식을 채택하여 보다 효율적으로 구현이 가능하였다.

만약 기존 기록을 지우지 않고 남겨놓는다면, 별도의 체크포인트를 관리해야 하는데 이를 위한 추가 로직도 필요가 없어진다!

reference)
Bluetooth Low Energy
Bluetooth Protocol Stack
안드로이드 블루투스 아키텍처
https://github.com/android/connectivity-samples/tree/main/BluetoothLeGatt
How To Use Android BLE to Communicate with Bluetooth Devices - An Overview & Code examples
BLUETOOTH 보안 : Legacy pairing vs LE Secure Connections
Bluetooth Low Energy (BLE) Security & Privacy: A 2025 Guide

profile
실력은 고통의 총합이다. Android Developer

0개의 댓글