프로젝트 BF-llama: 런타임 에러 제로(Zero)를 향하여 – ‘객체지향의 환상과 무자비한 컴파일러의 심판’

서문: 타협의 시대는 끝났다

“우리는 오랫동안 가비지 컬렉터(Garbage Collector)라는 거대한 기만 위에 시스템을 쌓아 올렸습니다.”

메모리가 새면 주기적으로 서버를 재시작하고, 알 수 없는 런타임 에러가 터지면 try-catch 블록으로 덮어버린 채 에러 로그나 남기며 안도했습니다. 파이썬, 자바스크립트, 자바 생태계가 제공하는 안락한 객체지향(OOP)의 추상화 뒤에 숨어, 내가 선언한 데이터가 스택에 쌓이는지 힙에 흩어지는지 통제권을 완전히 포기한 대가입니다.

하지만 프로젝트 BF-llama에서는 이러한 안일함이 통용되지 않습니다.

이 시스템은 초당 5만 건(50,000 TPS)의 거대 언어 모델(LLM) 추론 요청을 논블로킹으로 라우팅해야 하는 핵심 게이트웨이입니다. 여기서 발생하는 단 1밀리초의 ‘GC Pause(가비지 컬렉터 정지)’나, 멀티스레드 환경의 사소한 데이터 레이스(Data Race)는 곧바로 전체 클러스터의 P99 지연 시간 폭발로 이어집니다.

과거의 우리는 속도를 위해 C/C++의 날것을 다루다 ‘세그멘테이션 폴트(Segmentation Fault)’의 공포에 떨거나, 안전을 위해 무거운 런타임 오버헤드를 견디는 양극단의 선택을 강요받아 왔습니다.

러스트(Rust)는 이 타협을 단호히 거부합니다.

이 소설, <프로젝트 BF-llama: 런타임 에러 제로를 향하여>는 단순한 문법서가 아닙니다. 동적 타입 언어의 무한한 유연성에 중독된 주니어 백엔드 개발자 탐(Tom)과, 위험한 포인터 곡예를 즐기는 C++ 출신의 주니어 시스템 개발자 메리(Mary)가, 무자비한 컴파일러의 화신인 시니어 아키텍트 저스틴(Justin)과 충돌하며 시스템 프로그래밍의 뼈대를 다시 세우는 치열한 코드 리뷰의 기록입니다.

러스트의 컴파일러, 이른바 ‘보로우 체커(Borrow Checker)’는 자비가 없습니다. 소유권(Ownership)의 이동을 증명하지 못하거나, 생명주기(Lifetime)가 엇갈리거나, 스레드 간의 안전성(Send/Sync)을 보장하지 못하는 코드는 단 한 줄의 기계어로도 번역되지 않습니다. 여러분 역시 이 이야기 속의 탐과 메리처럼, 수많은 빌드 실패의 붉은 텍스트 앞에서 절망할 것입니다.

하지만 명심하십시오. 러스트 컴파일러는 여러분의 적이 아닙니다. 런타임에 고객의 데이터를 날려버리기 전에, 컴파일 타임에 여러분의 논리적 결함과 오만을 가차 없이 박살 내주는 가장 완벽하고 차가운 멘토입니다.

“가비지 컬렉터(GC)와 동적 타입이 유발하는 불확실성의 탑(Tower) 대(對) 소유권(Ownership)과 트레잇(Trait)으로 맞물린 BF-llama의 무결점 기어 시스템.”

 

런타임 에러 제로(Zero). 오버헤드 제로(Zero). 이 불가능해 보이는 수치를 향해, 객체지향의 환상을 버리고 메모리의 지배자로 거듭날 준비가 되셨습니까?


 

1장. 프롤로그와 탐의 절망: “가비지 컬렉터가 없다고요?”

“BF-llama 프로젝트의 목표는 명확하다. 초당 5만 건의 LLM 추론 요청을 라우팅하며, P99 지연 시간(Latency)은 2ms 미만을 유지해야 한다. 가비지 컬렉터(GC)가 메모리를 청소하느라 발생하는 ‘GC Pause(정지 현상)’는 단 1밀리초도 허용하지 않는다.”

