설계부터 결정론적으로: LLM 없이 수행하는 코드 리뷰
요약
산업 제어 코드(PLC) 리뷰를 위해 LLM의 비결정론적 특성 대신 결정론적 접근 방식을 채택한 사례를 다룹니다. 물리적 비용과 안전이 직결된 환경에서는 LLM의 유창함보다 일관된 결과 도출이 중요함을 강조합니다.
핵심 포인트
- 산업 제어 코드에서는 LLM의 비결정론적 특성이 위험할 수 있음
- 물리적 장치를 제어하는 코드에는 일관된 결과가 필수적임
- LLM의 환각 현상은 안전 관련 코드의 머지 게이트로 부적합함
- 결정론적 린터는 동일 입력에 대해 항상 동일한 결과를 보장해야 함
지난 2년 동안 출시된 모든 코드 리뷰 도구들은 동일한 단어, 즉 AI를 앞세우는 것처럼 보입니다. 모델을 diff(차이점)에 적용하면 무엇이 잘못되었을 수 있는지에 대한 산문 형태의 설명을 얻게 됩니다. 많은 코드의 경우, 이는 진정으로 유용합니다.
저는 최근에 코드 리뷰 도구를 하나 만들었는데, 의도적으로 LLM (Large Language Model)을 제외했습니다. 제가 LLM을 싫어해서가 아닙니다. 저도 매일 사용하니까요. 하지만 제가 목표로 한 코드는 비결정론적 (non-deterministic) 리뷰어가 적절하지 않은 도구가 되게 만드는 속성을 가지고 있습니다. 바로 기계를 작동시킨다는 점이며, 잘못되거나 일관되지 않은 답변은 물리적인 비용을 초래합니다.
이 글은 그 결정에 관한 것이며, 왜 이 사례에서는 유창함 (fluency)보다 결정론 (determinism)이 더 중요했는지, 그리고 LLM이 여전히 자리를 차지할 수 있는 곳은 어디인지에 대해 다룹니다.
사례 연구: 산업 제어 코드
plc-st-review라는 도구는 IEC 61131-3 Structured Text (ST)를 리뷰합니다. 이는 전 세계 공장, 정수 처리장, 공정 라인의 상당 부분이 프로그래밍되는 언어입니다. 여기서 발생하는 버그는 웹 페이지의 500 에러가 아닙니다. 너무 빠르게 돌아가는 컨베이어 벨트, 타임아웃이 조용히 변경된 안전 인터록 (safety interlock), 또는 아예 시작되지 않는 펌프와 같은 문제입니다.
이러한 사례의 유명한 극단적인 예는 Stuxnet입니다. 이는 이란의 우라늄 농축 원심분리기를 구동하는 PLC 로직을 조용히 변경하여 원심분리기가 손상을 입을 정도의 속도로 회전하게 만들었습니다. 그러면서 운영자들에게는 정상적인 센서 값을 재생하여 아무런 문제가 없는 것처럼 보이게 했습니다. 폭발은 없었지만, 수개월에 걸쳐 원심분리기가 스스로 파괴되었습니다. 그것은 자신을 숨기도록 설계된 국가 주도의 악성 코드였으므로, 분명히 말하자면 어떤 린터 (linter)도 이를 잡아내지 못했을 것입니다. 하지만 잘못된 숫자로 인한 물리적 결과를 초래하기 위해 반드시 국가급 공격자가 필요한 것은 아닙니다. 일반적인 변경 사항에서 타이머 설정값이 T#2s 대신 T#200ms로 입력되는 것만으로도 충분하며, 이것이 바로 코드 리뷰가 잡아내야 하지만 일상적으로 놓치는 바로 그런 종류의 일입니다.
이 논지를 따라가는 데 Structured Text를 알 필요는 없습니다. 핵심은 제약 조건(constraint)에 있습니다. 이것은 "아마 괜찮을 것"이라는 리뷰 결과가 허용되지 않는 코드이며, 동일한 입력에 대해 매번 반드시 동일한 답변을 생성해야 하는 코드입니다.
왜 여기서는 결정론(determinism)이 유창함(fluency)을 압도하는가
CI 파이프라인의 게이트(gate) 역할을 하는 린터(linter)는 다음과 같은 약속을 합니다. 동일한 코드는 오늘이나 6개월 후나, 내 컴퓨터에서든 빌드 서버에서든 동일한 결과(findings)를 도출한다는 것입니다. 팀이 "빌드가 실패(red)했으므로 머지(merge)를 차단한다"라고 말하고 이를 신뢰할 수 있는 이유는 바로 이 약속 덕분입니다.
LLM 리뷰어는 그러한 약속을 할 수 없습니다. 동일한 디프(diff)라도 실행할 때마다 다른 출력을 생성할 수 있습니다. 존재하지 않는 문제를 환각(hallucinate)하거나, 존재하는 문제를 놓칠 수도 있습니다. 온도(Temperature), 모델 버전(model version), 컨텍스트 윈도우(context window)가 모두 결과에 영향을 미칩니다. 탐색적 리뷰(exploratory review)를 위해서는 괜찮은 트레이드오프(trade-off)일 수 있습니다. 하지만 안전 관련 코드의 머지 게이트(merge gate)로서는 결격 사유입니다. 때로는 차단하고 때로는 차단하지 않는 게이트는 게이트가 아니기 때문입니다.
결정론은 여기서 자연어보다 더 중요한 네 가지를 저에게 가져다주었습니다:
재현성 (Reproducibility). 모든 결과(finding)는 파스 트리(parse tree)의 순수 함수(pure function)입니다. 천 번을 실행해도 천 번 모두 동일한 결과를 얻습니다. CI는 이를 신뢰할 수 있습니다.
감사 가능성 (Auditability). 도구가 무언가를 지적할 때, 이는 명명된 규칙(named rule)과 이를 트리거한 정확한 노드(node)를 가리킵니다. 규제 환경에서는 결국 누군가가 "왜 이것이 실패했습니까?"라고 물을 것입니다. "PT가 T#2s에서 T#200ms로 변경되었기 때문에 TIMER_VALUE_CHANGED라는 규칙이 실행되었습니다"는 답변이 됩니다. "모델이 위험해 보인다고 느꼈습니다"는 답변이 될 수 없습니다.
데이터 유출 방지 (No data leaving the building). 산업 현장에서는 제어 코드(control code)를 제3자 API로 전송하는 것에 대해 (당연하게도) 매우 민감합니다. 로컬에서 파싱하고 외부 호출을 전혀 하지 않는 도구는 구매 부서와의 싸움 없이도 이 기준을 통과합니다.
제로에 수렴하는 비용과 지연 시간 (Cost and latency that round to zero). 도구는 파싱하고 트리를 순회(walk)할 뿐입니다. 토큰(token)도, 속도 제한(rate limit)도, 리뷰당 청구되는 비용도 없습니다. 아무도 계량기를 지켜볼 필요 없이 모든 푸시(push)마다 실행됩니다.
실제 작동 방식
마법 같은 것은 없으며, 그것이 바로 핵심입니다. 이 파이프라인은 의도적으로 지루하게 설계되었습니다.
- 각
.st파일을 tree-sitter grammar를 사용하여 구문 트리 (syntax tree)로 파싱 (parse)합니다. 텍스트에 대한 정규 표현식 (regex)이 아닌, 실제 파싱을 수행합니다. - 리비전 (revision)별로 심볼 테이블 (symbol table)을 구축합니다: 모든 프로그램 유닛과 그 매개변수 시그니처 (parameter signature), 전역 변수 (global variables), 열거형 (enums), 타이머 인스턴스 (timer instances), 호출 지점 (call sites), CASE 문 등을 포함합니다.
- 이 구조화된 모델을 각 체크 (check)에 전달합니다. 체크는 트리와 심볼 테이블을 살펴보고 발견 사항을 반환하는 작고 독립적인 함수입니다.
- 풀 리퀘스트 (pull request) 리뷰의 경우, 변경 사항의 이전 버전과 이후 버전 모두에 대해 위의 과정을 수행한 뒤 두 모델을 디프 (diff) 합니다.
마지막 단계가 바로 이 시스템의 가치를 증명하는 부분입니다. 단일 리비전 분석기는 타이머가 존재한다는 사실을 알려줄 수 있습니다. 하지만 두 리비전을 비교하면, 이 특정 커밋에서 타이머의 설정값이 2초에서 200밀리초로 10배 빨라졌다는 것을 알려줍니다. 이는 육안 리뷰에서는 통과되지만 실제 운영 환경(production)에서 기계를 오작동하게 만드는 바로 그 한 글자 오타와 같은 사례입니다.
텍스트 매칭 (text matching) 대신 실제 모델을 가짐으로써 얻을 수 있는 몇 가지 추가 사례는 다음과 같습니다:
- 출력값은 읽고 있지만 아무도 호출하지 않아, 결국 오래된 값 (stale values)을 읽게 되는 기능 블록 (function block) 인스턴스.
- 선언된 범위를 벗어난 리터럴 배열 인덱스 (literal array index).
- 이름이
SAFETY_로 시작하는 상수의 값이 변경되었을 때, 접두사(prefix)로 인해 더 높은 심각도 (severity)로 표시되는 경우. - 필수 입력값이 늘어났으나 일부 호출 지점 (call sites)만 업데이트된 함수.
이 중 그 어떤 것도 언어 모델 (language model)을 필요로 하지 않습니다. 이들에게 필요한 것은 코드에 대한 정확한 모델과 규칙 (rule)입니다.
LLM이 적합한 위치
이 부분에 대해서는 솔직해지고 싶습니다. 왜냐하면
LLM이 명확하게 도움이 되는 곳이 한 군데 있습니다. 바로 도메인 전문가가 아닌 사람에게 발견된 사항을 설명하는 것입니다. EDGE_TRIG_REUSED를 읽고 있는 주니어 엔지니어는 서로 다른 두 개의 클록 표현식(clock expressions)에서 하나의 R_TRIG 인스턴스를 공급하는 것이 왜 문제인지 모를 수 있습니다. 모델은 간결하고 정확한 발견 사항을 평이한 영어 문단으로 변환하는 데 탁월합니다.
그래서 제가 결정한 설계 규칙은 다음과 같습니다. LLM은 절대로 발견 사항을 생성하지 않습니다. LLM은 결정론적 엔진(deterministic engine)이 이미 생성하고 특정 노드에 근거를 둔 사항을 의역(paraphrase)할 뿐입니다. 결정론(Determinism)이 진실의 근원(source of truth)으로 남으며, 모델은 그 위에 얹혀진 선택적인 번역 계층(translation layer)입니다. 이를 통해 출력물을 이해하기 쉽게 만들면서도 검증 게이트(gate)의 신뢰성을 유지합니다. 이는 로드맵상에서 기본적으로는 꺼져 있고, 통과(pass) 또는 실패(fail)를 결정하는 경로에는 절대 관여하지 않는, 엄격하게 부가적인 --explain 플래그로 구현될 예정입니다.
모델은 설명할 수는 있지만 결코 결정할 수는 없다는 이 경계가 바로 논문의 핵심 주제입니다. 결정론적 핵심(deterministic core)이 정확성과 머지 게이트(merge gate)를 담당하게 하십시오. LLM은 가끔 틀리더라도 비용이 들지 않는 유창함(fluency)을 담당하게 하십시오.
PLC를 넘어 얻을 수 있는 교훈
현재의 반사적인 반응은 모델을 먼저 찾고, 나중에 무엇을 건드리지 말아야 할지 묻는 것입니다. 리뷰 결과가 실질적인 무언가를 제어하는 모든 코드에 대해서는 이를 뒤집어 생각할 가치가 있다고 생각합니다. 무엇이 결정론적이고 감사 가능(auditable)해야 하는지 결정하고, 모델 없이 그 부분을 구축한 다음, 틀린 답이 나와도 비용이 적게 드는 곳에만 LLM을 추가하십시오.
모든 것을 AI가 리뷰해서는 안 됩니다. 어떤 것들은 매번 동일한 답을 내놓고 왜 그런지 정확히 알려줄 수 있는 규칙(rule)에 의해 리뷰되어야 합니다.
체크 사항들을 확인하고 싶다면 도구는 오픈 소스(MIT)로 공개되어 있습니다: https://github.com/HeytalePazguato/plc-st-review
다른 사람들은 이 경계선을 어디에 긋는지 궁금합니다. 여러분의 스택에서 의도적으로 결정론적으로 유지하는 것은 무엇이며, 어디에 모델을 도입하셨나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기