시리즈 · AI는 어떻게 개발자들의 벽을 허물었나
3회 · AI는 어떻게 개발자들의 벽을 허물었나 3 - 운영체제·임베디드까지 넓어진 실험의 범위
웹·게임·임베디드의 개발 장벽과 AI 도구가 이를 재구성하는 방식을 살펴보는 3부 연재의 마지막 글이다. 이 글에서는 ‘어떻게 구현하는지’를 코드와 명령어로 살펴보고 구체적인 기술장벽의 실체를 살짝이나마 엿본다. AI가 강하게 낮추는 벽은 바로 이런 종류다. 어떤 OS API가 시작점인지, 콜백이 어디로 돌아오는지, 어떤 로그를 열어야 하는지, 보드와 서버를 어떤 순서로 연결하는지를 한 번에 묶어 준다.
아래 예시는 소위 오토마우스 구현을 위한 자동화된 Windows UI 입력, 모바일 블루투스 장치 컨트롤을 위한 Android BLE, 하드웨어 임베디드를 위한 Raspberry Pi 기반 장난감 자동차라는 세 개의 작은 프로젝트의 기술장벽의 일부를 코드레벨로 살펴보자. 이 글에서는 실질적인 기술장벽의 실체에 대한 그 한 꼭지들을 다룬다. 더 깊은 이슈와 그 장벽들 예컨데 프로세스 메모리·후킹·커널 드라이버·안티치트 우회는 이 글의 범위가 아니다.
1. 오토 마우스의 UI 층: 좌표를 SendInput 이벤트로 바꾸기
Windows에서 ‘오토 마우스’의 가장 바깥쪽 구현은 의외로 단순하다. 화면에서 클릭할 픽셀 좌표를 정하고, 이를 0~65535의 절대 좌표로 정규화한 뒤, 이동·왼쪽 버튼 down·up을 INPUT 배열로 넣어 SendInput에 전달한다. Microsoft의 SendInput 문서는 이 함수가 키보드·마우스 이벤트를 입력 스트림에 순서대로 넣는다고 설명한다.
핵심은 MOUSEEVENTF_ABSOLUTE가 픽셀을 직접 받는 것이 아니라 정규화 좌표를 받는다는 점이다. 멀티 모니터에서는 MOUSEEVENTF_VIRTUALDESK와 가상 데스크톱의 left/top/width/height를 함께 써야 한다. 클릭 대상이 고정 좌표가 아니라 버튼이라면 다음 단계는 화면 캡처 → 템플릿 매칭 또는 OCR → 버튼 중심 좌표 산출 → clickAt() 호출이다. 이 흐름이 UI 자동화의 기술적 뼈대다.
익숙한 개발자들에게는 "Hello World" 수준의 단순 빌드겠지만 타 분야의 개발자들은 이런 생소한 언어의 단순 컴파일 조차 허들이 되는 경우가 허다했다.
SendInput은 User32 입력 계층의 API다. 이 글은 여기까지, 즉 OS가 제공하는 UI 이벤트 생성과 화면 좌표 처리만 다룬다. UIPI 제약과 앱별 입력 처리 차이는 실제 실행 때 확인할 운영체제 조건이다.
오토마우스 프로그램이 이렇게 허술하지는 않겠지만 자동화된 입력큐의 구현은 이 정도의 Input 트리거가 어떻게 보면 자동화된 마우스 입력의 핵심 허들 포인트라고 볼 수 있다.
2. Android Bluetooth LE: 스캔 콜백부터 GATT notification까지
Bluetooth 이어폰이든 센서든 Android 앱의 BLE(블루투스) 제어는 세 클래스가 중심이다.
BluetoothLeScanner는 주변 장치를 찾고
ScanCallback으로 결과를 받는다.
찾은 BluetoothDevice는
connectGatt()로 연결한다. 이후 연결·서비스 탐색·읽기·notification은
BluetoothGattCallback으로 돌아온다.
이것이 ‘블루투스 콜백 구현’ 뒤에 붙는 실질적인 허들 포인트이다.
Android 12 이상에서는 다음 권한을 manifest에 선언하고 런타임에도 요청한다. Android Bluetooth 권한 문서가 BLUETOOTH_SCAN과 BLUETOOTH_CONNECT의 용도를 정리한다.
아래 Kotlin 예시는 소유한 BLE 테스트 장치의 advertised name을 찾고, 연결 후 문서화된 service UUID와 characteristic UUID를 찾아 notification을 켠다. UUID는 이어폰마다 임의로 추측하는 값이 아니라 장치 제조사가 공개했거나 nRF Connect에서 확인한 GATT 계약 값으로 바꿔야 한다.
onConnectionStateChange()은 링크 상태를, onServicesDiscovered()는 GATT 테이블을, onCharacteristicChanged()는 장치가 push한 값을 받는다. Android API 33부터는 마지막 콜백이 ByteArray value를 직접 받는 새 시그니처를 쓰는 편이 안전하다. BluetoothGattCallback 레퍼런스가 이 변경과 각 콜백의 의미를 명시한다.
3. BLE가 안 붙을 때: nRF Connect, HCI, ATT, Wireshark를 어디에 쓰는가
BLE가 실패하는 경우도달하는 계층을 나눠 보면 failed point를 이해할 수 있다. nRF Connect는 휴대폰에서 주변 장치를 스캔하고 GATT service·characteristic을 직접 탐색·읽기·구독하는 테스트 앱이다. 즉 앱 코드의 UUID와 장치 GATT 계약이 맞는지 먼저 확인하는 도구다. **ATT(Attribute Protocol)**는 GATT가 characteristic 값을 읽고 쓰는 데이터 프로토콜이다. **HCI(Host Controller Interface)**는 Android Bluetooth host와 실제 Bluetooth controller 사이의 패킷 계층이다. Wireshark는 HCI snoop 파일을 열어 광고, 연결, ATT read/write/notification이 실제로 오갔는지 보는 분석기다. 이런 테스트가 까다로운 장비들을 테스트하는 도구, 장치, 방법등은 장치 디바이스 코딩의 실질적인 대표적 허들들이다.
먼저 디바이스를 수신하는 OS 레벨에서 앱과 서비스 상태는 다음 명령으로 본다.
그다음 운영체제의 Developer options에서 Enable Bluetooth HCI snoop log를 켜고 Bluetooth를 다시 시작한다. AOSP의 Bluetooth 검증·디버깅 문서는 dumpsys bluetooth_manager, bug report의 btsnooz.py BUG_REPORT.txt > BTSNOOP.log, 그리고 HCI snoop 로그 수집 절차를 제공한다.
이 순서면 블루투스 신호의 어디가 문제인지 좁힐 수 있다. 주변에 수많은 장비들이 보내는 신호들을 마구잡이로 수신하는 OS레벨에서 내 장비의 신호를 구별해 내고 해당 신호만 정제하는 핵심 기술이 드라이버 기술이다. 만약 nRF Connect에서도 service가 안 보이면 장치 광고·연결·GATT 자체 문제다. nRF에서는 보이는데 앱만 안 되면 권한, UUID, 콜백 순서, descriptor write를 먼저 본다. HCI에는 write가 있는데 notification이 없으면 peripheral firmware나 characteristic subscription을 본다. 코드·운영체제·controller 로그가 하나의 시간축으로 이어지는 지점이 바로 AI가 개발자의 탐색 시간을 크게 줄이는 곳이다. 조금 어려운 말들이 나온다고 쫄 필요없다. 이제 우리는 이런 수많은 용어, 방법, 해석을 직접할 필요가 없지 않은가. 중요한것은 그 허들이 어디쯤인지를 인지하는 것 뿐이다.
4. 임베디드 장비를 붙여보기위한 Raspberry Pi 자동차: 모터, 센서, UART
코드를 보기 전에 부품이 맡는 층을 먼저 나눠야 한다. 이 자동차는 ‘Linux 컴퓨터가 판단하고, GPIO가 명령 신호를 내고, 모터 드라이버가 전류를 바꾸며, 센서·MCU가 입력을 돌려주는’ 구조다.
| 용어 | 이 구성에서 맡는 역할 | 연결에서 확인할 것 |
|---|---|---|
| Raspberry Pi | Linux·Python·네트워크를 실행하는 메인 컴퓨터 흔히 경량 리눅스 운영체제를 깔고 실행할수 있는 기판이라고 생각하면 쉽다 | GPIO 핀맵, 카메라, /dev/tty*, Wi-Fi/Ethernet |
| GPIO | Pi가 0/1·PWM 같은 저전압 논리 신호를 내고 받는 핀 | BCM 번호와 물리 핀 번호, 3.3V 논리 레벨 |
| H-bridge | 모터 전류의 방향을 바꿔 전진·후진을 만드는 전력 스위치 | Pi의 GPIO 입력(IN1/IN2/PWM)과 모터 전원 분리 |
| TB6612FNG / L298N | H-bridge를 모듈로 만든 대표 모터 드라이버 | enable/STBY, 모터 전원, GND 공통 연결 |
| UART | TX/RX 두 선으로 바이트를 주고받는 직렬 통신 | 포트명, baud rate, TX와 RX를 교차 연결 |
| MCU·센서 | 거리·속도·ADC 같은 현장 입력을 읽어 Pi로 전달하는 보조 장치 | 전송 포맷(JSON 등), 샘플 주기, 센서 전원 |
따라서 장난감 자동차의 최소 구성은 Raspberry Pi + H-bridge 모터 드라이버 + 거리 센서 + UART로 연결된 보조 MCU 또는 센서다. Raspberry Pi가 “앞으로 45% 출력”이라는 논리 명령을 GPIO로 내면, H-bridge가 별도 모터 전원으로 실제 바퀴를 돌린다. MCU는 센서값을 UART로 Pi에 돌려주고, Pi는 그 값을 다시 Python·네트워크·카메라 코드와 결합한다. 모터는 GPIO에 직접 꽂지 않고 TB6612FNG·L298N 같은 H-bridge 보드를 거친다. Raspberry Pi 문서도 모터를 GPIO에 직접 연결하지 말고 motor controller를 사용하라고 안내한다. GPIO 하드웨어 문서에서 핀맵은 pinout 명령으로 바로 확인할 수 있다.
여기서 중요한 질문은 “센서의 신호가 Pi에 들어오면 무엇이 실제로 깨어나는가”다. 답은 센서 종류에 따라 다르다. 라즈베리 파이에 센서를 꽂는다는 말은 아래 셋 중 하나의 경로를 고르는 일이다.
| 센서의 출력 | Pi 안에서 지나는 길 | 프로그램에서 보이는 모습 |
|---|---|---|
버튼·PIR·센서의 INT/DRDY처럼 0V/3.3V가 바뀌는 디지털 선 |
GPIO 핀 → SoC GPIO 컨트롤러 → 상승/하강 엣지 → Linux GPIO 이벤트 | when_pressed 같은 이벤트 콜백 또는 gpiomon 대기 |
| I2C·SPI·UART로 바이트를 보내는 센서·MCU | I2C/SPI/UART 주변장치 → FIFO → 하드웨어 인터럽트 → Linux 드라이버 버퍼 | read()·readline()으로 블로킹 대기 |
| 가변저항처럼 연속 전압을 내는 아날로그 센서 | Pi에는 ADC가 없으므로 외부 ADC나 MCU가 먼저 디지털 값으로 변환 | SPI/I2C/UART로 변환된 값을 읽기 |
즉 전압이 변하면 SoC의 GPIO 또는 통신 주변장치가 먼저 감지한다. GPIO 입력은 상승·하강 엣지 또는 high/low level 인터럽트 원천으로 설정할 수 있다. Linux는 그 이벤트를 드라이버와 파일 디스크립터에 전달하고, 사용자 프로그램은 이벤트를 기다리거나 라이브러리 콜백을 등록한다. Raspberry Pi의 GPIO 문서도 각 GPIO 입력이 Arm의 인터럽트 원천이 될 수 있다고 설명한다. GPIO interrupt 문서를 보면 상승·하강 엣지와 level interrupt의 차이도 확인할 수 있다.
폴링, 블로킹 읽기, 콜백은 서로 다른 말이다. 이 차이를 먼저 잡아야 센서 코드를 제대로 읽을 수 있다.
| 코드 형태 | 실제 방식 | 자동차에서의 역할 |
|---|---|---|
while True: sensor.distance; sleep(0.05) |
50ms마다 현재 값을 물어보는 폴링 | HC-SR04처럼 Pi가 trigger를 먼저 보내야 하는 거리 측정 |
motor.forward(0.45) |
PWM 출력 | 바퀴를 돌리는 명령이며 입력 수신은 아님 |
uart.readline() |
UART 드라이버 버퍼에서 데이터가 올 때까지 잠드는 블로킹 읽기 | MCU telemetry를 CPU 낭비 없이 받기 |
sensor.when_pressed = handler |
GPIO 엣지를 기다렸다가 라이브러리가 사용자 함수를 실행 | INT·DATA_READY 핀이 있는 센서의 새 데이터 알림 |
엄밀히 말하면 Linux 커널이 Python 함수를 직접 호출하는 것은 아니다. 전압 변화 → GPIO 하드웨어 인터럽트 → 커널 드라이버 이벤트 → 기다리던 사용자 프로세스 또는 라이브러리 스레드가 깨어남 → handler 실행 순서다. gpiozero의 when_pressed는 이 마지막 사용자 공간 콜백을 편하게 만든 API다. gpiozero 입력 이벤트 문서는 is_pressed 폴링, wait_for_press() 블로킹 대기, when_pressed 콜백을 각각 구분한다.
1단계: 핀과 장치 파일을 먼저 확인한다
pinout은 물리 핀과 BCM 번호를 대조할 때 쓰고, /dev/ttyACM0·/dev/ttyUSB0은 USB/UART MCU가 Linux에 만든 장치 파일이다. GPIO 5라고 썼다면 물리 5번 핀이 아니라 BCM 번호 5번이라는 뜻이다.
2단계: HC-SR04처럼 요청해야 답하는 센서는 폴링한다
아래 예시는 gpiozero.Motor로 H-bridge의 IN1/IN2를, DistanceSensor로 HC-SR04 계열의 trigger/echo를 읽는다. HC-SR04는 Pi가 trigger 펄스를 보내고 echo 왕복 시간을 재야 값을 얻으므로, 여기서는 20Hz 폴링이 맞다. Motor의 핀 번호는 BCM 기준이며 실제 배선과 driver board enable/STBY 핀은 보드에 맞춰 조정한다.
3단계: INT 또는 DATA_READY 핀이 있는 센서는 엣지 이벤트로 받는다
가속도계·ADC·일부 거리 센서는 새 측정값이 준비되었음을 알리는 INT 또는 DATA_READY 핀을 제공한다. 이 핀을 Pi GPIO에 연결하면 매 50ms마다 값을 물을 필요가 없다. 예를 들어 센서가 BCM GPIO 5에 active-high DATA_READY 펄스를 보낸다고 하자.
when_pressed는 하드웨어 ISR 자체가 아니라 사용자 공간 콜백이다. 그럼에도 프로그램 관점에서는 “센서가 새 데이터 준비 신호를 내면 코드가 실행된다”는 push 방식이다. Linux GPIO 문자 장치 API도 상승·하강 엣지 이벤트를 사용자 공간에 전달하며 시간정보와 순번을 함께 제공한다. 더 낮은 층을 확인하고 싶다면 libgpiod 도구로 실제 엣지를 먼저 관찰할 수 있다.
4단계: UART는 블로킹 읽기로 받는다
UART가 붙은 Arduino·센서 보드는 센서 측에서 한 줄씩 JSON을 보내고 Pi에서 읽는다. 이것이 기판과 Linux 애플리케이션 사이의 가장 단순한 telemetry 통로다. 여기서 readline()은 50ms마다 빈 버퍼를 확인하는 busy polling이 아니다. UART 하드웨어가 바이트 도착을 인터럽트로 알리고, Linux TTY 드라이버가 커널 버퍼에 넣은 뒤, 잠들어 있던 readline()을 깨운다.
정리하면 폴링만 가능한 것이 아니다. DATA_READY가 있으면 GPIO 엣지 이벤트, UART처럼 스트림을 보내면 블로킹 readline(), I2C 센서가 요청에만 답하면 필요한 주기의 폴링을 고른다. 센서의 데이터시트에서 먼저 볼 항목은 “측정값 레지스터”만이 아니라 INT, DRDY, DATA_READY, FIFO 같은 단어다. 이 한 줄이 있으면 센서 → 운영체제 → Python 제어 루프의 구현 방식이 달라진다.
5. 카메라 프레임을 서버로 보내기: Picamera2와 HTTP
카메라까지 붙이면 자동차는 센서값뿐 아니라 프레임을 서버에 보낼 수 있다. 최근 Raspberry Pi OS에서는 Picamera2가 libcamera/rpicam 스택 위에서 동작한다. Raspberry Pi 카메라 문서와 Picamera2 매뉴얼은 capture_array()로 NumPy 프레임을 얻는 흐름을 설명한다.
서버 쪽은 처음에는 프레임을 받아 상태 코드만 돌려주는 정도면 충분하다. 이후 MQTT로 모터 명령을 분리하거나, 서버에서 객체 탐지 결과만 반환해 Pi가 판단에 사용하도록 확장할 수 있다. 이때 AI가 특히 잘하는 일은 GPIO 핀, UART baud rate, camera format, HTTP payload, systemd 서비스 로그처럼 서로 다른 문서에 흩어진 연결 규칙을 하나의 실행 가능한 초안으로 합치는 것이다.
임베디드가 어렵다고 느껴지는가? 필자도 마찬가지다. 하드웨어 센서들의 스펙과 그 콜백 구조를 모두 알아야 정확한 폴링구조와 센싱 값의 스펙을 분류하고 원하는 신호해석과 동작을 구현할 수 있다. 그러나 이제 그 허들의 실체를 눈으로 확인한거로 족하다. 이 허들을 보았으니 AI에게 질문할 수 있고 AI의 답을 재질문하며 딥 다이브를 할 수 있다.
3화의 핵심은 다양한 분야별 문턱을 눈으로 확인하는 것이다. 그리고 그 문턱의 실체를 비로소 AI에게 질문하는 것이다. AI는 이미 그 문제의 답을 어느정도 알고 있다. 애초에 문지방이 어디있는지 조차 몰랐던 우리가 이제 AI를 이용해 그 문턱의 실체를 확인하고 그 턱을 넘어가는 방법을 익히며 비로소 완성형까지도 노려볼수 있는 시대가 되었다는 것을 깨닳자.
마치며
과거 각 분야를 혼자서 감당 하기에는 상당한 분량과 방대한 지식을 필요로 했었다. 이 기술들로 산업을 꾸리는 업체들은 이 모든 기술과 역량으로 수익까지 실현해야 하니 산업현장 난이도는 더욱 올라간다. 그런데 AI가 등장했다. 많던 적던 수많은 운영체제의 기초 코드, 임베디드 코드, 가상화 기술, 수많은 공개된 오픈소스 기술, 모든 언어, 오래된 파이썬, 스크립트, 디자인을 모두 흡수한 AI는 인간의 응용력을 가뿐히 뛰어 넘는다. AI는 운영체제의 모든 API 세트를 알고 있고 Windows API를 이용해 손쉽게 윈도우의 창을 띄운다. 띄운 윈도우 창에서 윈도우즈 운영체제가 지원하는 모든 컴포넌트, 하드웨어 제어, 임베딩 드라이버 목록, 제어, 후킹 을 제한없이 알려준다. 방화벽, 권한 문제도 체계적으로 접근하거나 얻어오도록 알려준다. 키보드, 마우스 HID의 제어는 이제 누워서 떡먹기다. 윈도우즈가 제공하는 화면의 캡쳐, 이미지 프로세싱, 이미지 분석, 거대 LLM AI까지 붙이면 실시간 이미지를 분석까지 한다. 자동화 이벤트 발신 및 트리거 등 옛날의 그 얇팍했던 윈도우즈 커널 허들을 가뿐하게 넘어준다.
하드웨어의 컨트롤이 윈도우 뿐아니라 안드로이드여도 문제되지 않는다. 이미 공개되어 있는 AOSP(안드로이드 운영체제 오픈소스)는 안드로이드 운영체제의 파운데이션 코드가 공개되어 있다. AOSP 아키텍처 개요는 AOSP를 공개·수정 가능한 Android 소스라고 설명한다. 이는 학습한 AI는 모바일의 구동, 드라이버의 구현과 그 원리를 모를리가 없다. 당연히 모바일 블루투스, C-Type Cable 인터페이스, NFC의 기초 기술들은 AI를 통해 그 기초를 손쉽게 넘어버린다. 리눅스 운영체제 기반으로 동작하는 라즈베리파이, 아두이노 역시 마찬가지이다. 리눅스 운영체제와 기판을 연동하는 임베디드 드라이버 기술은 그 자체로 공공연한 비밀 같았는데 이제 AI 앞에서 비밀따위는 없다. 모든 기술, 테스트환경, 도구 등이 무엇이었는지 낱낱히 공개되었다. 개발자들에게 항상 외딴 섬 같았던 파이썬의 풍부한 기술들이 이제 AI로 인해 모든 개발자들에게 해금되었다. 기존 자바나 스크립트 레벨만 다루던 개발자들도 이제 C,C++,파이썬을 마음대로 활용할 수 있는 새로운 기회가 열린 것이다. 아직도 초기인 AI 바이브코딩 시장에서 개발자들은 아직 자신의 능력이 자신도 모르게 어디까지 확장되었는지 깨닳지도 못하고 있다. 이미 뻥튀기되어 거의 무한대로 뻗어진 자신의 기술력을 알기도 전에 AI에게 완전히 대체될지도 모를 일이다. AI의 파급력을 가장 먼저 얻어맞고 있는 개발자들은 이 외계기술 같은 AI를 가지고 감탄만 하면서 아직 능력의 10%도 못 사용하고 있다. 여러분은 이 외계의 기술의 극의를 어디까지 확인해 보았는가? 눈을 더욱 크게 떠야할 때이다.
이 글에서 살펴본 예시처럼 개발자 CLI와 AI 코딩 도구를 통해 낯선 분야에서도 어떤 클래스 → 어떤 콜백 → 어떤 로그 → 어떤 포트 → 어떤 테스트 순서로 질문하고 손을 움직일 수 있다. 그 흐름을 직접 실행하고 수정하는 순간, 언어와 플랫폼이 나눠 놓았던 벽은 이전보다 훨씬 낮아진다.
댓글 없음:
댓글 쓰기