회사에서 일하면서 제안서를 많이 작성했다.
RFP를 받아서 요구사항을 정리하고 수행방안을 잡는 것부터 시작해서 기술 구성도, 개발 범위, 일정 같은 내용을 정리했고 최종적으로 PPT까지 만들었다. 프로젝트에 따라서는 연구개발계획서나 용역수행방안서, 실증 결과보고서까지 이어졌다.
그중에서 지금 생각해도 개발 경험이 많이 쌓였던 건 제안서를 쓰면서 프로토타입을 같이 만들었던 작업이다.
제안서가 다 나온 다음에 개발을 시작한 게 아니다. 제안서를 작성하다가 이건 실제로 가능한가 싶은 부분이 있으면 개발 환경부터 잡고 직접 확인했다. 문서에서는 문제없이 보였는데 막상 만들어보면 생각보다 어려운 경우가 있었다.
그런 부분은 그냥 넘어가지 않았다. 프로토타입에서 문제가 확인되면 제안서의 구조나 기술 구성 자체를 수정하기도 했다.
처음부터 서비스 전체를 만드는 것도 아니었다. 실제 개발에 들어갔을 때 위험할 것 같은 부분을 먼저 골랐다.
브라우저에서 3D 데이터를 얼마나 버틸 수 있는지, 서버가 어느 정도 접속자를 받을 수 있는지, VR 장비가 실제 환경에서 정상적으로 연결되는지, AI를 외부 서버 없이 돌릴 수 있는지 같은 것들이다.
제안서에 넣기 전에 먼저 만들어봤다
제안서를 쓰다 보면 기술적으로는 상당히 그럴듯한 구성이 나온다.
WebGL을 사용하고, AI를 붙이고, 클라우드 서버를 구성하고, 실시간 통신을 넣는 식으로 기술을 나열하는 건 어렵지 않다. 문제는 실제로 만들 때다.
예를 들어 “WebGL 기반 대규모 3D 공간”이라고 적어놓고 실제 브라우저에서 지형과 건물을 계속 올려보면 바로 문제가 나온다.
서버도 비슷했다. 동시접속 100명이라고 적는 것 자체는 어렵지 않지만, 실제로 100명이 붙었을 때 CPU와 메모리가 어떻게 변하는지는 직접 돌려보기 전에는 알기 어렵다.
그래서 제안서에 기술적으로 중요한 내용이 들어가면 가능하면 프로토타입부터 만들어봤다.
프로토타입도 화면을 예쁘게 만드는 데 시간을 많이 쓰지는 않았다. 성능이 필요한 부분이나 장비 호환성처럼 나중에 뒤집기 어려운 부분을 먼저 확인했다.
WebGL에서 메모리 문제가 먼저 나왔다
웹 기반 3D 프로젝트에서는 브라우저 메모리가 문제였다.
처음에는 에셋 용량을 줄이고 LOD를 적용하면 어느 정도 해결될 거라고 생각했다. 그런데 실제로 지형이나 건물, 주변 오브젝트를 많이 올려보니까 메모리가 계속 증가했다.
오브젝트 수가 늘어나면서 브라우저가 버티지 못하는 상황도 나왔다.
그래서 모델을 계속 줄이는 방식보다는 로딩되는 객체 자체를 관리하는 쪽으로 방향을 바꿨다.
카메라 위치를 기준으로 필요한 범위의 오브젝트만 활성화하고, 화면에서 멀어진 객체는 비활성화했다. 비활성화한 객체는 버리지 않고 풀에 넣어뒀다가 다시 필요할 때 사용했다. 위치 기반 오브젝트 풀링 방식이었다.
LOD도 같이 적용했다.
이걸 실제 브라우저에서 돌려보고 나서야 제안서에 넣었던 WebGL 기반 대규모 공간 구성이 어느 정도까지 가능한지 감이 잡혔다.
문서에는 한 줄로 들어가는 내용인데 실제로 확인하려면 꽤 많은 걸 만들어봐야 했다.
서버는 실시간 데이터와 일반 데이터를 나눴다
동시접속자가 많은 프로젝트에서는 서버 구조도 그냥 하나로 묶지 않았다.
로그인이나 세션, 일반적인 데이터 처리는 Node.js, MySQL, Redis를 이용하고, 사용자 움직임처럼 계속 들어오는 실시간 데이터는 별도 통신 구조로 처리했다.
프로젝트에 따라 C++ 기반 자체 네트워크 엔진을 사용하거나 Relay/P2P 구조를 적용했다.
이것도 제안서에 구성도만 그려놓고 끝내지는 않았다. 프로토타입 서버를 실제로 띄워놓고 접속자를 조금씩 늘려봤다.
작은 세션은 30~50명 정도를 기준으로 확인했고, 규모가 큰 서비스는 단일 인스턴스에서 100명 이상 접속했을 때 문제가 없는지도 봤다.
접속자 수만 본 것도 아니다. CPU와 메모리 사용량을 계속 확인했다. 공공 클라우드 환경을 기준으로는 자원 사용률을 70% 이하에서 유지하는 것을 목표로 잡았다.
웹 화면이나 결과 조회는 3초 이내를 기준으로 잡았고, 오래 걸리는 작업은 10초가 넘어가면 진행 상태를 보여주고 20초가 넘어가면 타임아웃이나 Fallback을 처리하는 식으로 정리했다.
이런 숫자는 PPT에 넣을 때는 별거 아닌 것처럼 보이는데 실제로 테스트해보면 기준을 잡는 데 시간이 꽤 걸린다.
VR은 결국 직접 연결해봤다
VR 장비 관련 제안은 문서만 보고 작성하기가 애매했다.
Vive Focus 3를 지원한다고 적는 것 자체는 어렵지 않다. 그런데 실제로 Business Streaming을 연결하고 SteamVR까지 같이 사용하면 장비나 런타임 쪽에서 예상하지 못한 문제가 나올 수 있다.
그래서 제안서를 작성하면서 Vive Focus 3 환경을 직접 구성했다.
Business Streaming으로 연결하고 SteamVR을 사용하는 과정에서 충돌이 없는지 확인했고, 장비 연결 상태나 실행 순서 때문에 생기는 문제도 확인했다.
이런 건 검색해서 나오는 호환성 정보만 보는 것보다 직접 한번 실행해보는 게 훨씬 빨랐다.
그래서 장비가 들어가는 제안은 가능하면 실제 장비를 연결해보고 문서에 반영했다.
AI는 생각했던 것보다 손볼 게 많았다
AI 프로젝트는 특히 프로토타입을 먼저 만들어본 효과가 컸다.
전시장용 대화형 AI를 제안할 때 처음에는 클라우드 API를 사용하는 방식도 생각했다. 그런데 실제 환경을 생각해보니까 네트워크에 계속 의존하는 게 부담이었다.
전시장에서는 외부 API가 항상 정상적으로 연결된다는 보장도 없고, 응답시간이 길어지면 사용자 입장에서는 그냥 AI가 느린 것처럼 보인다.
사람이 많은 환경이라 마이크 입력도 깨끗하지 않았다.
그래서 제안서를 작성하는 동안 로컬 PC에서 파이프라인을 직접 구성했다.
VAD를 먼저 넣고 Whisper 계열 STT를 연결한 다음 RAG 검색, LLM, Melo TTS, RVC 순서로 붙였다. 마지막에는 Unity에서 결과를 받아 음성과 캐릭터 동작으로 연결했다.
RTX 2070 Super 환경에서 LLaMA 3 8B를 양자화해서 테스트했다.
처음에는 LLM 쪽만 신경 쓰고 있었는데 실제로 돌려보니까 STT가 생각보다 많이 흔들렸다. 주변 소리가 그대로 들어오면서 인식 결과가 달라졌다.
그래서 VAD를 앞단에 넣었다.
70dB 정도의 주변 소음 환경을 기준으로 테스트했고 90% 이상의 인식률을 목표로 잡았다.
이렇게 직접 연결해보고 나니까 온디바이스 구조를 왜 사용하는지 설명하기도 쉬워졌다. 단순히 “네트워크가 필요 없다”가 아니라 실제로 어느 부분에서 문제가 생기는지 알고 있었기 때문이다.
LLM이 C#을 직접 만드는 방식은 포기했다
캐릭터 제어 AI도 처음부터 지금 구조로 만든 건 아니다.
처음에는 사용자가 자연어로 명령하면 LLM이 C# 코드를 만들고 Unity에서 실행하는 방식도 테스트했다.
프로토타입을 돌려보니 문제가 금방 나왔다.
없는 함수를 호출하거나 변수 이름이 틀리는 경우가 있었고, 조건이 꼬이면서 같은 상태를 계속 반복하는 경우도 있었다. 게임 런타임에서 이런 코드를 바로 실행시키는 건 위험했다.
그래서 아예 생성 범위를 줄였다.
LLM이 C# 코드를 만드는 대신 Action, Condition, Transition 형태의 구조화된 명령만 만들게 했다. Unity에서 실행할 수 있는 함수도 미리 등록해두고 그 범위 안에서만 실행하도록 했다.
구조는
LLM -> 구조화된 명령 -> 정적 검증 -> FSM -> Unity Runtime
정도로 단순하게 가져갔다.
상태가 순환하거나 고립되는 경우도 실행 전에 검사했다.
이 방식도 처음부터 정답이라고 생각해서 만든 건 아니다. C# 코드를 직접 생성하는 프로토타입을 먼저 돌려보고 문제가 생겨서 바꾼 것이다.
3-State 프롬프트 기준으로 Pass@1이 0.625 정도 나온 것도 확인했다. 이 결과를 보고 LLM에게 시스템 전체를 맡기는 것보다는 실행할 수 있는 범위를 제한하는 게 낫다고 판단했다.
역사 정보는 모델에게 전부 맡기지 않았다
역사 인물 AI 도슨트는 다른 문제였다.
LLM은 답변을 상당히 자연스럽게 만들지만, 그 내용이 실제 역사적 사실인지까지 보장해주지는 않는다.
그래서 검증된 자료를 따로 구성했다.
BGE-m3-ko로 임베딩하고 FAISS를 이용해서 관련 자료를 검색한 다음 검색된 내용을 LLM에 전달하는 방식으로 만들었다.
여기서도 처음에는 벡터 검색만 붙이면 될 줄 알았는데 실제로는 그렇지 않았다.
어떤 자료를 넣을지 정하는 것부터 검색 결과 개수, 프롬프트, 답변 검수까지 계속 손볼 부분이 생겼다.
특히 역사 관련 내용은 틀린 답변 하나가 전체 시스템 신뢰도에 영향을 줄 수 있어서 학예사 검수 결과도 다시 데이터와 프롬프트에 반영했다.
이런 작업은 제안서에서는 RAG 한 줄로 끝난다. 실제로 만들어보면 그 한 줄 안에 해야 할 일이 꽤 많다.
PPT도 조금씩 달라졌다
프로토타입을 계속 만들다 보니 PPT를 만드는 방식도 바뀌었다.
예전에는 기술 스택이나 시스템 구성을 먼저 보여주는 경우가 많았다. 그런데 실제로 개발해본 결과가 있으면 굳이 그렇게 설명할 필요가 없었다.
WebGL은 실제로 브라우저 메모리가 문제였고, 그래서 오브젝트 풀링과 LOD를 적용했다.
AI 도슨트는 네트워크와 주변 소음이 문제였고, 그래서 온디바이스 처리와 VAD를 넣었다.
LLM 캐릭터 제어는 코드 생성 자체가 불안정해서 FSM으로 바꿨다.
이런 내용을 수행방안이나 시스템 구성도에 넣었다.
직접 만들어본 구조라 질문을 받아도 설명하기가 편했다. “가능합니다”라고만 이야기하는 것보다 실제 프로토타입에서 확인한 결과를 보여주는 편이 훨씬 낫기도 했다.
제안서 이후 문서까지 연결됐다
사업이 실제 개발이나 실증으로 이어지는 경우에는 제안서 작성으로 끝나지 않았다.
RFP 요구사항을 조견표로 정리하고, 제안서에서 어떤 기능을 제공한다고 했는지 실제 개발 결과와 연결했다.
월간 보고서나 중간평가, 최종 결과보고서를 작성할 때도 처음 제안했던 정량적인 기준을 다시 확인했다.
처음에는 목표였던 숫자가 나중에는 실제 테스트 결과가 되는 경우도 있었다.
그래서 제안서를 작성할 때부터 나중에 어떻게 측정할 것인지 생각하게 됐다.
“성능이 좋다”처럼 애매한 표현보다는 응답시간이나 동시접속자 수처럼 실제로 확인할 수 있는 값을 잡는 게 편했다.
일을 하면서 생각이 좀 바뀌었다
처음에는 제안서 작성하고 개발하는 걸 별개의 일이라고 생각했다.
실제로 해보니 그렇지는 않았다.
RFP를 읽다 보면 개발했을 때 문제가 될 만한 부분이 보이고, 그걸 확인하려면 프로토타입을 만들어봐야 했다. 만들어보면 처음 생각했던 구조가 맞는지 알 수 있고, 결과에 따라 다시 제안서 내용을 수정했다.
이게 프로젝트마다 반복됐다.
특히 실패한 프로토타입이 도움이 많이 됐다.
LLM이 C#을 직접 만드는 방식이 잘 안 됐기 때문에 FSM 구조로 바꿀 수 있었고, 클라우드 AI가 현장 환경과 맞지 않는 것도 직접 돌려보고 알게 됐다. WebGL도 실제 브라우저에서 메모리가 터져봤기 때문에 이후에는 처음부터 객체 관리 방식을 같이 생각하게 됐다.
결국 제안서에 적힌 기술이 실제로 가능한지는 문서만 보고 판단하기 어려웠다.
그래서 애매한 부분은 제안서를 쓰면서 바로 만들어봤다.
잘 되면 그대로 제안서에 반영했고, 안 되면 구조를 바꿨다.
프로토타입을 완성품처럼 만드는 게 목적은 아니었다. 나중에 개발을 시작했을 때 크게 문제가 될 부분을 미리 확인하는 게 더 중요했다.
그렇게 만들어본 결과가 다시 제안서와 PPT의 근거가 됐다.