벤치마크의 함정
벤치마크를 홍보하는 이들은 결코 전체 그림을 보여주지 않습니다. 그들은 거의 아무것도 하지 않는 핸들러를 측정합니다. JSON Hello World나 정적 문자열 같은 것 말이죠. 그런 진공 상태에서는 Fiber가 압도적으로 승리합니다. Gin보다 초당 더 많은 요청을 처리하고 메모리도 적게 사용하니까요. 그것은 진정한 엔지니어링입니다. 하지만 여러분의 애플리케이션은 진공 상태가 아닙니다.
저는 케냐에서 실시간 보안 앱을 구축하고 있습니다. 나이로비와 몸바사 전역의 사용자로부터 GPS 좌표를 수집하고 범죄 다발 지역이나 도로 사고에 대한 알림을 보냅니다. 이는 PostgreSQL 쓰기, Firebase Cloud Messaging 호출, 그리고 역지오코딩(reverse geocoding) 조회를 의미합니다. 스택에 네트워크 홉(network hops)과 데이터베이스 라운드트립(round-trips)이 포함되면, 라우팅에서 나노초를 아끼는 프레임워크는 눈에 보이지도 않습니다. 병목 현상은 결코 라우터에서 발생하지 않습니다. SMS 게이트웨이의 타임아웃이나, 사고 보고서 테이블을 스캔하는 PostGIS 쿼리에서 발생합니다. 가공되지 않은 처리량(Raw throughput)은 실제 운영 환경을 만나기 전까지는 매혹적으로 보일 뿐입니다.
표준 라이브러리에 대한 충성도
Gin은 net/http를 기반으로 직접 구축되었습니다. 이는 생각보다 훨씬 중요합니다.
모든 Go 개발자는 net/http를 알고 있습니다. 디버깅 시 익숙한 스택 트레이스를 따릅니다. 5년 전에 작성된 미들웨어라도 별도의 절차 없이 바로 사용할 수 있습니다. 요청(request)과 응답(response) 객체는 Go 문서에 명시된 대로 정확하게 동작합니다. Gin이 예측 가능한 이유는 언어 자체에 밀착되어 있기 때문입니다.
Fiber는 그 기반을 제로 할당(zero-allocation) 성능에 최적화된 커스텀 HTTP 엔진인 fasthttp로 대체합니다. 이를 달성하기 위해 요청/응답 풀링(Request/Response Pooling)을 사용합니다. 매 요청이 끝난 후 가비지 컬렉터가 메모리를 회수하도록 두는 대신, Fiber는 메모리 블록을 재사용합니다. 메모리를 비운 뒤 다음 연결에 전달하는 방식입니다. 이 트릭이 속도의 원천이지만, 동시에 위험의 근원이기도 합니다.
재사용된 메모리의 숨겨진 위험
이론적으로 풀링은 안전해 보입니다. 하지만 실제로는 미묘한 약속이 따릅니다. 핸들러가 반환된 후에는 요청 데이터에 대한 참조를 절대 유지해서는 안 된다는 것입니다. 만약 고루틴(goroutine)이 요청보다 오래 지속되거나, 나중에 처리하기 위해 요청 본문의 슬라이스(slice)를 캡처해 두면, Fiber는 해당 메모리를 회수하여 다음 사용자에게 넘겨버립니다.
그 결과는 오염입니다. 이 버그는 유닛 테스트에서는 나타나지 않습니다. 나이로비에 있는 운전자의 GPS 좌표가 갑자기 키수무의 지도에 나타나는 식으로 발생합니다. 한 사용자의 JWT 토큰이 다른 사용자의 컨텍스트로 유출되는 식으로 나타나기도 합니다. 이것들은 시간적 결함(temporal failures)입니다. 로깅을 추가하면 사라지기도 하는데, 로깅 행위 자체가 새로운 메모리를 할당하고 타이밍을 바꾸기 때문입니다. 결국 유령을 쫓는 격이 됩니다.
Gin을 사용하면 표준 라이브러리가 새로운 요청 객체를 할당합니다. 오염시킬 풀(pool) 자체가 없습니다. 실제 안전과 직결된 실제 위치 정보를 다루는 앱에서는 이러한 안전성이 매우 중요합니다.
생태계의 중력
호환성이라는 조용한 세금도 존재합니다.
대부분의 Go 미들웨어는 net/http를 가정합니다. JWT 검증기, OpenTelemetry 익스포터, CORS 핸들러, 속도 제한기(rate limiter) 모두 표준 인터페이스를 사용합니다. Gin은 이 인터페이스를 네이티브하게 이해합니다. 미들웨어를 가져다 넣기만 하면 바로 작동합니다.
Fiber는 자체적인 컨텍스트 타입과 핸들러 시그니처를 가집니다. 따라서 어댑터가 필요합니다. 때로는 공식 어댑터가 있지만, 때로는 업스트림 라이브러리보다 1년씩 뒤처지기도 합니다. 때로는 부하가 걸릴 때 약간 다르게 동작하기도 합니다. 소규모 팀을 운영할 때는 인증 미들웨어가 노트북에서는 잘 작동하는데 서버에서는 정당한 토큰을 거부하는 이유를 디버깅하느라 시간을 허비할 여유가 없습니다. 여러분은 go get이 지루하기를 원합니다. Gin은 이를 지루하게 유지해주며, 그것이 바로 새벽 3시에 여러분이 원하는 것입니다.
애플리케이션에 실제로 필요한 것
제가 만드는 보안 서비스는 동시 접속자들로부터 몇 초마다 위치 핑(ping)을 처리합니다. 고위험 도로에 지오펜싱(geofencing)을 설정하고 알림을 트리거합니다. 느린 알림은 답답함을 주지만, 잘못된 알림은 위험합니다. 풀링된 메모리 오염 때문에 누군가를 실제 사고 현장으로 보내는 일은 용납될 수 없습니다.
여기서는 처리량보다 신뢰성이 우선입니다. pprof를 연결했을 때 이해할 수 있는 스택 트레이스가 필요합니다. 커스텀 메모리 풀의 내부 생명 주기를 이해할 필요가 없는 메모리 프로파일이 필요합니다. 이 프로젝트를 물려받을 다음 개발자가 fasthttp 객체 모델을 배우지 않고도 오후 한나절 만에 적응할 수 있기를 바랍니다. Gin은 저에게 그것을 제공합니다. 운영상의 안정성(Operational sanity)은 벤치마크에 반영되지 않지만, 서비스를 유지해 주는 것입니다.