회의실 모니터 앞에 선 저스틴의 목소리는 건조하고 단호했습니다. 파이썬과 Node.js 생태계에서 3년을 구르다 합류한 탐(Tom)은 자신만만하게 키보드를 잡았습니다.

“저스틴, 걱정 마세요. 설정 객체를 만들어서 라우터 함수에 넘겨주는 기본 뼈대는 제가 금방 짤 수 있습니다. 자바스크립트에서 수천 번은 해본 패턴이니까요.”

탐은 익숙한 객체지향의 감각으로 BF-llama의 첫 번째 초기화 코드를 작성했습니다.


탐의 습관 (Anti-Pattern)

탐이 작성한 코드는 모델의 설정을 담은 구조체를 생성하고, 이를 실행 함수에 전달한 뒤, 로그를 남기는 아주 단순한 로직이었습니다.

“자, 실행합니다.” 탐이 호기롭게 cargo run을 입력했습니다. 하지만 화면에 나타난 것은 런타임 결과가 아니라, 러스트 컴 컴파일러의 핏빛 에러 메시지였습니다.

“어? 이게 무슨 소리죠?” 탐이 당황하며 화면을 가리켰습니다. “자바스크립트나 파이썬이면 my_config는 당연히 참조(Reference)로 넘어가서 원본이 유지되어야 하는 거 아닙니까? 제가 메모리를 지운 적도 없는데요!”

저스틴이 팔짱을 낀 채 모니터를 응시했습니다. “이건 노이즈야. 네 코드는 러스트의 제1 원칙을 위반했다. 여기엔 널 뒤치다꺼리해 줄 가비지 컬렉터 따위는 없어.”


저스틴의 해부: 소유권(Ownership)과 이동(Move)

저스틴은 탐의 코드를 지적하며 러스트가 메모리를 통제하는 무자비한 방식을 설명하기 시작했습니다.

“탐, 네가 작성한 String 타입은 스택(Stack)이 아니라 기숙 메모리인 힙(Heap)에 할당된다. 힙 메모리는 누군가 정확히 해제(Free)해주지 않으면 메모리 누수(Leak)가 발생하지. C언어에서는 개발자가 직접 free()를 호출해야 했고, 자바나 파이썬은 무거운 GC를 백그라운드에서 돌려서 안 쓰는 메모리를 주워 담는다.”

저스틴이 화이트보드에 선을 그었습니다.

“러스트는 둘 다 거부한다. 대신 ‘소유권(Ownership)’이라는 컴파일 타임 규칙을 만들었지. 규칙은 단 세 개다.”

  1. 러스트의 모든 값은 ‘소유자(Owner)’라고 불리는 변수를 가진다.
  2. 특정 시점에 소유자는 단 하나뿐이다.
  3. 소유자가 스코프(Scope, {})를 벗어나면, 값은 즉시 버려진다(Drop).

“네 코드의 18번 라인을 봐라.” 저스틴이 화면을 두드렸습니다.

“네가 my_config를 start_routing 함수의 인자로 넘기는 순간, 힙 메모리에 대한 소유권은 start_routing 함수의 내부 변수인 config로 완전히 ‘이동(Move)’했다. 더 이상 main 함수의 my_config는 유효하지 않아. 빈 껍데기일 뿐이지.”

탐의 눈이 커졌습니다. “그럼 start_routing 함수가 끝날 때는 어떻게 되나요?”

“스코프(})가 닫히잖아? 러스트 컴파일러가 그 자리에 메모리를 해제하는 drop 코드를 기계어로 몰래 박아 넣는다. 즉, 함수가 끝나는 순간 llama-3-70b라는 힙 데이터는 메모리에서 완벽히 소멸된 거다. 그런데 네가 22번 라인에서 이미 죽어버린 my_config를 다시 부르려 했으니, 컴파일러가 너의 목덜미를 잡은 거지.”

“그럼 데이터를 함수에 넘기고 나서 다시 쓰려면 어떻게 해야 합니까?” 탐이 물었습니다. “매번 데이터를 무식하게 복제(Clone)해야 하나요? 그건 오버헤드가 너무 크잖아요!”

저스틴이 입꼬리를 올렸습니다. “맞아. 무의미한 clone()은 범죄나 다름없지. 데이터를 주지 않고 ‘빌려주는(Borrow)’ 방법이 있다. 그게 다음 단계야.”


 

