Go로 만드는 실시간 객체 인식 서버
Github: denev6/stream-yolo
- Frontend:
Flutter - Backend:
FastAPI,net/http,OpenCV,ONNX - DevOps:
Docker,Prometheus,Grafana,AWS EC2,Nginx,Github Actions - Tools:
Claude Code,Gemini Pro,VS Code
Go는 정말 빠를까
저는 단순 연구보다 실제 서비스 개발에 관심이 많았어요. 그래서 비전 모델을 다룰 때면 종종 실시간 처리 파이프라인을 만들었어요. 그렇다보니 CPU-bound 작업을 할 때마다 Python의 성능이 불만이었어요. Python은 인터프리터 언어이기 때문에 컴파일 언어보다 실행 속도가 느릴 수 밖에 없어요. 게다가 GIL로 인한 성능 제약과 reference count 기반 GC는 Python 커뮤니티에서 오랫동안 논의되었던 단점이에요. 물론 3.11 이후로 GIL 개선에 대한 논의가 이루어지고는 있지만 아직은 한계가 분명해요.
그에 반해 Go는 빠르고 가볍기로 유명해요. 우선은 컴파일 언어이기 때문에 인터프리터를 거칠 필요가 없어요. 가장 큰 장점은 go routine이에요. Go routine은 대규모 병렬 처리에 최적화된 경량 스레드에요. 최소한의 메모리만 점유하면서 효율적인 스케줄링을 통해 수십만 개의 작업을 동시에 관리할 수 있어요. 당근팀도 Go를 도입하고 CPU 성능을 최적화할 수 있었다고 해요. FastAPI(Python)와 Go를 비교한 벤치마크를 봐도 Go가 더 적은 자원으로 더 많은 일을 한다는 것을 알 수 있어요.
여기서 의문이 생겼어요. 그럼 내가 만드는 AI 서비스도 Go로 구축하면 더 빠를까? 궁금증을 해결하기 위해 YOLO를 이용한 실시간 객체 인식 서버를 구축했어요. 각각 Python과 Go로 서버를 구현하고 동일한 부하를 주었을 때 지연 시간과 CPU 사용률을 비교하려 해요.
WebSocket + YOLO로 만들자
서버는 연속된 이미지 스트림을 받아 처리해야 해요. 일반적인 RESTful API는 매 요청마다 중복된 헤더 정보를 전송하는 오버헤드가 크고, HTTP의 Stateless한 특성 때문에 실시간 스트리밍에 비효율적이에요. 그래서 WebSocket을 통해 한 번의 연결만으로 양방향 데이터 전송이 가능하도록 구현했어요. 이렇게 하면 통신 지연 시간을 줄여 실시간성을 확보할 수 있어요.
Python은 FastAPI를 사용했어요. 실험을 보면 FastAPI가 Flask, Django 등 다른 Python 프레임워크보다 뛰어난 것을 볼 수 있어요. 빠른 속도 덕분에 최근 Python 커뮤니티에서 가장 인기 있는 프레임워크이기도 해요. Go는 표준 라이브러리인 net/http와 gorilla/websocket을 이용했어요. 가장 안정적인 라이브러리로, Go 커뮤니티에서 오랫동안 사용되어온 표준적인 조합이에요.
이미지가 입력으로 들어오면 OpenCV를 이용해 전처리해요. Python과 Go 모두 내부적으로는 C++로 구현된 OpenCV 코어를 사용해요. opencv-python은 Python 래퍼를 통해 C++로 구현된 함수를 호출해요. GoCV는 cgo 기반 바인딩을 통해 C++ 구현체에 직접 연결해 함수를 호출해요. 다시 말해, 두 방식 모두 동일한 C++ 백엔드를 기반으로 동작해요. 전처리된 이미지는 YOLO26n을 이용해 객체를 검출해요. 26n은 경량화된 모델이라 CPU 환경에서 실험할 수 있다는 장점이 있어요. Python과 Go에서 동일한 모델을 사용하기 위해 PyTorch(.pt) 모델을 ONNX 형식으로 변환했어요. 두 언어 모두 ONNX Runtime을 이용해 모델 추론을 수행할 수 있도록 구성했어요.
실험의 일관성을 위해 Docker 환경을 구축했고, CPU 모니터링을 위해 Prometheus와 Grafana를 사용했어요.
역시 Go가 빠르다
먼저 각 서버 별 지연 시간을 비교하기 위해 20명의 사용자가 동시 요청을 보내는 상황을 재현했어요. 실험은 애플 M4 환경에서 실행했으며, Python 3.12, Go 1.28을 사용했어요. 실험 결과는 다음과 같아요.
| Metric | Go Server | Python Server |
|---|---|---|
| Average Latency | 413.09 ms | 437.10 ms |
| Average FPS | 2.53 ms | 2.35 ms |
| P95 Latency | 542.26 ms | 549.43 ms |
| P99 Latency | 613.48 ms | 623.11 ms |
Go를 사용했을 때 평균 FPS가 7.66% 개선되었어요. 그 외 모든 지표에서 Go가 더 뛰어난 성능을 보였어요.
추가로 리소스 사용을 확인하기 위해 CPU 점유율을 측정했어요. M4가 10 코어 CPU인 점을 감안해 20명이 아닌 6명이 동시 접속하는 상황을 재현하고 지표를 측정했어요.
같은 부하를 주었을 때, Go가 CPU를 약 7.3% 적게 사용하는 걸 볼 수 있어요. 이를 통해 Go가 Python보다 빠르며 리소스를 효율적으로 사용한다는 걸 알 수 있어요.
하지만 항상 Go가 정답은 아니라는 생각이 들어요. Go를 OpenCV에 바인딩하는 할 때 CPU 아키텍쳐가 달라 빌드가 안 되는 등 개발 과정에서 어려움이 있었어요. 반면 Python은 별도의 환경을 설정할 필요 없이 pip로 설치해서 사용하면 되기 때문에 윈도우 환경에서도 문제 없이 작동했어요. 또 Go의 소스코드를 보면 YOLO가 예측 클래스를 문자열 레이블로 변환하는 과정이 매우 길어요. 직접 문자열을 조작해 값을 찾기 때문이에요. 하지만 Python은 단 4줄로 해결했어요. 이처럼 Python의 개발 편의성이나 머신러닝 분야의 방대한 커뮤니티 덕분에 개발이 비교적 쉬워요. 따라서 프로덕션 환경에서 리소스와 성능이 중요하다면 Go, 빠른 프로토타이핑이 중요한 실험 환경에서는 Python이 유리해요. 특히 학술 연구와 같이 수정이 잦고 많은 부하를 견딜 일이 없는 상황에서는 Python을 선택하는 것이 합리적이라고 생각해요.
실사용 가능한 서비스로 만들자
마지막으로 완성된 서버를 실제 프로덕트로 배포해보려 해요. 크로스플랫폼으로 활용하기 위해 Flutter를 선택했어요. 완성된 코드를 안드로이드와 ios 환경에서 빌드하고 테스트 해봤어요. 아래 사진은 아이폰에서 실행한 모습이에요.
서버 배포는 AWS EC2를 사용했어요. 앞서 말했듯 Docker를 ARM 아키텍쳐에 맞춰 작성했기 때문에 그대로 실행할 수 있는 t4g.small 인스턴스를 선택했어요. 혼자 테스트하는 용도이며, YOLO26n을 사용한다는 점을 고려했을 때 small이면 충분하다고 판단했어요. 추가로 Github Actions을 이용해 CI/CD 환경을 구축했어요.
전체 흐름을 정리하면 다음 그림과 같아요.
여담으로 이번 프로젝트는 Claude Code와 Gemini Pro를 사용해 개발했어요. AI를 사용하니 이틀만에 프론트부터 배포까지 모든 작업을 끝낼 수 있었어요. 그리고 AI 모델마다 잘 하는 분야가 다르다는 것도 알 수 있었어요. 예를 들어, Flutter 개발 중 bounding box가 좌상단에 몰리는 문제가 있었어요. iOS 카메라 데이터(bgra8888)의 이미지 처리 방식과 안드로이드/iOS 센서의 이미지 회전 방식 차이가 겹치며 발생하는 문제였어요. Claude Code는 하루치 토큰 모두 태우고도 결국은 해결을 못 했어요. Thinking을 열어보면 합리적으로 접근은 하는데, 계속 제자리를 맴돌고 있더군요.. 그래서 Gemini에게 스크린샷과 코드를 넘겨주니 3번의 대화만으로 해결되었어요. 최근 PyCon이나 GopherCon에 가면 많은 발표자분들이 코드를 AI로 짰다고 말씀하세요. 이제는 AI 툴을 잘 찾아 사용하는 것도 개발 역량이라는 생각이 들어요.




