BluetoothGatt — BLE 연결을 통해 GATT(Generic Attribute Profile) 클라이언트 작업을 위한 API를 제공하는 Android 클래스입니다. BluetoothGatt는 원격 GATT 서버(주변 BLE 장치)에 대한 연결을 캡슐화하고 서비스 검색, 특성 읽기 및 쓰기, 알림 및 표시 구독 등 모든 프로필 작업을 관리합니다. BluetoothGatt 인스턴스는 BluetoothDevice.connectGatt()와 BluetoothGattCallback을 통해 얻을 수 있습니다. Android Developers, 2026에 따르면 BluetoothGatt는 양방향 BLE 통신의 핵심 클래스이며 BLE 4.0에서 BLE 5.4까지의 GATT 작업을 지원합니다.
핵심 사항
BluetoothGatt — Android 장치(중앙)와 BLE 주변 장치(서버) 간의 GATT 연결을 나타내는 프록시 객체입니다. 각 BluetoothGatt 인스턴스는 하나의 활성 BLE 연결에 해당합니다. 모든 GATT 프로필 작업(검색, 읽기, 쓰기, 알림)이 이를 통해 수행됩니다. BluetoothGatt는 직접 생성되지 않고 BluetoothDevice.connectGatt() 메서드에 의해 반환됩니다.
BluetoothGatt를 생성하려면 네 개의 매개변수가 필요합니다. Context — 애플리케이션 컨텍스트(Activity 또는 Application). autoConnect — false인 경우 Android가 즉시 직접 연결을 시작하고, true인 경우 장치가 감지되면 자동으로 연결합니다(백그라운드 연결에 유용). BluetoothGattCallback — 모든 GATT 이벤트에 대한 필수 콜백. transport — BluetoothDevice.TRANSPORT_LE(BLE) 또는 TRANSPORT_BREDR(Classic). BLE 장치에서는 항상 TRANSPORT_LE를 사용하세요.
BluetoothGatt 수명 주기는 다섯 가지 상태로 구성됩니다. DISCONNECTED — 초기 상태. CONNECTING — connectGatt 호출 후, 확인 전. CONNECTED — STATE_CONNECTED로 onConnectionStateChange 후. 연결 후 GATT 계층 구조를 얻기 위해 discoverServices()가 호출됩니다. 작업 완료 후 — disconnect()와 close()로 시스템 리소스를 해제합니다. close()를 호출하지 않으면 앱이 Android의 BLE 연결 제한(보통 4~8개)을 소진할 수 있습니다.
// BluetoothGatt 연결 생성 중
import android.bluetooth.*
class GattConnector(private val context: Context) {
private var bluetoothGatt: BluetoothGatt? = null
fun connect(device: BluetoothDevice): BluetoothGatt? {
// 이전 연결이 있으면 닫기
close()
bluetoothGatt = device.connectGatt(
context,
false, // autoConnect = false ( )
object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt, status: Int, newState: Int
) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
// 2. 연결 설정됨 → 검색
gatt.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
// 3. 연결 끊어짐
close()
}
}
}
override fun onServicesDiscovered(
gatt: BluetoothGatt, status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 4. GATT 계층 구조 수신됨
onGattReady(gatt)
}
}
},
BluetoothDevice.TRANSPORT_LE
)
return bluetoothGatt
}
private fun onGattReady(gatt: BluetoothGatt) {
// GATT 읽기/쓰기 작업 준비 완료
}
fun close() {
bluetoothGatt?.disconnect()
bluetoothGatt?.close()
bluetoothGatt = null
}
}
GattConnector 클래스는 올바른 BluetoothGatt 생성을 보여줍니다. autoConnect=false 매개변수는 직접 연결(스캔된 장치용)을 의미합니다. STATE_CONNECTED로 onConnectionStateChange 시 즉시 discoverServices()가 호출됩니다. onServicesDiscovered는 GATT가 준비되었음을 알립니다. close()는 disconnect()와 close()를 순차적으로 호출합니다 — close() 없이는 시스템 리소스가 해제되지 않아 BLE 연결 누수가 발생합니다.
discoverServices() — 연결 후 호출되는 첫 번째 GATT 메서드입니다. BLE 주변 장치의 모든 서비스를 비동기적으로 검색합니다. 결과는 onServicesDiscovered()에 상태 코드와 함께 도착합니다: GATT_SUCCESS(0) — 성공, 133 — GATT_ERROR, 8 — GATT_CONNECTION_TIMEOUT. 검색 성공 후 BluetoothGatt는 getServices()를 통해 접근 가능한 서비스 목록을 채웁니다.
각 BluetoothGattService에는 BluetoothGattCharacteristic 목록이 포함됩니다. 특성에는 UUID, 속성(PROPERTY_READ, PROPERTY_WRITE, PROPERTY_NOTIFY) 및 선택적 설명자가 있습니다. 속성은 허용되는 작업을 결정합니다: 특성에 PROPERTY_READ가 없으면 readCharacteristic 호출 시 오류가 반환됩니다. 특성 설명자를 얻으려면 getDescriptors()를 사용하세요.
// 서비스 검색 및 특성 찾기
class GattServiceExplorer {
// UUID로 서비스 찾기
fun findService(gatt: BluetoothGatt, uuid: UUID): BluetoothGattService? {
return gatt.services?.firstOrNull { it.uuid == uuid }
}
// 서비스에서 특성 찾기
fun findCharacteristic(
service: BluetoothGattService,
uuid: UUID
): BluetoothGattCharacteristic? {
return service.characteristics?.firstOrNull { it.uuid == uuid }
}
// 지원되는 모든 특성 작업 가져오기
fun getCharacteristicProperties(chars: BluetoothGattCharacteristic): List<String> {
val props = mutableListOf<String>()
with(chars.properties) {
if (and(BluetoothGattCharacteristic.PROPERTY_READ) != 0) props.add("READ")
if (and(BluetoothGattCharacteristic.PROPERTY_WRITE) != 0) props.add("쓰기")
if (and(BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0) props.add("응답_없이_쓰기")
if (and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) props.add("NOTIFY")
if (and(BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) props.add("INDICATE")
}
return props
}
// 전체 GATT 계층 구조 로그
fun dumpGattTree(gatt: BluetoothGatt) {
gatt.services?.forEach { service ->
print("Service: ${service.uuid}")
service.characteristics?.forEach { char ->
print(" Characteristic: ${char.uuid}, properties: ${char.properties}")
char.descriptors?.forEach { desc ->
print(" Descriptor: ${desc.uuid}")
}
}
}
}
}
GattServiceExplorer 클래스는 GATT 계층 구조를 탐색하기 위한 유틸리티를 제공합니다. findService와 findCharacteristic은 UUID로 서비스와 특성을 검색합니다. getCharacteristicProperties는 and()를 통해 속성의 비트 마스크를 확인합니다. dumpGattTree는 로그에 전체 계층 구조를 출력합니다 — BLE 장치 디버깅 시 유용합니다. BluetoothGatt에 대한 모든 작업은 성공적인 onServicesDiscovered 후에 수행되어야 하며, 그렇지 않으면 getServices()가 빈 목록을 반환합니다.
readCharacteristic() — 원격 BLE 장치에서 특성 값을 읽기 위한 비동기 BluetoothGatt 메서드입니다. 결과는 BluetoothGattCallback의 onCharacteristicRead()에 도착합니다. 장치에 2개 이상의 일치하는 특성이 있는 경우(드물지만 가능) readCharacteristic()이 대상이 아닌 특성을 읽을 수 있습니다 — UUID 대신 BluetoothGattCharacteristic 인스턴스에서 readCharacteristic()을 호출하는 것이 더 안전합니다.
readDescriptor() — 특성 설명자 값을 읽는 메서드입니다. 일반적인 설명자는 CCCD(Client Characteristic Configuration Descriptor, UUID 0x2902)로, 알림이 활성화되었는지 여부를 결정합니다. 결과는 onDescriptorRead()에 있습니다. 실제로 설명자를 읽을 필요는 거의 없습니다 — CCCD는 setCharacteristicNotification()으로 관리되지만, 사용자 정의 설명자(User Description 0x2901, Presentation Format 0x2904)의 경우 readDescriptor()가 메타데이터를 얻는 유일한 방법입니다.
MTU 및 대용량 데이터 읽기 — 특성 값이 MTU(BLE 4.0의 경우 23바이트)를 초과하면 Android가 BLE 스택을 통해 읽기 요청 시퀀스로 데이터를 자동으로 분할 및 재조립합니다. 확장 MTU(최대 251바이트)를 지원하는 BLE 5.0+의 경우 분할이 필요하지 않습니다 — 한 번 읽기로 전체 데이터가 반환됩니다. 읽기 전에 requestMtu()를 호출하여 최대 MTU를 협상할 수 있습니다.
// BluetoothGatt를 통해 특성 및 설명자 읽기
class GattReader {
fun readHeartRate(gatt: BluetoothGatt) {
// 심박수 서비스 UUID = 0x180D
val service = gatt.getService(UUID.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
?: return
// 심박수 측정 UUID = 0x2A37
val characteristic = service.getCharacteristic(
UUID.fromString("00002A37-0000-1000-8000-00805F9B34FB")
) ?: return
if (hasProperty(characteristic, BluetoothGattCharacteristic.PROPERTY_READ)) {
gatt.readCharacteristic(characteristic)
}
}
fun readDescriptor(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
// CCCD(0x2902)
val cccd = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
)
cccd?.let { gatt.readDescriptor(it) }
}
// onCharacteristicRead 콜백에서 데이터 처리:
fun parseHeartRate(value: ByteArray): Int {
// BLE 심박수: bytes = flags, second = bpm
return if (value.isNotEmpty()) value[1].toInt() and 0xFF else 0
}
private fun hasProperty(char: BluetoothGattCharacteristic, prop: Int): Boolean {
return char.properties and prop != 0
}
}
GattReader 클래스는 심박수 특성 읽기를 보여줍니다. 서비스 0x180D에는 특성 0x2A37(Heart Rate Measurement)이 포함되어 있습니다 — Bluetooth SIG의 표준 BLE 프로필입니다. 읽기 전에 hasProperty를 통해 PROPERTY_READ 속성이 확인됩니다. parseHeartRate는 BLE 심박수 형식을 구문 분석합니다: 첫 번째 바이트 — flags(데이터 형식), 두 번째 — bpm 값. CCCD 설명자(0x2902)는 알림 상태를 확인하기 위해 읽힙니다.
writeCharacteristic() — BLE 주변 장치에 데이터를 쓰는 BluetoothGatt 메서드입니다. Android API 33+에서 writeCharacteristic()은 writeCharacteristic(request)로 대체되었으며, BluetoothGattCharacteristicWriteRequest는 특성, 바이트 배열 및 WriteType을 포함하는 요청 객체입니다. setValue()를 사용하는 이전 writeCharacteristic(characteristic) 메서드는 더 이상 사용되지 않습니다. WriteType은 요청 동작을 결정합니다: WRITE_TYPE_DEFAULT(특성 속성에 따라 다름), WRITE_TYPE_NO_RESPONSE(응답 없음), WRITE_TYPE_SIGNED(권한 부여).
WriteType 선택은 속도와 신뢰성에 영향을 미칩니다. WRITE_TYPE_DEFAULT는 일반적으로 withResponse(특성에 PROPERTY_WRITE가 있는 경우) 또는 withoutResponse(PROPERTY_WRITE_NO_RESPONSE가 있는 경우)에 해당합니다. 스트리밍 데이터(OTA 업데이트, 로그)의 경우 WRITE_TYPE_NO_RESPONSE를 사용하세요 — 최대 처리량. 전달 보장이 필요한 명령(활성화, 구성)의 경우 — onCharacteristicWrite를 통한 확인과 함께 WRITE_TYPE_DEFAULT를 사용하세요.
// Android API 33+에서 BLE 특성 쓰기
class GattWriter {
// 응답과 함께 쓰기(withResponse)
fun writeWithResponse(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// API 33+: writeCharacteristic
val request = BluetoothGattCharacteristicWriteRequest(
characteristic,
data,
BluetoothGattCharacteristicWriteRequest.WRITE_TYPE_DEFAULT,
@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
)
gatt.writeCharacteristic(request)
} else {
// API < 33: (deprecated)
characteristic.setValue(data)
gatt.writeCharacteristic(characteristic)
}
}
// 응답 없이 쓰기(최대 속도)
fun writeWithoutResponse(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, data: ByteArray) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val request = BluetoothGattCharacteristicWriteRequest(
characteristic,
data,
BluetoothGattCharacteristicWriteRequest.WRITE_TYPE_NO_RESPONSE,
@android.annotation.RequiresPermission(android.Manifest.permission.BLUETOOTH_CONNECT)
)
gatt.writeCharacteristic(request)
} else {
characteristic.setValue(data)
characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
gatt.writeCharacteristic(characteristic)
}
}
// onCharacteristicWrite 콜백(API 33+)
private val writeCallback = object : BluetoothGattCallback() {
override fun onCharacteristicWrite(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
status: Int,
callbackType: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
print("Write success: ${value.size} bytes")
}
}
}
}
GattWriter 클래스는 다양한 API 수준에 대해 두 WriteType을 모두 지원합니다. writeWithResponse는 WRITE_TYPE_DEFAULT를 사용합니다 — BLE 장치가 onCharacteristicWrite를 통해 쓰기를 확인합니다. writeWithoutResponse는 WRITE_TYPE_NO_RESPONSE를 사용합니다 — 데이터가 확인 없이 전송되어 최대 처리량을 제공합니다. API 33+에서는 BluetoothGattCharacteristicWriteRequest와 함께 새로운 writeCharacteristic(request)가 사용됩니다. API < 33에서는 — 이전 setValue() + writeCharacteristic()이 사용됩니다.
setCharacteristicNotification() — 주변 장치의 특성 변경에 대한 알림을 구독하는 BluetoothGatt 메서드입니다. 구독을 활성화하면 BLE 장치가 onCharacteristicChanged()를 통해 새 값을 보냅니다. 그러나 setCharacteristicNotification()은 로컬 Android 알림만 활성화합니다 — BLE 장치 자체에서 알림을 활성화하려면 CCCD 설명자(0x2902)에 값 0x0100도 써야 합니다.
CCCD(Client Characteristic Configuration Descriptor) — BLE 주변 장치에서 알림 전송을 제어하는 설명자입니다. 값 0x0000 — 알림 비활성화, 0x0100 — 알림 활성화, 0x0200 — 표시 활성화. CCCD 쓰기는 setCharacteristicNotification() 호출 후 BluetoothGatt의 writeDescriptor()를 통해 수행됩니다. Android는 자동으로 CCCD를 쓰지 않습니다 — 이 책임은 개발자에게 있습니다.
// 올바른 BLE 알림 구독
class GattNotificationManager {
// 1. 알림 활성화
fun enableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
// 1단계: 로컬 Android 구독
val success = gatt.setCharacteristicNotification(characteristic, true)
if (!success) {
print("구독 실패")
return
}
// 2단계: BLE 장치에 CCCD(0x2902) 쓰기
val cccdDescriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
) ?: return
// 0x0100 = 알림, 0x0200 = 표시
val cccdValue = if (characteristic.properties
and BluetoothGattCharacteristic.PROPERTY_NOTIFY != 0) {
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE // [0x01, 0x00]
} else {
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE // [0x02, 0x00]
}
cccdDescriptor.setValue(cccdValue)
gatt.writeDescriptor(cccdDescriptor)
}
// 2. 특성 검색
fun disableNotification(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
gatt.setCharacteristicNotification(characteristic, false)
val cccdDescriptor = characteristic.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")
) ?: return
cccdDescriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE)
gatt.writeDescriptor(cccdDescriptor)
}
// 3. 알림 콜백
private val notificationCallback = object : BluetoothGattCallback() {
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray,
callbackType: Int
) {
// BLE 주변 장치에서 새 값
print("Notification: ${value.size} bytes")
}
}
}
GattNotificationManager 클래스는 올바른 2단계 BLE 알림 구독 프로토콜을 구현합니다. enableNotification은 먼저 Android에서 setCharacteristicNotification(true)을 호출한 다음 writeDescriptor를 통해 CCCD 설명자에 0x0100을 씁니다. disableNotification은 역순 작업을 수행합니다. CCCD 쓰기 없이 BLE 장치는 알림을 보내지 않습니다 — 이것이 Android BLE 개발자들의 가장 흔한 실수입니다.
전체 예제 — 비동기 처리를 위해 코루틴을 사용하여 BluetoothGatt 생성, 검색, 읽기 및 알림 구독을 단일 관리자에 결합한 Kotlin의 GATT 클라이언트입니다.
// Kotlin에서 코루틴을 사용한 전체 GATT 클라이언트
class GattClient(context: Context) {
private val context = context.applicationContext
private var gatt: BluetoothGatt? = null
// 코루틴으로 연결
suspend fun connect(device: BluetoothDevice): Boolean =
suspendCoroutine { continuation ->
gatt = device.connectGatt(
context, false,
object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt, status: Int, newState: Int
) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
} else {
continuation.resume(false)
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
continuation.resume(status == BluetoothGatt.GATT_SUCCESS)
}
},
BluetoothDevice.TRANSPORT_LE
)
}
// 코루틴을 통해 특성 읽기
suspend fun readCharacteristicValue(char: BluetoothGattCharacteristic): ByteArray? =
suspendCoroutine { continuation ->
gatt?.let { gatt ->
// 콜백 식별을 위해 특성 태그 저장
gatt.setCharacteristic(char, null) // API용 < 33
gatt.readCharacteristic(char)
}
}
// 연결 닫기
fun release() {
gatt?.disconnect()
gatt?.close()
gatt = null
}
}
GATT 클라이언트 GattClient는 코루틴(suspendCoroutine)을 사용하여 콜백 기반 BluetoothGatt API를 순차적 호출로 변환합니다. connect()는 onServicesDiscovered를 기다린 후 GATT 계층 구조를 사용할 수 있습니다. readCharacteristicValue()는 onCharacteristicRead를 기다립니다. 이 접근 방식은 콜백 중첩을 제거하고 BLE 코드를 선형으로 만듭니다. release()는 리소스 정리를 보장합니다 — Activity의 onDestroy 또는 ViewModel.onCleared에서 필수 호출입니다.
자주 묻는 질문
BluetoothGatt — 주변 장치와의 BLE 연결을 관리하는 GATT 클라이언트용 Android 클래스입니다. BluetoothDevice.connectGatt()를 통해 생성되며 discoverServices(), readCharacteristic(), writeCharacteristic(), setCharacteristicNotification() 메서드를 제공합니다. 모든 작업 결과는 BluetoothGattCallback을 통해 비동기적으로 도착합니다. BluetoothGatt 없이는 Android에서 양방향 BLE 통신이 불가능합니다.
상태 133(GATT_ERROR)은 Android BLE 스택의 내부 오류를 나타냅니다. 원인: 검색 중 장치 연결 끊김, MTU가 최소(23바이트) 미만, BLE 스택 과부하. 해결책: 500ms 지연으로 discoverServices()를 재시도하고, 장치 RSSI를 확인하며, 주변 장치가 현재 상태에서 GATT 검색을 지원하는지 확인하세요.
확인과 함께 쓰려면 WRITE_TYPE_DEFAULT(API 33+: BluetoothGattCharacteristicWriteRequest)로 writeCharacteristic()을 호출하세요. 성공 시 BLE 장치가 확인을 보내고 Android가 GATT_SUCCESS로 onCharacteristicWrite를 호출합니다. 장치가 30초(스택 타임아웃) 내에 응답하지 않으면 콜백이 오류 상태를 반환합니다. Watchdog에는 postDelayed와 함께 Handler를 사용하세요.
Android 13+(API 33)에서 BluetoothGatt 메서드가 변경되었습니다: writeCharacteristic()은 BluetoothGattCharacteristicWriteRequest를, readCharacteristic()은 BluetoothGattCharacteristicReadRequest를 받습니다. 이전 setValue()/writeCharacteristic()은 더 이상 사용되지 않습니다. BluetoothGattCallback도 변경되었습니다: onCharacteristicRead(), onCharacteristicWrite(), onCharacteristicChanged()가 ByteArray 값과 callbackType을 받습니다. 분기에는 Build.VERSION.SDK_INT를 사용하세요.
Android는 4~8개의 동시 BLE GATT 연결을 지원합니다(제조업체 및 Android 버전에 따라 다름). Pixel/Google: 최대 7, Samsung: 최대 5, Xiaomi: 최대 4. 제한을 초과하면 connectGatt가 null을 반환하거나 onConnectionStateChange가 오류를 반환합니다. 많은 수의 장치로 작업하려면 순환 연결 또는 Bluetooth Mesh를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.