[STATE] MODE:FULL COLOR:ON LANG:KO IMG:AUTO FORMAT:MARKDOWN

2장의 후반부, 저스틴의 규칙 설명 직후에 탐과 메리의 이해도를 검증하는 기습 질문과 비유(Analogy) 씬을 추가하여 완전히 리팩토링한 버전을 제공합니다.


 

2장. 메리의 반격과 저스틴의 방어: 값의 이동을 막아라

“탐, 소유권이 함수로 통째로 넘어가서 메모리가 파괴된다면, 그냥 C++처럼 포인터(Pointer)만 넘기면 되잖아. 원본은 그대로 두고 주소값만 쓱 빌려주면 끝나는 문제라고.”

화면의 붉은 에러에 넋이 나간 탐을 밀어내며, 메리(Mary)가 키보드를 빼앗았습니다. C++로 학부를 졸업하고 날것의 메모리 주소를 주무르는 데 익숙한 그녀에게, 데이터를 넘기지 않고 접근 권한만 주는 것은 숨 쉬는 것만큼 당연한 일이었습니다.

“가비지 컬렉터에 뇌를 의탁한 파이썬 개발자들은 메모리 주소라는 개념 자체를 모르지. 잘 봐. 러스트에서는 앰퍼샌드(&>)를 쓰면 참조(Reference)를 넘길 수 있어. 이번엔 라우팅이 일어날 때마다 request_count를 1씩 증가시키는 로직까지 추가해 볼게.”


메리의 오만 (Anti-Pattern)

메리는 탐의 코드를 수정하여, 객체 전체를 이동(Move)시키는 대신 참조(&)를 전달하도록 변경했습니다.

“자, 소유권 이동(Move) 에러는 완벽하게 피했어. 실행해 볼까?” 메리가 자신만만하게 엔터를 쳤습니다. 그러나 이번에도 터미널은 자비 없이 붉은 피를 토해냈습니다.

“뭐야? 주소값을 넘겨줬는데 왜 값을 못 바꿔? C++의 일반 포인터는 당연히 값을 덮어쓸 수 있다고!” 메리가 미간을 찌푸렸습니다.

지켜보던 저스틴이 차갑게 끊어냈습니다. “네가 방금 짠 코드가 C++에 널리고 널린 ‘데이터 레이스(Data Race)’의 주범이다. 러스트의 참조는 C++의 멍청한 원시 포인터(Raw Pointer)가 아니야.”


 

저스틴의 해부: 빌림(Borrowing)의 절대 원칙

저스틴이 화이트보드에 붉은 펜으로 러스트의 빌림(Borrowing) 2대 원칙을 적어 내렸습니다.

  1. 불변 참조(&T)는 동시에 여러 개 가질 수 있다. (동시 읽기 허용)
  2. 하지만 가변 참조(&mut T)를 생성했다면, 그 순간 다른 어떤 참조(가변이든 불변이든)도 동시에 존재할 수 없다. (독점적 쓰기 권한)

“이 규칙 때문에 네 코드가 막힌 거다. &LlamaConfig는 읽기 전용 티켓인데, 거기에 대고 쓰기(+= 1)를 시도했으니까. 변수와 참조 모두에 명시적으로 mut을 붙여야만 쓰기가 가능하다.”

저스틴의 설명에 탐과 메리가 고개를 끄덕였습니다. 하지만 저스틴은 그들의 텅 빈 눈빛을 놓치지 않았습니다.

“너희 둘 다 고개는 끄덕이고 있지만, 이 규칙이 시스템 프로그래밍에서 가지는 파괴력을 전혀 이해하지 못했군. 테스트를 하나 하지.”

저스틴이 화이트보드에 짧은 코드를 휘갈겨 썼습니다.

“자, 이 코드를 실행하면 어떻게 될까?”

탐이 먼저 입을 열었습니다. “자바스크립트라면… 순서대로 실행되니까 마지막에 writer가 데이터를 바꾸더라도, 출력할 때는 바뀐 데이터가 세 번 찍히거나 하겠죠?”

