← 에세이 /Post · 15 of 21 · Embedded Dev

AI가 짠 펌웨어: 어디까지 믿어도 될까?

LLM에게 로깅 데몬을 짜게 했다. 컴파일 OK, 동작 OK. 코드 리뷰에서 eMMC를 죽이는 고빈도 write를 발견했다. 프롬프트에 말하지 않은 도메인 지식이 진짜 위험 구간이다.

·3 min read · · · #llm#embedded-dev#firmware#ai-agents
AI가 짠 펌웨어: 어디까지 믿어도 될까?
On this page

LLM이 짠 임베디드 코드는 틀려서 위험한 게 아니다. 조용히 틀려서 위험하다. 명시적으로 시키면 대부분 맞춘다. 하지만 암묵적 도메인 지식이 필요한 순간, 통과율이 뚝 떨어진다. 이 격차를 기존 코딩 벤치마크는 안 잡는다. 코드 리뷰에서 걸린 로깅 데몬 하나가 나한테 그걸 가르쳐줬다.

eMMC를 조용히 죽이는 코드

i.MX8M Mini BSP 프로젝트에서 주니어 엔지니어 코드를 리뷰하던 중이었다. CAN 로깅 데몬. AI 코딩 툴로 뽑은 코드라는 게 한눈에 보였다. 200줄이 넘는데 시그널 핸들링, 데몬화, PID 파일 관리까지 하나도 안 빠진 프로덕션급 구조였다. 주니어가 이 완성도로 짜지는 못한다. 핵심 루프만 보자.

/* AI가 생성한 CAN 로깅 데몬 (핵심 루프) */
#define DEFAULT_LOGFILE  "/var/log/can.log"
#define LOG_FLUSH_INTERVAL 10  /* seconds */

FILE *fp = fopen(logfile, "a");   /* logfile = "/var/log/can.log" */
alarm(LOG_FLUSH_INTERVAL);        /* 10초마다 SIGALRM으로 flush */

while (running) {
    struct can_frame frame;
    ssize_t nbytes = read(sock, &frame, sizeof(frame));
    if (nbytes < 0) {
        if (errno == EINTR) {
            if (flush_flag) { fflush(fp); flush_flag = 0; alarm(LOG_FLUSH_INTERVAL); }
            continue;
        }
        break;
    }
    log_frame(fp, iface, &frame, &ts);

    if ((frame_count + err_count) % 1000 == 0)
        fflush(fp);  /* 1000프레임마다 flush */
}

잘 짜여 있다. 매 프레임마다 fflush()를 때리는 대신 10초 주기에 1000프레임 간격으로 버퍼링한다. 에러 핸들링도 챙겼고 시그널로 graceful shutdown도 된다. “깔끔하네” 하고 approve 누를 뻔했다.

그런데 손이 멈췄다. /var/log/can.log. 이게 eMMC 위다.

우리 시스템은 CAN 메시지가 100ms 주기로 들어온다. 1000프레임 버퍼링이면 대략 100초마다 flush다. 여기에 10초 alarm까지 겹치면 하루 eMMC write가 수천 회다. 한 파일에 계속 append하는 패턴은 wear leveling이 깔려 있어도 같은 블록 그룹에 write가 몰린다. MLC/TLC eMMC의 P/E cycle이 야금야금 닳고, 운 없으면 수개월 뒤 eMMC가 read-only로 넘어가거나 부팅 파티션까지 물고 들어간다.

수정은 간단하다. /var/log/ 대신 tmpfs에 쓰고, eMMC에는 주기적으로만 내려주면 된다.

/* 수정: tmpfs 버퍼 + 주기적 eMMC flush */
#define DEFAULT_LOGFILE  "/tmp/can_buffer.log"  /* tmpfs (RAM) */
#define EMMC_FLUSH_INTERVAL 3600                /* 1시간마다 eMMC에 저장 */

하루 수천 회 write가 24회로 뚝 떨어진다. logrotate 걸어서 오래된 파일도 정리한다. 이걸 리뷰에서 놓쳤으면 현장에 깔린 지 수개월 뒤에야 알았을 것이다.

파일 I/O 자체는 흠잡을 데 없었다. 시그널 핸들링, 버퍼링, 에러 처리까지 다 프로덕션급이다. 문제는 이 코드가 “eMMC 위에서 돈다”는 사실이 어디에도 반영 안 됐다는 점이다. 서버였으면 완벽한 코드다. 임베디드에서는 하드웨어를 죽인다. AI 툴한테 “eMMC 수명 생각해라”고 안 적었으니, 걔가 그걸 챙길 리가 없다.

더 큰 문제는 주니어 본인도 이걸 몰랐다는 점이다. AI가 깔끔하게 짜줬으니 맞겠거니 했다. 리뷰에서 걸러줄 시니어가 없었으면 그대로 현장에 나갔다.

35%p: 벤치마크가 숨기는 격차

이게 로깅 하나로 끝나는 문제가 아니었다. 같은 패턴이 반복되는지 궁금해서, 20개 카테고리에 카테고리당 10개쯤 임베디드 테스트 케이스를 만들었다. 그걸 두 가지 버전으로 돌렸다.

  • Explicit: “volatile int flag를 선언하고, ISR에서 설정해라” — 대부분 pass
  • Implicit: “ISR과 메인 스레드 간에 flag로 통신하라” (volatile 언급 없이) — 통과율이 눈에 띄게 떨어짐

