
엔지니어를 AI로 대체하는 남은 2할
요약
AI가 작성하고 테스트까지 통과한 코드라도 실제 운영 환경에서의 비즈니스 로직 오류(환불 후 기능 유지)를 완벽히 잡아내지 못할 수 있음을 지적합니다. 엔지니어의 역할은 코드 작성을 넘어 실전 환경에서의 검증과 설계의 무결성을 확보하는 '남은 2할'에 있음을 강조합니다.
핵심 포인트
- AI는 코드 작성과 테스트 통과에는 능숙하지만, 비즈니스 로직의 결함을 놓칠 수 있음
- 엔지니어의 핵심 역량은 코드가 실제 운영 환경에서 의도대로 작동하는지 검증하는 것
- 우회 불가능한 설계를 위해 프로토콜 단계에서 특정 커맨드를 정의하지 않는 방식의 보안 설계 사례 공유
서론
AI에게 코드를 작성하게 하여, Windows용 셀프 블로커(Self-blocker)를 만들었습니다. 차단 중에는 본인이라도 언인스톨(Uninstall)할 수 없고, 태스크 킬(Task kill) 등으로도 해제할 수 없는 사양의 앱입니다.
출시 직전, 가상 머신에서 동작 확인을 하고 있었더니 이런 버그가 발생했습니다.
Stripe로 환불했는데, 앱을 재시작하면 Pro 기능을 다시 사용할 수 있다.
환불 보증을 제공하고 있는 이상, 이는 "환불해도 공짜로 계속 사용할 수 있다"는 뜻입니다. 방치하면 그대로 손실이 됩니다. 게다가 이 코드는 AI가 작성하고, AI가 리뷰했으며, 테스트 스위트(Test suite)는 모두 초록색(Pass)이었습니다. 그럼에도 불구하고, 실제 기기에서 환불을 시도해 보기 전까지는 검출할 수 없었습니다.
모두 초록색인 테스트를 본 뒤에 이런 일이 발생하니, 결국 인간이 확인해야만 하는구나라는 것을 느꼈습니다.
"AI가 엔지니어의 일자리를 빼앗을 것인가"라는 논의를 자주 봅니다. 이 기사는 실제로 해본 입장에서의 답변입니다.
먼저 결론을 쓰자면, 코드를 작성하는 업무에 대해서는 이미 8할 정도는 AI에게 맡겨도 된다고 생각합니다. 적어도 제가 혼자 쓰는 것보다 빠르고, 깔끔하며, 테스트도 두터웠습니다.
남은 2할이 이 기사의 이야기입니다. 작성한 코드가 실전(Production)에서 정말로 작동하는지를 확인하는 업무. 이것은 코드를 읽는 것만으로는 채워지지 않았습니다.
무엇을 만들었나
Vowkeeper라는 Windows 전용 셀프 블로커입니다. 지정한 앱이나 사이트를 지정한 일시까지 실행할 수 없게 만듭니다. 설정을 지워도, 앱을 종료시켜도, 언인스톨하려고 해도 멈추지 않습니다. 본인이라도 말이죠. 참고로 라이벌 앱에 비해 조작성이 좋습니다♡
기술 스택은 다음과 같습니다.
- Rust — Windows 서비스 본체 (차단을 강제하는 상주 프로세스)
- Tauri 2 + React — GUI (WebView2 기반)
- Cloudflare Workers + Stripe — 결제 및 라이선스 발행
「버튼을 무효화」하는 것이 아니라 「커맨드를 정의하지 않음」
셀프 블로커에서 가장 중요한 것은, 약해진 자신이 우회로를 찾을 수 없어야 한다는 점입니다.
솔직하게 만든다면, GUI에 「해제」 버튼을 두고 차단 중에는 그레이아웃(Gray-out) 시키는 형태가 됩니다. 하지만 GUI와 서비스는 네임드 파이프(Named Pipe)로 JSON을 흘려보내고 있을 뿐이며, 이 파이프는 동일한 PC의 사용자라면 연결할 수 있습니다. GUI를 통하지 않고 직접 메시지를 던지면, 그레이아웃은 아무런 장애가 되지 않습니다.
그래서 「차단을 지금 즉시 종료한다」는 커맨드를 프로토콜 측에 두지 않았습니다. 존재하지 않는 함수는 호출할 수 없습니다.
실제로 서비스가 받아들이는 IPC 커맨드는 다음과 같습니다.
// service/src/ipc/dispatcher.rs
Command::ExtendBlock(p) => handlers::extend_block(ctx, p), // 뒤로 미루는 것은 가능
Command::AddTarget(p) => handlers::add_target(ctx, p), // 대상은 추가 가능
...
ExtendBlock은 해제 시각을 뒤로 미룰 수는 있지만, 앞으로 당기는 커맨드는 없습니다. AddTarget으로 대상을 추가할 수는 있지만, 추가한 대상을 제거하는 커맨드도 없습니다. 움직일 수 있는 방향이 한쪽뿐인 구조입니다.
「취소」도 두 가지가 있지만, 둘 다 가동 중인 차단에는 관여하지 않습니다. CancelRelease는 해제 요청의 취소이며, 취소하더라도 차단은 계속됩니다. CancelReservation은 아직 시작되지 않은 예약을 지우는 것입니다.
빨리 끝낼 방법이 없는 것은 아닙니다. 결제하거나(RedeemUnlock), 3~24시간을 기다리거나(RequestRelease) 합니다. 다만 그 두 가지뿐이며, 둘 다 먼저 대가를 치르게 합니다. 무조건·즉시·무료로 끝내는 커맨드가 처음부터 존재하지 않는다. 이 부분이 설계의 핵심이었습니다.
또 다른 원칙은 「망설여지면 BLOCKING」입니다. 상태를 읽을 수 없거나, 시각을 신뢰할 수 없거나, 서명(Signature)을 검증할 수 없는 경우. 이상 계통은 모두 차단 지속 쪽으로 몰아넣습니다. "문제가 없으면 해제"가 아니라 "해제 조건을 충족함을 확인했으므로 해제". 페일 세이프(Fail-safe)의 방향을 항상 구속하는 쪽으로 설정했습니다.
AI와 어떻게 만들었나
코드는 거의 전부 AI가 작성했습니다. 저는 요건과 설계 판단을 결정하고, 구동하며 확인하는 역할을 맡았습니다. 한 줄 한 줄의 코드 리뷰는 하지 않았습니다. 저의 역할은 코드를 읽는 것이 아니라, 제품으로서 올바르게 작동하는지를 실제 기기와 진짜 돈으로 확인하는 것이었습니다.
구체적으로는, 출하 전에 가상 머신에서 이것을 한 차례 통과시키고 있습니다.
Pro를 실제로 구매 → 기능이 해제되는지 확인 → Stripe에서 환불 → 앱을 재시작 → Pro 기능이 해제되었는지 확인
각 단계에서 서버 측 KV(Key-Value)에 라이선스가 어떻게 기록되었는지, 만료 플래그(expiration flag)가 세워졌는지, 재시작 후에 로컬 상태가 어떻게 변했는지까지 확인하고 있습니다. 테스트가 통과(green)되더라도 이 공정을 거치기 전까지는 출하하지 않는다는 운영 방침을 세웠습니다.
먼저 말씀드리자면, 코드 품질이라는 측면에서 AI의 작업은 높은 수준이었습니다. 여기서 말하는 품질이란 버그의 유무뿐만 아니라 가독성(readability), 책임의 분리(separation of concerns), 해당 언어다운 작성 방식(idiomatic way), 테스트의 포괄성(test coverage) 등을 의미합니다. 구체적인 사례를 3가지 들겠습니다.
책임이 분리되어 있습니다. 서비스의 상태 관리는 거대한 파일 하나가 아니라 도메인별로 나누어져 있습니다. 결제(payment_ops.rs)와 시각 조작 방지 대책(clock_ops.rs)이 별도의 파일로 존재하므로, 한쪽을 변경해도 다른 쪽에 파급 효과가 적습니다.
service/src/state/
├── block_ops.rs 블록 생성·연장
├── license_ops.rs Free/Pro 라이선스
...
관용구(idiom)도 일관적입니다. Rust의 Mutex는 락(lock)을 보유한 동안 스레드가 패닉(panic)에 빠지면 오염된(poisoned) 상태가 되어 이후의 락 시도가 실패합니다. 여기서는 오염된 락에서 내부 값을 꺼내 처리를 계속하는 방식으로 통일되어 있어, "감시 루프를 절대로 멈추지 않는다"라는 제품의 철학과 맞닿아 있습니다.
let storage = self.storage.lock_recover();
참고로 이것은 처음에 self.storage.lock().unwrap_or_else(|e| e.into_inner())라는 식이 75곳에 복사되어 있었습니다. 작성 방식 자체는 맞지만, 동일한 식이 흩어져 있을 뿐 추상화되어 있지 않았습니다. 이 글을 쓰면서 깨달았기에 확장 트레이트(extension trait)로 정리했습니다. 식은 짧아지지만 의미론(semantics)은 동일하며, "왜 unwrap 하지 않는가"에 대한 설명이 트레이트 문서 한 곳에 모이게 됩니다.
요청하니 정리는 순식간이었습니다. 하지만 요청하지 않았다면 75곳 그대로였을 것입니다. 주어진 범위 내에서는 요령을 피우지 않습니다. 하지만 범위 밖으로는 스스로 나가지 않습니다. 나중에 나올 "Worker에 테스트가 제로였다"는 이야기도 같은 맥락이라고 생각합니다.
테스트와 정적 검사(static analysis)도 수행되고 있습니다. 서비스와 공통 라이브러리에서 360개의 Rust 테스트를 실행합니다. 약 20,500행의 코드가 cargo clippy 경고 제로를 유지하고 있으며, 커밋 전에 fmt / clippy / test를 통과하는 것을 매번 준수하고 있습니다.
코드의 깔끔함만 따진다면 제가 혼자 작성하는 것보다 분명히 뛰어났습니다.
단, 「코드를 읽으면 알 수 있는 품질」에 한함
방금 언급한 기준들에는 공통점이 있습니다. 설계, 관용구, 테스트, 가독성 모두 코드를 읽으면 알 수 있는 품질이라는 점입니다. AI는 코드를 읽는 데 능숙하므로 이 부분은 높은 수준을 유지할 수 있습니다.
하지만 품질에는 또 다른 종류가 있는데, 그것은 코드를 읽어도 알 수 없습니다.
- 실제 운영 환경에서, 실제 타이밍에 작동했을 때의 동작
- 실제 돈이 얽혔을 때의 경로
- 프로토타입의 더미(dummy) 데이터가 운영 환경에 섞여 들어가지 않았는지
- 빌드가 수정했을 것으로 생각되는 사항을 정말로 포함하고 있는지
이번 체제에서 실기(actual device)와 실제 돈을 다루는 것은 저뿐이었습니다. 코드를 읽고 정당성을 판단하거나 테스트를 작성하는 것은 AI도 할 수 있습니다. 하지만 환불 버튼을 누르고 앱을 재시작할 수는 없습니다.
출하 전에 겪은 4가지 사례
다음은 모두 코드 리뷰나 테스트가 아니라, 가상 머신에서 실제로 조작해 보고 나서야 발견된 것들입니다. 만료, 경합(race condition), 더미 혼입, 빌드의 불일치. 나열해 보면 모두 새로운 것이 아닙니다. 새로운 점은 AI가 작성한 깔끔한 코드 상태 그대로 그 문제들이 발생했다는 사실입니다.
(a) 환불했는데 Pro가 부활함
가장 심각했던 사례입니다.
Stripe에서 환불 → 앱 재시작 → Pro 기능이 다시 사용 가능.
여기서 아까 "360개의 테스트가 있다"라고 말한 것을 떠올려 보십시오. 그 360개는 전부 Rust 서비스 측 테스트입니다. 라이선스를 실제로 발행·만료시키는 결제 로직은 Cloudflare Workers 상의 662줄짜리 JavaScript에 있었고, 그곳의 테스트는 제로였습니다. 환불 버그는 바로 그 테스트가 제로였던 Worker에 있었습니다.
이것은 무효화 (revocation)라는, 분산 시스템의 고전적인 난제입니다. 한 번 발행한 것을 나중에 무효화하는 것. 말하기는 쉽지만, 상태가 Stripe, KV, 클라이언트, 로컬의 3중 중복 저장소 (redundant storage)에 흩어져 있으면 삭제 누락이 발생합니다.
AI가 처음에 작성한 것은 발행된 코드를 전부 찾아다니며 삭제하는 방식이었습니다. 깔끔하게 작성되어 있었습니다. 하지만 삭제 누락 경로가 세 가지 (claim_id 경유, 수동 무효화, webhook의 재발행) 있었고, 그중 하나라도 남으면 Pro 플랜이 복구됩니다.
더 까다로운 것은 Stripe의 사양입니다. 환불 후에도 Checkout Session의 payment_status는 `
이것만큼은 코드의 버그가 아닙니다. 그럼에도 같은 이야기의 또 다른 형태이기에, 네 번째 사례로 나열합니다.
위의 버그를 수정하고, 빌드하여, 배포했습니다. 그런데도 수정되지 않았습니다.
원인은 빌드 절차의 허점이었습니다. npm run tauri build는 GUI를 빌드하지만, 서비스 측의 바이너리(binary)는 빌드하지 않습니다. 그래서 며칠 전의 오래된 서비스가 서명된 상태로 패키징되었고, 수정 사항이 2 버전이나 뒤처진 채 사용자에게 전달되었습니다.
빌드 결과물이 소스 코드의 최신 상태와 일치하지 않는 불일치(inconsistency) 문제입니다. 그리고 다시 한번, 실제 기기에서 "수정했을 터인 동작이 수정되지 않음"을 확인하기 전까지는 감지할 수 없었습니다. 빌드 스크립트에 패키징된 바이너리의 버전을 검증하여 오래되었다면 중단하는 처리를 추가함으로써, 구조적으로 재발을 방지했습니다.
"E2E 테스트를 작성했다면 막을 수 있지 않았을까?"
여기까지 읽고 이렇게 생각한 분이 있을 것입니다. 그것은 AI의 한계가 아니라 테스트 설계의 미비이며, 실제 Stripe의 환불을 포함한 E2E(End-to-End) 테스트를 구성했다면 전부 막을 수 있었을 것이라고 말입니다.
E2E가 유효하다는 것은 사실입니다. 환불을 실행하고 라이선스 상태를 검증하는 E2E, 실행 순서를 흔드는 테스트, 최종 확인 화면의 스냅샷 테스트(snapshot test). 모두 구축 가능했으며, 다음 과제이기도 합니다.
하지만 그것으로 이야기가 끝나지는 않습니다. E2E에 무엇을 작성할지를 누가 결정하는가라는 문제가 한 단계 더 높은 차원에서 남기 때문입니다.
이 프로젝트에는 360개의 테스트가 있었습니다. 작성한 것은 AI입니다. 설계의 분리도, 관용구(idiom)도, 테스트의 망라성(coverage)도 높은 수준이었습니다. 그럼에도 불구하고, 라이선스를 발행 및 만료시키는 662줄의 Worker에는 테스트가 단 하나도 없었습니다. 저도 인지하지 못했고, AI도 보고하지 않았습니다. 테스트를 두텁게 작성할 수 있는 AI가, 결과적으로 가장 금전과 밀접한 코드만을 공백으로 남겨두었던 것입니다.
설령 그 Worker에 E2E를 작성했다 하더라도 다음의 벽이 있습니다. 환불 후에도 Stripe의 payment_status는 `
「paid를 반환하므로 주의할 것」이라고 한 줄 적어두면 첫 번째 케이스는 방지할 수 있습니다. 하지만 그것은 제가 그 특성을 알고 있었을 경우의 이야기이며, 알고 있었다면 처음부터 적었을 것입니다. 지시(Instruction)도 테스트도, 작성할 수 있는 범위는 스스로 의심할 수 있는 범위까지입니다.
지시 대신 놓은 것
그래서 준비한 것은 프롬프트(Prompt)가 아니라, 출하 전에 직접 손으로 움직여 확인하는 항목들입니다. 4건의 사례로부터 역산하면 다음과 같습니다.
- 외부 서비스의 상태는 SDK의 반환값이 아니라 관리 화면의 실물과 대조한다. 반환값만 보고 있으면 모크(Mock)와 함께 빠져나갑니다.
- 기동 순서에 의존하는 처리는 빠른 환경과 느린 환경 양쪽 모두에서 실행한다. 개발 환경에서는 나타나지 않는 것이 실기(Real device)에서 나타납니다.
- 프로토타입 유래의 고정값을 grep으로 찾아낸다. 고정 날짜, 샘플 문자열,
TODO등은 그대로 동작해 버리기 때문에, 직접 찾아 나서지 않으면 발견할 수 없습니다. - 빌드가 모든 결과물을 생성하고 있는지 버전 번호로 대조한다. 소스 코드가 올바르더라도 출하물이 오래되었다면 의미가 없습니다.
화려하지는 않습니다. 다만, 4개 항목 모두 실제로 겪어봤기에 쓸 수 있었던 것들입니다.
AI와 인간의 경계선
이번 프로젝트에서 업무는 다음과 같이 나뉘었습니다.
AI가 강했던 부분은 코드를 읽으면 알 수 있는 품질입니다. 설계의 분리, 이디엄 (Idiom), 가독성, 그리고 눈앞에 있는 코드의 테스트 커버리지 (Test coverage). 피드백 루프 (Feedback loop)가 수중에 닫혀 있는 영역에서는 혼자 작성하는 것보다 명백히 고품질이었습니다. 뒤집어 말하면, 시야 밖은 공백으로 남았습니다. 다른 언어로 작성되었고, 배포 대상도 다르며, 수중에서 실행할 수 없는 Worker가 바로 그것입니다.
인간에게 남은 것은 코드를 읽어도 알 수 없는 품질입니다. 실기의 상태, 기동 타이밍, 실제 돈이 얽힌 경로, 외부 서비스의 현실적인 특성, 프로토타입 더미 데이터의 혼입, 빌드와 소스의 일치 여부입니다.
흥미로운 점은 이것이 AI가 우수한가에 대한 이야기가 아니라는 것입니다. AI는 우수했습니다. 그럼에도 4건의 사례는 모두 코드를 읽는 것만으로는 도달할 수 없는 곳에 있었습니다. 환불 후에도 paid가 반환되는 것은 Stripe의 사정이며, 코드에는 적혀 있지 않습니다. 기동 순서의 경합(Race condition)은 실행해 봐야 비로소 존재합니다. 코드를 아무리 정독해도 그곳에는 없습니다.
「AI가 엔지니어의 일자리를 빼앗을 것인가」에 대한 저의 대답은 이렇습니다. 코드를 작성하는 일은 이제 상당히 잘할 수 있습니다. 빼앗기지 않은 것은, 그것이 실제 환경에서 정말로 동작하는지 확인하는 일입니다. 적어도 지금으로서는 말입니다.
한계에 대하여
마지막으로, 제품으로서의 한계도 적어둡니다.
이 앱은 「절대로 해제할 수 없다」고 주장하지 않습니다. 세이프 모드 (Safe mode)로 부팅하기, OS 재설치하기, 디스크 분리하기. 이러한 수단으로는 빠져나갈 수 있습니다. 약관에도 명시되어 있습니다. 할 수 있는 것은, 빠져나가는 비용이 충동적으로 실행하기에는 너무 높도록 만드는 것뿐입니다. '절대'를 팔지 않는 것 또한 하나의 성실함이라고 생각합니다.
그럼에도 기술로 담보하는 장치도 넣었습니다. 해제 과금에는 주간 하드 캡 (Hard cap)이 있으며, 과금 합계는 본인이 Pro 버전에 지불한 금액을 1원도 초과하지 않습니다. 인스톨러는 실명 코드 사이닝 (Code signing) 인증서로 서명되어 있습니다. 금전과 관련된 버그를 출하 전에 없애는 공정을 판매 전 필수 절차로 만들었습니다.
광고: 이 기사에 등장하는 Vowkeeper는 현재 베타 가격으로 판매 중입니다 (Windows 10/11 · 일시불 구매). 의지력만으로는 이길 수 없다고 느끼시는 분들은 한번 살펴보시기 바랍니다.
마찬가지로 「AI에게 코드를 작성하게 하여 무언가를 파는 것」을 생각하고 있는 분이 있다면, 전하고 싶은 것은 하나입니다. AI가 작성한 코드의 깔끔함은 이제 의심하지 않아도 될 수준에 와 있습니다. 의심해야 할 것은 작성되지 않은 코드입니다. 662행의 Worker에 단 하나도 없었던 테스트와 같은 공백은, 내버려 두면 아무도 지적하지 않습니다. 무엇이 작성되지 않았는지, 그리고 그것이 운영 환경(Production)에서 정말로 동작하는지 누가 확인했는가. 남아 있는 일은 바로 그 지점입니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기