GATT(Generic Attribute Profile)는 Bluetooth Low Energy(BLE) 프로필로, BLE 장치 간의 데이터 구조와 정보 교환 규칙을 정의합니다. GATT는 Attribute Protocol(ATT) 위에 구축되며 데이터를 서비스, 특성, 디스크립터의 계층 구조로 구성합니다. Bluetooth SIG(2025)에 따르면 GATT 프로필은 피트니스 트래커부터 스마트 잠금장치, 의료용 센서에 이르기까지 모든 BLE 애플리케이션의 98%에서 사용됩니다.
핵심 사항
GATT(Generic Attribute Profile)는 두 BLE 장치가 Attribute Protocol(ATT)을 통해 데이터를 교환하는 방법을 정의하는 Bluetooth Low Energy(BLE) 프로필입니다. GATT는 데이터 구조를 표준화합니다: 모든 데이터는 “서비스 → 특성 → 디스크립터” 계층 구조로 구성됩니다. 이 프로필은 BLE와 함께 Bluetooth 4.0 사양(2010)에서 도입되었으며, 그 이후로 에너지 효율적인 Bluetooth 장치의 주요 데이터 전송 메커니즘으로 남아 있습니다. 데이터가 직렬 포트(SPP)를 통해 전송되는 클래식 Bluetooth와 달리, GATT는 읽기, 쓰기, 알림 작업을 통해 데이터에 대한 구조화된 액세스를 제공합니다.
BLE 스택은 여러 계층으로 구성됩니다: 물리 계층(무선), 링크 계층(연결 관리), L2CAP(논리 채널 제어), ATT(Attribute Protocol — 속성 액세스), GATT(ATT 기반 프로필). GATT는 애플리케이션 개발자가 작업하는 최상위 계층입니다. 하위 ATT는 기본 작업을 제공합니다: 속성 읽기, 쓰기, 알림, 표시. GATT는 의미론을 추가합니다: 서비스와 특성이 무엇인지, 어떻게 그룹화되는지, 읽고 쓸 때 어떤 규칙이 적용되는지를 정의합니다. Bluetooth Core Specification 5.4(2023)에 따르면 GATT는 단일 장치에서 최대 65,535개의 속성(서비스 + 특성 + 디스크립터)을 지원합니다.
GATT 계층 구조는 세 가지 수준으로 구성됩니다. 서비스는 하나의 작업을 해결하는 특성의 논리적 그룹입니다(예: “배터리 서비스” 또는 “심박수 서비스”). 특성은 알려진 유형의 데이터 단위입니다: 현재 배터리 수준, 센서 판독값, 스위치 상태. 각 특성에는 값과 메타데이터(측정 단위, 알림 설정, 값 범위)를 설명하는 하나 이상의 디스크립터가 있습니다. UUID(Universally Unique Identifier)는 각 서비스와 각 특성을 고유하게 식별합니다.
GATT는 특성과 상호 작용하기 위한 네 가지 유형의 작업을 지원합니다. Read — 클라이언트가 서버에서 특성의 현재 값을 요청합니다. Write — 클라이언트가 서버에 새 값을 보냅니다. Notify — 서버가 확인 없이 클라이언트에 값을 보냅니다(더 빠르지만 신뢰성이 낮음). Indicate — 서버가 수신 확인과 함께 값을 보냅니다(더 신뢰성이 높지만 느림). 개발자는 시나리오에 따라 유형을 선택합니다: 심박수 센서 판독값에는 Notify로 충분합니다; 스마트 잠금장치 구성을 쓰려면 확인과 함께 Write가 필요합니다.
// Android 코드의 GATT 데이터 구조
data class BleService(
val uuid: UUID,
val characteristics: List<BleCharacteristic>
)
data class BleCharacteristic(
val uuid: UUID,
val properties: Int, // READ, WRITE, NOTIFY, INDICATE
val descriptors: List<BleDescriptor>,
var value: ByteArray?
)
BLE 사양은 두 가지 주요 프로필을 정의합니다: GAP(Generic Access Profile)와 GATT(Generic Attribute Profile). GAP는 장치 검색, 연결 설정, 가시성 관리를 담당합니다 — BLE의 “네트워크 계층”입니다. GATT는 연결 설정 후 데이터 교환을 담당합니다 — “애플리케이션 계층”입니다. 개발자는 장치 스캔 및 연결에 GAP를 사용하고, 연결된 장치에서 데이터 읽기, 쓰기, 알림 수신에 GATT를 사용합니다.
| 특성 | GAP | GATT |
|---|---|---|
| 목적 | 검색 및 연결 | 데이터 교환 |
| 역할 | 중앙 / 주변 | 클라이언트 / 서버 |
| 프로토콜 | HCI, 링크 계층 | ATT(Attribute Protocol) |
| 단계 | 연결 전 | 연결 후 |
| 메인 클래스 | BluetoothAdapter | BluetoothGatt |
GATT는 두 가지 역할을 정의합니다: GATT 서버와 GATT 클라이언트. 서버는 데이터를 제공하는 장치입니다(예: 심박수 데이터를 보내는 피트니스 트래커). 클라이언트는 데이터를 요청하는 장치입니다(예: 데이터를 읽는 스마트폰). 대부분의 시나리오에서 Android 앱은 GATT 클라이언트로 작동하고 BLE 주변 장치는 GATT 서버로 작동합니다. 그러나 Android는 GATT 서버가 될 수도 있습니다 — 예를 들어, 앱이 다른 장치를 위해 BLE 장치를 에뮬레이트하는 경우입니다. 역할은 GATT 연결 설정 단계에서 결정되며 세션 중에 변경되지 않습니다.
GATT 통신 프로세스는 장치 간 BLE 연결이 설정된 후 시작됩니다. 먼저, GATT 클라이언트는 Service Discovery를 통해 GATT 서버에서 서비스를 검색합니다 — Android는 BluetoothGatt.discoverServices()를 통해 연결 시 자동으로 이를 수행합니다. 검색 후 클라이언트는 사용 가능한 서비스, 특성, 디스크립터 목록을 얻습니다. 그런 다음 클라이언트는 특성 값을 읽거나(Read), 새 값을 쓰거나(Write), 알림을 구독할 수 있습니다(Set Notify/Indicate). GATT 서버는 연결 설정 후 언제든지 클라이언트에 Notify/Indicate를 보낼 수 있습니다.
기본적으로 BLE에서 단일 GATT 패킷(MTU)의 크기는 23바이트이며, 그중 3바이트는 ATT 헤더, 20바이트는 페이로드입니다. 애플리케이션이 더 많은 데이터를 전송해야 하는 경우(예: OTA 펌웨어 업데이트), Android의 requestMtu()를 통해 MTU를 최대 517바이트까지 늘릴 수 있습니다. MTU 증가는 하나의 데이터 블록을 전송하는 데 필요한 패킷 수를 줄입니다 — 50개의 작은 패킷 대신 2개의 큰 패킷을 보낼 수 있어 전력 소비를 줄이고 전송 속도를 높입니다. 최대 MTU는 BLE 버전과 칩 성능에 따라 다릅니다 — Bluetooth 5.0은 최대 517바이트, 이전 버전은 최대 247바이트를 지원합니다.
// 특성 알림 구독
fun enableNotifications(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
gatt.setCharacteristicNotification(characteristic, true)
// CCCD(클라이언트 특성 설정 디스크립터) 활성화
val cccdUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
val descriptor = characteristic.getDescriptor(cccdUuid)
descriptor?.let {
it.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
gatt.writeDescriptor(it)
}
}
Android에서 GATT 서버로부터 알림을 받으려면 BluetoothGattCallback.onCharacteristicChanged() 콜백을 구현해야 합니다. 이 메서드에서 앱은 서버가 Notify 또는 Indicate를 보낼 때마다 업데이트된 특성 값을 받습니다. 실시간 데이터 처리를 위해 버퍼와 Kotlin 코루틴을 사용합니다 — 이를 통해 집중적인 알림(예: 초당 100회 이상의 심박수 판독) 중에 UI 스레드가 차단되는 것을 방지할 수 있습니다. Indicate의 경우 서버 측에서 gatt.sendResponse()를 호출해야 합니다 — Android 클라이언트에서는 자동으로 처리됩니다.
Android에서 GATT 작업은 android.bluetooth 패키지의 클래스를 통해 구현됩니다: BluetoothGatt(연결), BluetoothGattService(서비스), BluetoothGattCharacteristic(특성), BluetoothGattDescriptor(디스크립터). BLE 장치에 연결은 BluetoothDevice.connectGatt()를 호출하여 시작됩니다 — 이 메서드는 이후 모든 작업이 수행되는 BluetoothGatt를 반환합니다. 모든 GATT 콜백은 BluetoothGattCallback으로 전달됩니다 — 이는 BluetoothGatt가 생성된 동일한 스레드에서 호출되는 비동기 인터페이스입니다. 중요: 모든 GATT 작업은 순차적으로 수행되어야 합니다 — 동일한 BluetoothGatt에서 여러 작업을 동시에 호출하면 오류가 발생합니다.
GATT 연결 수명 주기를 적절히 관리하는 것은 BLE 앱 안정성에 매우 중요합니다. connectGatt()를 호출한 후, 앱은 STATE_CONNECTED 상태의 onConnectionStateChange() 콜백을 기다립니다. 그런 다음 시스템이 자동으로 Service Discovery를 시작하고, 완료 후 onServicesDiscovered()가 호출됩니다. 그 후에야 읽기, 쓰기, 구독을 수행할 수 있습니다. 완료 시, 항상 onDestroy() 또는 onPause()에서 gatt.close()를 호출하세요 — 닫히지 않은 GATT 연결은 배터리를 소모하고 Android에서 동일한 장치에 대한 재연결을 차단할 수 있습니다.
// GATT를 통해 BLE 장치에 연결
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(
gatt: BluetoothGatt, status: Int, newState: Int
) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
val service = gatt.getService(UUID.fromString("180D"))
val characteristic = service?.getCharacteristic(
UUID.fromString("2A37")
)
characteristic?.let { enableNotifications(gatt, it) }
}
}
Bluetooth SIG는 고정 16비트 UUID로 수십 개의 표준 GATT 프로필과 서비스를 정의합니다. 가장 일반적인 것: Battery Service(UUID 180F, Battery Level 특성 2A19), Heart Rate 서비스(UUID 180D, Heart Rate Measurement 특성 2A37), Device Information 서비스(UUID 180A, Manufacturer Name 및 Serial Number 특성). 표준 서비스를 사용하면 다른 제조업체의 장치 간 호환성이 보장됩니다 — Heart Rate 서비스가 있는 모든 피트니스 밴드는 이를 지원하는 모든 Android 앱과 작동해야 합니다.
표준 서비스가 작업을涵盖하지 않는 경우, 개발자는 128비트 UUID로 사용자 정의 GATT 서비스를 만들 수 있습니다. 사용자 정의 서비스를 설계할 때는 다음이 필요합니다: 특성의 논리적 그룹 정의(예: “잠금 제어 서비스”), 각 특성에 올바른 속성(Read, Write, Notify) 할당, Characteristic Presentation Format 디스크립터를 통해 허용 값 및 측정 단위 정의. 복잡한 프로토콜의 경우 Write 속성이 있는 Command 특성과 Notify가 있는 Status 특성을 포함하는 것이 좋습니다 — 이는 산업용 BLE 애플리케이션에서 채택된 Command/Status 패턴을 따릅니다.
// 사용자 정의 GATT 서비스 생성(GATT 서버로서의 Android)
private fun createCustomService(): BluetoothGattService {
val serviceUuid = UUID.fromString("12345678-1234-5678-1234-56789abcdef0")
val service = BluetoothGattService(
serviceUuid, BluetoothGattService.SERVICE_TYPE_PRIMARY
)
val charUuid = UUID.fromString("12345678-1234-5678-1234-56789abcdef1")
val characteristic = BluetoothGattCharacteristic(
charUuid,
BluetoothGattCharacteristic.PROPERTY_READ or
BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ
)
service.addCharacteristic(characteristic)
return service
}
자주 묻는 질문
GATT(Generic Attribute Profile)는 장치 간 데이터 교환 구조를 정의하는 Bluetooth Low Energy 프로필입니다. 정보 액세스를 표준화하기 위해 필요합니다: 모든 BLE 장치는 데이터를 서비스와 특성으로 구성하여 모든 클라이언트가 센서 판독값을 읽고, 장치를 제어하고, 알림을 받을 수 있도록 합니다.
GAP는 BLE 장치 검색 및 연결(스캔, 광고, 연결 설정)을 담당합니다. GATT는 연결 후 데이터 교환(읽기, 쓰기, 알림)을 담당합니다. GAP는 연결 전에 작동하고 GATT는 연결 후에 작동합니다. 두 프로필 모두 BLE에 필수적이지만 다른 기능을 수행합니다: GAP는 “네트워크 계층”, GATT는 “애플리케이션 계층”입니다.
표준 서비스에는 Battery Service(180F, 배터리 수준), Heart Rate(180D, 맥박), Device Information(180A, 장치 데이터)이 포함됩니다. 각 서비스에는 16비트 UUID를 가진 여러 특성이 포함됩니다. 개발자는 특정 작업(예: 스마트 잠금 제어 또는 피트니스 트래커 데이터 전송)을 위해 128비트 UUID로 사용자 정의 서비스를 만들 수 있습니다.
Android에서 GATT는 BluetoothGatt(연결), BluetoothGattService(서비스), BluetoothGattCharacteristic(특성), BluetoothGattDescriptor(디스크립터)를 통해 구현됩니다. 연결은 connectGatt()를 통해 이루어지며, 그 후 BluetoothGattCallback을 통해 이벤트가 전달됩니다: onConnectionStateChange, onServicesDiscovered, onCharacteristicChanged(알림용). 모든 GATT 작업은 순차적으로 수행되어야 합니다.
UUID(Universally Unique Identifier)는 GATT에서 서비스 또는 특성을 고유하게 식별하는 16비트 또는 128비트 식별자입니다. 표준 Bluetooth SIG 서비스는 16비트 UUID를 사용합니다(예: Heart Rate의 경우 180D). 개발자의 사용자 정의 서비스는 128비트 UUID를 사용합니다(예: 12345678-1234-5678-1234-56789abcdef0). UUID를 통해 클라이언트는 GATT 서버에서 필요한 데이터를 찾을 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.