내 테스트에서 이 격차는 35%p 정도 나왔다. 물론 검증은 regex 기반 heuristic이라 런타임까지 돌린 게 아니고, 테스트 케이스도 Zephyr 쪽에 쏠려 있다. 정확한 숫자는 중요하지 않다. 핵심은 패턴이다. explicit 프롬프트와 implicit 프롬프트 사이에서 격차가 크게 벌어진다는 것.

이 테스트들은 나중에 컴파일과 런타임 레이어까지 붙은 233 케이스 벤치마크 EmbedEval로 자랐다. 거기서 Sonnet 4.6은 68.0%, Haiku 4.5는 56.9%가 나왔고, 두 모델이 무너지는 자리는 여전히 implicit 지식 카테고리다.

기존 코딩 벤치마크(HumanEval, SWE-bench)는 죄다 explicit 스타일이다. “이 함수를 구현하라”고 콕 집어 지시한다. 하지만 임베디드의 진짜 난이도는 volatile, eMMC 수명, cleanup 순서, 타이밍 마진처럼 프롬프트에 안 적는 것에 몰려 있다. 이걸 재는 벤치마크는 거의 없다.

“LLM이 90% 맞춘다”는 숫자를 보면 이걸 물어야 한다. 그 90%는 답이 프롬프트 안에 이미 들어 있는 90%인가, 아니면 도메인 지식을 스스로 추론해야 하는 90%인가?

같은 격차가 논문에도 있다

내 테스트에서만 나타나는 게 아니다. EmbedAgent(2025, arXiv preprint)를 보면 LLM의 pass@1이 MicroPython(Raspberry Pi Pico)에서 73.8%인데 ESP-IDF에서는 29.4%까지 주저앉는다. MicroPython은 pin.value()처럼 API가 대놓고 명시적이다. ESP-IDF는 다르다. esp_err_t 체크, component 시스템, menuconfig 같은 암묵적 규칙이 잔뜩 깔려 있다. 결국 같은 implicit knowledge 문제다.

IoT-SkillsBench(2026)는 한술 더 뜬다. LLM이 생성한 “도움말 컨텍스트”를 넣었더니 Zephyr 성능이 되레 나빠졌다. LLM이 자기 잘못된 가정을 스스로 강화한 셈이다. 반면 인간 전문가가 쓴 컨텍스트는 점수를 39/42까지 끌어올린다. 아무 컨텍스트나 쑤셔넣으면 독이 된다. 실패 경험에서 뽑아낸 정확한 컨텍스트, 그것만 먹힌다.

그래서 뭘 바꿨나

그 리뷰 이후 딱 하나 바꿨다. 프로젝트 설정 파일(CLAUDE.md)에 silent failure checklist를 만들었다.

## Silent Failure Checklist
- eMMC write -- 고빈도 write는 tmpfs 버퍼링 필수. 직접 fflush 금지
- 에러 경로 -- init 순서의 역순 cleanup을 거의 안 한다
- IRQ context -- blocking 호출(mutex, sleep)을 넣는다
- Device Tree -- pinctrl, clock 필수 속성을 빠뜨린다
- 타이밍 마진 -- 타임아웃 = 주기로 설정한다 (마진 0)

항목은 리뷰에서 하나 걸릴 때마다 한 줄씩 붙는다. 이 체크리스트를 넣고 3개월, 비슷한 코드를 수십 번 뽑았는데 같은 유형의 실패는 다시 안 났다. 데이터시트를 통째로 RAG에 넣는 것보다 이 5줄짜리 체크리스트가 낫다. 512페이지 레퍼런스 매뉴얼에서 “eMMC write endurance 주의” 한 줄을 골라내는 건 LLM이 못 한다. 하지만 체크리스트 한 줄은 읽는다.

완벽한 해결책이냐고? 아니다. 내가 아는 함정만 적을 수 있다. 모르는 함정은 여전히 그 격차 안에 숨어 있다. 그래도 함정이 쌓일수록 CLAUDE.md는 “암묵적 지식을 명시적으로 바꿔놓은 문서”로 자란다. 엔지니어 경험이 두꺼울수록 이 파일도 두꺼워지고, LLM한테 맡길 수 있는 안전 영역이 넓어진다.

LLM이 짠 임베디드 코드를 볼 때 첫 질문은 “컴파일 되나?”가 아니다. 첫 질문은 “여기서 내가 말 안 한 게 뭐지?”다. 그 답이 안 떠오르는 영역이라면, 아직 그 코드를 LLM한테 맡길 준비가 안 됐다는 뜻이다.

이 문제를 context engineering으로 푸는 구체적인 방법은 Context Engineering에서 따로 다뤘다.

ecro
글쓴이 ecro

LLM 펌웨어 벤치마크를 만들었습니다. EmbedEval →

프로젝트에 맞는 Claude Code / Cursor / Codex 하네스를 생성하는 도구를 만듭니다. harness-maker →

Datasheet을 읽는 터미널을 만들고 있습니다. NeuroTerm →

댓글

댓글 불러오는 중...