메리가 콧방귀를 뀌며 반박했습니다. “멍청하긴. C++ 관점에서는 심각한 버그야. writer가 문자열 길이를 늘리면서 메모리 재할당(Reallocation)을 일으키면, 기존에 reader1과 reader2가 가리키던 주소는 쓰레기값이 돼버려. 이른바 ‘댕글링 포인터(Dangling Pointer)’지. 출력할 때 프로그램이 크래시 나거나 이상한 문자가 찍힐 거야.”

저스틴이 차갑게 웃었습니다. “둘 다 틀렸다. 정답은 ‘아무 일도 일어나지 않는다’다. 왜냐하면 러스트 컴파일러는 이 코드를 기계어로 번역하는 것 자체를 거부하기 때문이지.”

비유(Analogy): 회의실의 화이트보드와 붉은 펜

저스틴이 화이트보드 마커를 들어 올렸습니다.

“이해를 못 하는 것 같으니 완벽하게 꽂아주지. 이 화이트보드가 우리가 가진 데이터(data)다.”

저스틴이 주머니에서 스마트폰을 꺼내 화이트보드 사진을 찍는 시늉을 했습니다. “불변 참조(&)는 스마트폰으로 사진을 찍어가는 행위와 같다. 탐, 메리, 그리고 100명의 다른 개발자들이 동시에 들어와서 화이트보드 사진을 찍어가도(reader1reader2) 아무 문제가 없다. 각자 자기 폰으로 데이터를 읽기만 하니까.”

이어서 저스틴이 붉은색 마커를 집어 들고 화이트보드 앞에 섰습니다. “하지만 가변 참조(&mut)는 화이트보드를 지우고 새로 쓸 수 있는 ‘단 하나의 붉은 펜’이다. 내가 이 펜을 쥐는 순간(writer), 규칙은 이렇게 바뀐다.”

저스틴이 탐과 메리를 가리켰습니다.

  1. “내가 펜을 쥐고 글을 고치고 있는 동안에는, 아무도 사진을 찍을 수 없다.” (글이 반쯤 수정된, 깨진 데이터를 읽는 것을 방지)
  2. “너희들이 이전에 찍어간 사진(reader1reader2)을 지금 이 순간 쳐다보고 있다면, 나는 펜을 쥘 수 없다.” (읽고 있는 데이터가 실시간으로 변조되는 것을 방지)

“C++은 누군가 화이트보드를 지우고 있는데도 사진을 찍게 내버려 둔다. 그래서 화면이 절반 날아간 심령사진(쓰레기값)이 찍히지. 파이썬은 이 문제를 막으려고 회의실 문에 무거운 자물쇠(GIL)를 달아놓고 한 번에 한 명만 들어가게 만든다. 속도가 끔찍하게 느려지지.”

저스틴이 펜을 책상에 탁 내려놓았습니다.

“러스트는 자물쇠를 쓰지 않는다. 컴파일러가 너희들의 코드를 처음부터 끝까지 스캔하면서, 누군가 사진을 쳐다보는 순간과 펜을 쥐는 순간이 단 1줄이라도 겹치면 빌드 자체를 박살 내버리는 거다. 런타임 오버헤드 없이(Zero-cost), 데이터 레이스를 컴파일 타임에 100% 멸종시키는 방법론이다.”

 

탐은 마른침을 삼켰고, 메리는 무언가 큰 충격을 받은 듯 자신의 멍청한 C++식 접근법을 부끄럽게 쳐다보았습니다. 메모리의 주소값을 다룬다는 것의 진짜 의미를 이제야 깨달은 것입니다.

저스틴이 말을 이었습니다. “소유권의 이동과 빌림의 동시성 통제 규칙까지는 완벽히 이해했겠지. 하지만 너희들이 진정한 지옥을 맛보려면 아직 멀었다. 참조(주소값)를 빌려줬을 때 가장 큰 문제가 뭔지 아나? 원본 화이트보드 자체가 불타 없어졌는데, 누군가 계속 옛날 사진을 들여다보려 할 때지.

저스틴이 등 뒤의 모니터를 켰습니다. 화면에는 러스트 개발자들의 영원한 트라우마, ‘라이프타임(Lifetime)’ 문법이 기다리고 있었습니다.


 

 

About the Author
(주)뉴테크프라임 대표 김현남입니다. 저에 대해 좀 더 알기를 원하시는 분은 아래 링크를 참조하세요. http://www.umlcert.com/kimhn/

Leave a Reply

*