TormentNexus가 Go에서 446개의 HTTP 핸들러를 실행하면서도 모듈형 모놀리스(Modular Monolith)를 유지하는 이유
요약
TormentNexus는 고성능 AI 백엔드를 위해 마이크로서비스 대신 Go와 TypeScript를 결합한 모듈형 모놀리스 아키텍처를 채택했습니다. Go를 커널 계층으로 사용하여 네트워크 I/O와 동시성을 처리하고, TypeScript를 애플리케이션 계층으로 활용하여 개발 편의성을 확보했습니다.
핵심 포인트
- 마이크로서비스의 복잡성을 피하기 위해 단일 배포 바이너리인 모듈형 모놀리스 사용
- Go의 고루틴 스케줄링을 활용해 네트워크 I/O 및 AI 추론 오케스트레이션 최적화
- Go(커널)와 TypeScript(애플리케이션)를 분리한 폴리글랏 아키텍처 구축
- 빌드 타임 타입화된 인터페이스를 통해 폴리글랏 시스템의 통합 드리프트 방지
TormentNexus가 Go에서 446개의 HTTP 핸들러를 실행하면서도 모듈형 모놀리스(Modular Monolith)를 유지하는 이유
TormentNexus가 어떻게 다중 언어 아키텍처(Polyglot Architecture)를 갖춘 Go-TypeScript 모놀리스를 활용하여 고성능 AI 백엔드를 구축하는지 알아보세요. Go의 동시성 모델(Concurrency Model)과 TypeScript의 개발자 편의성(Developer Ergonomics)을 결합하는 것이 왜 우수한 모듈형 모놀리스를 만드는지 배워보십시오.
마이크로서비스(Microservices)라는 혼란이 필요하다는 신화
매주 또 다른 엔지니어링 블로그가 AI 제품을 확장하기 위한 유일한 "진정한" 아키텍처는 마이크로서비스(Microservices)라고 선언합니다. 팀들은 응집력 있는 코드베이스를 분해하고, 분당 12,000개의 요청을 처리하는 플랫폼을 위해 40개의 서비스를 배포하며, 기능을 출시하는 것보다 분산 트레이싱(Distributed Tracing)을 디버깅하는 데 더 많은 시간을 소비합니다. TormentNexus에서 우리는 의도적으로 반대되는 접근 방식을 취했습니다. 446개의 HTTP 핸들러(HTTP Handlers)를 노출하는 모듈형 모놀리스(Modular Monolith)를 기반으로 하는 단일 배포 가능 바이너리(Single Deployable Binary)를 구축했으며, 이는 아무런 어려움 없이 우리의 전체 AI 백엔드 워크로드(Workload)를 처리합니다.
우리 아키텍처의 핵심 통찰은 간단합니다. 잘 구조화된 모놀리스(Monolith)는 단일 단위로서의 배포 단순성을 제공하는 동시에, 내부 모듈 경계는 혼란을 방지하는 관심사 분리(Separation of Concerns)를 제공합니다. TormentNexus 플랫폼을 구축하기 시작했을 때, 우리는 근본적인 질문을 던졌습니다. 시스템의 커널(Kernel)—즉, 요청 라우팅(Request Routing), 커넥션 풀링(Connection Pooling), AI 추론 오케스트레이션(AI Inference Orchestration), WebSocket 팬아웃(Fanout)—을 어떤 언어가 담당해야 하는가? 정답은 Go였습니다. 단순히 유행이기 때문이 아니라, 프로파일링(Profiling) 결과 지연 시간 예산(Latency Budget)의 87%가 비즈니스 로직이 아닌 네트워크 I/O 및 고루틴 스케줄링(Goroutine Scheduling)에 집중되어 있었기 때문입니다. Go의 런타임(Runtime)은 Node.js의 이벤트 루프(Event Loop)나 Python의 GIL이 따라올 수 없는 정밀도로 해당 계층을 처리합니다.
우리의 폴리글랏 아키텍처 (Polyglot Architecture)를 작동하게 만드는 핵심은 우리가 설정한 경계입니다. Go는 커널 (Kernel)을 담당합니다. TypeScript는 애플리케이션 계층 (Application Layer)을 담당하며, 관리자 대시보드 컴포넌트부터 스키마 기반 폼 빌더 (Schema-driven Form Builders), 실시간 협업 모듈에 이르기까지 모든 것을 처리합니다. 두 언어는 서로의 영역을 침범하지 않습니다. 두 언어는 빌드 타임 (Build Time)에 생성되는 타입화된 인터페이스 (Typed Interfaces)를 통해 통신하며, 이를 통해 대부분의 폴리글랏 시스템을 망가뜨리는 통합 드리프트 (Integration Drift) 현상을 제거합니다.
왜 Go가 AI 백엔드를 위한 적합한 커널 언어인가
우리의 Go 커널은 23개의 라우트 그룹 (Route Groups)에 걸쳐 446개의 HTTP 핸들러 (Handlers)를 관리합니다. 각 핸들러는 일관된 패턴을 따릅니다: 요청 파싱 (Parse), 인증 (Authenticate), 인가 (Authorize), 검증 (Validate), 비즈니스 로직 실행 (Execute Business Logic), 응답 직렬화 (Serialize Response), 그리고 반환 (Return). 이 워크로드에 대해 Go가 탁월한 이유는 단일 기능 때문이 아니라, 대규모 환경에서 함께 작동하는 기능들의 조합 때문입니다.
첫째, 고루틴 (Goroutines)입니다. 우리의 AI 백엔드는 200ms에서 45초까지 걸릴 수 있는 장기 실행 추론 파이프라인 (Inference Pipelines)을 생성합니다. 각 파이프라인은 지속적인 데이터베이스 연결, 스트리밍 응답 라이터 (Streaming Response Writers), 그리고 때로는 모델 제공업체에 대한 외부 API 호출을 필요로 합니다. 요청당 스레드 (Thread-per-request) 모델에서는 50개의 동시 추론 요청만으로도 상당한 메모리를 소비하게 됩니다. Go에서 각 고루틴은 단 2KB의 스택 공간으로 시작하며, 필요에 따라 동적으로 확장됩니다. 지난 분기 피크 부하 테스트 (Peak Load Test) 동안, 우리는 340ms의 p99 레이턴시 (Latency)를 유지하면서 추론 스트림을 처리하는 12,400개의 동시 고루틴을 유지했습니다.
둘째, 표준 라이브러리 (Standard Library)입니다. Go의 net/http 패키지는 프로덕션급 (Production-grade)입니다. 우리는 핵심 라우팅을 위해 Echo, Gin 또는 Fiber를 사용하지 않으며, 얇은 미들웨어 체인 (Middleware Chain)과 함께 표준 라이브러리만을 사용합니다. 이를 통해 Go의 두 가지 릴리스 주기 (Release Cadence)에 걸쳐 버전 고정 (Version Pinning), 보안 감사 (Security Auditing), 호환성 테스트가 필요한 의존성 (Dependency)을 제거합니다. 우리의 미들웨어 스택은 정확히 7개의 핸들러 깊이로 구성되어 있습니다:
func (s *Server) BuildRouter() http.Handler {
mux := http.NewServeMux()
...
각 미들웨어(middleware)는 http.Request를 전달받아 http.Handler를 반환하며, 이를 통해 프레임워크의 마법(magic) 없이도 조합 가능한(composable) 동작을 구현할 수 있습니다. 관찰 가능성(observability)이나 서킷 브레이킹(circuit breaking)을 추가해야 할 때는 새로운 미들웨어를 삽입하기만 하면 됩니다. 그렇지 않을 때는 오버헤드가 전혀 없습니다. 리플렉션(reflection), 인터페이스 단언(interface assertions), 런타임 타입 체크(runtime type checks)가 발생하지 않기 때문입니다.
셋째, 컴파일 타임 안정성(compile-time safety)입니다. 우리의 AI 백엔드는 외부 모델 제공자(model providers)에게 14개의 서로 다른 호출을 수행합니다. 각 제공자는 서로 다른 인증 체계(authentication schemes), 속도 제한(rate limits), 페이로드 형식(payload formats), 그리고 에러 의미론(error semantics)을 가지고 있습니다. Go의 타입 시스템(type system)은 우리가 컴파일 타임에 모든 에러 경로를 처리하도록 강제합니다. 실수로 삼켜버릴 수 있는 try/catch는 없습니다. 새벽 3시에 운영 환경에서 조용히 실패하는, 처리되지 않은 거부(unhandled rejection)를 동반한 async/await도 없습니다. 모든 에러는 값(value)이며, 컴파일러는 우리가 이를 반드시 처리하도록 보장합니다.
TypeScript 레이어: 개발 속도(Developer Velocity)가 중요한 곳
여기가 바로 다중 언어 아키텍처(polyglot architecture)가 제 가치를 발휘하는 지점입니다. Go는 인프라 관련 작업에 탁월하지만, 제품 관리자(product managers)가 2주 단위의 사이클로 배포하는 애플리케이션 레이어에는 개발 속도가 필요합니다. TypeScript는 세 가지 메커니즘, 즉 우리의 컴포넌트 라이브러리(component library), 스키마 코드 생성(schema codegen) 파이프라인, 그리고 실시간 협업 엔진(real-time collaboration engine)을 통해 그 속도를 제공합니다.
우리의 관리자 대시보드에는 파이프라인 설정, 사용자 권한, 결제, 모델 미세 조정(fine-tuning) 인터페이스를 관리하는 340개 이상의 React 컴포넌트가 포함되어 있습니다. 이 컴포넌트들은 Go 백엔드와 타입 정의(type definitions)를 공유해야 합니다. 우리는 어긋나기 쉬운 REST 계약(REST contract) 없이 이 문제를 다음과 같이 해결합니다.
우리는 두 코드 생성(codegen) 파이프라인이 모두 이해할 수 있는 JSON Schema의 하위 집합을 사용하여 공유 디렉토리에 API 스키마를 유지합니다.
{
"type": "object",
"title": "InferenceRequest",
...
빌드 타임에 두 개의 생성기(generators)가 병렬로 실행됩니다. Go 생성기는 검증 태그(validation tags)가 포함된 구조체(struct)를 생성합니다.
// AUTO-GENERATED — DO NOT EDIT
type InferenceRequest struct {
ModelID string `json:"model_id" validate:"required,uuid"`
...
TypeScript 생성기(generator)는 이에 대응하는 인터페이스와 검증 함수를 생성합니다:
// AUTO-GENERATED — DO NOT EDIT
export interface InferenceRequest {
model_id: string;
...
이러한 공유 스키마 (shared schema) 접근 방식은 도입 후 첫 분기에 통합 버그(integration bugs)의 94%를 제거했습니다. 코드 생성 (codegen) 파이프라인을 도입하기 전에는 Go와 TypeScript 간의 타입을 수동으로 동기화해야 했으며, 이는 백엔드 엔지니어가 런타임 (runtime) 전까지 프론트엔드 팀이 알지 못하는 파괴적 변경 (breaking change)을 배포할 수 있음을 의미했습니다. 이제는 단일 진실 공급원 (single source of truth)이 두 언어 모두에서 컴파일 타임 (compile time)에 계약 (contract)을 강제합니다.
AI 추론 파이프라인을 위한 고루틴 (Goroutine) 아키텍처
우리의 고루틴 (goroutine) 아키텍처가 AI 백엔드의 독특한 요구 사항을 어떻게 처리하는지 구체적으로 살펴보겠습니다. TormentNexus의 전형적인 추론 (inference) 요청은 단순히 모델을 호출하고 결과를 반환하는 데 그치지 않습니다. 이는 최대 6단계로 구성된 파이프라인을 따르며, 각 단계는 잠재적으로 자체적인 리소스 할당을 필요로 할 수 있습니다:
1단계: 요청 검증 (request validation) 및 프롬프트 인젝션 (prompt injection) 탐지. 2단계: Redis를 통한 사용자 할당량 (quota) 확인. 3단계: 가용성, 지연 시간 SLA (latency SLAs), 비용 최적화에 기반한 모델 라우팅 (routing). 4단계: 토큰 카운팅 (token counting) 및 예상 비용 계산. 5단계: 실제 추론 (inference) 호출로, SSE를 통해 토큰을 스트리밍할 수 있음. 6단계: 응답 후처리 (post-processing), 로깅 (logging), 및 메트릭 (metric) 방출.
각 단계는 동일한 고루틴 (goroutine) 내에서 실행됩니다. 단계들이 본질적으로 순차적이기 때문에 여기서 단계 간 병렬성 (cross-stage parallelism)은 필요하지 않습니다. 하지만 Go가 빛을 발하는 지점은 동시 요청 처리 (concurrent request handling)입니다. 지난 11월 부하 테스트 (load test) 동안, 우리는 각각 스트리밍 응답을 기대하는 800명의 사용자가 동시에 추론 요청을 제출하는 상황을 시뮬레이션했습니다. Go 런타임 (runtime)은 각각 http.Flusher를 열어둔 상태로 800개의 동시 스트리밍 연결을 관리하는 동시에, 200개의 새로운 큐 제출 (queue submissions)을 처리하고 15개의 Prometheus 카운터 (counters)에 걸쳐 백그라운드 메트릭 집계 (metric aggregation)를 수행했습니다.
메모리 프로파일 (memory profile)은 놀라웠습니다. 800개의 동시 스트림 (concurrent streams)에 대한 총 힙 할당 (total heap allocation)은 340MB로 정점을 찍었습니다. 프로토타입에 구현된 동일한 로직을 기반으로 추정한 동일한 Node.js 워크로드의 경우 1.2GB였으며, 이는 동일한 처리량 (throughput) 대비 3배 이상의 메모리를 사용하는 것이었습니다. 고정된 크기의 Kubernetes 노드에서 실행하며 비용 효율성을 위해 노드당 포드 (pod) 수를 최대화하려 할 때, 이러한 차이는 매우 중요합니다.
우리의 고루틴 (goroutine) 관리 또한 단순히 던져두고 잊어버리는 (fire-and-forget) 방식이 아닙니다. 우리는 컨텍스트 취소 (context cancellation)를 포함한 구조적 동시성 (structured concurrency) 패턴을 사용합니다:
func (s *Server) HandleInferenceStream(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
defer cancel()
...
클라이언트가 연결을 끊으면, 컨텍스트는 파이프라인 내의 모든 고루틴으로 취소 신호를 전파합니다. 고립된 (orphaned) 고루틴도 없고, 누수된 (leaked) 데이터베이스 연결도 없습니다. 컴퓨팅 예산을 낭비하는 유령 GPU 할당 (phantom GPU allocations)도 없습니다. 컨텍스트 기반의 생명주기 관리 (lifecycle management)는 AI 백엔드 워크로드에 있어 Go의 핵심 기능 (killer feature)이며, 우리의 446개 핸들러는 모두 이 패턴을 일관되게 따릅니다.
모듈형 모놀리스 (Modular Monolith) 배포: 1개의 바이너리, 0의 다운타임
우리의 배포 프로세스는 공격적일 정도로 단순합니다. Go 바이너리는 446개의 HTTP 핸들러, 31개의 데이터베이스 모델, 14개의 외부 API 클라이언트, 그리고 8개의 백그라운드 워커 고루틴을 모두 하나의 28MB 실행 파일로 컴파일합니다. TypeScript 에셋은 컴파일 타임에 go:embed를 사용하여 바이너리에 내장됩니다. 그 결과 TormentNexus 플랫폼 전체를 포함하는 단 하나의 아티팩트 (artifact)가 생성됩니다.
우리는 롤링 업데이트 (rolling update) 전략을 사용하여 Kubernetes에 배포합니다. 지난 배포(3일 전) 당시, 우리의 카나리 (canary) 프로세스는 트래픽의 5%를 새 바이너리로 전환하고, 에러율이 0.1% 미만으로 유지되는지 검증하며, 전체 롤아웃 (rollout)을 완료하는 데 정확히 47초가 걸렸습니다. Go 바이너리의 시작 시간은 프로세스 생성부터 첫 번째 HTTP 요청을 수락하기까지 380ms입니다. 모듈 해석 (module resolution) 단계로 인해 콜드 스타트 (cold start)가 평균 4.2초였던 이전의 Node.js 설정과 비교해 보십시오.
우리 모듈형 모놀리스 (Modular Monolith)의 "모듈형" 부분은 Go의 패키지 구조를 통해 강제됩니다. 우리의 디렉토리 레이아웃은 경계가 지정된 컨텍스트 (Bounded Contexts)와 직접적으로 매핑됩니다:
/kernel/inference/— AI 추론 오케스트레이션 (Inference Orchestration), 모델 라우팅 (Model Routing), 토큰 관리 (Token Management)/kernel/auth/— 인증 (Authentication), 인가 (Authorization), 세션 관리 (Session Management)/kernel/billing/— 사용량 추적 (Usage Tracking), 할당량 강제 (Quota Enforcement), 인보이스 생성 (Invoice Generation)/kernel/pipelines/— 사용자 정의 파이프라인 구축 및 실행 (User-defined Pipeline Construction and Execution)/kernel/connectors/— 외부 서비스 통합 (External Service Integrations) (14개 제공업체)/app/dashboard/— 임베디드 TypeScript 애플리케이션 (관리자 UI)/app/forms/— 스키마 기반 폼 생성 라이브러리 (Schema-driven Form Generation Library)
각 커널 (Kernel) 패키지는 고유한 인터페이스 경계 (Interface Boundaries)를 가집니다. inference 패키지는 billing을 직접 임포트 (Import)할 수 없으며, 반드시 정의된 인터페이스를 통해 billing.CheckQuota()를 호출해야 합니다. 이는 의존성 방향 (Dependency Direction)을 강제하고, 많은 Go 모놀리스를 괴롭히는 순환 임포트 스파게티 (Circular Import Spaghetti)를 방지합니다. 새로운 기능을 추가해야 할 때는 하나의 패키지에 추가하면 됩니다. 테스트가 필요할 때는 하나의 인터페이스를 모킹 (Mock)하면 됩니다. 이 모놀리스는 컴파일 속도가 빠르고 (CI에서 클린 빌드 시 11.4초 소요), 테스트 속도가 빠르며 (전체 스위트 실행 시 2분 38초 소요), 배포 속도도 빠릅니다.
이 아키텍처가 무너지는 시점 (그리고 대처 방법)
정직함이 중요합니다. 우리의 Go TypeScript 모놀리스가 작동하는 이유는 특정 제약 조건 때문입니다. 플랫폼은 단일 제품을 서비스합니다. 팀 규모는 80명이 아니라 8명의 엔지니어입니다. 배포 대상은 예측 가능한 트래픽 패턴을 가진 단일 클라우드 제공업체입니다.
만약 당신이 플랫폼을 구축하고 있다면
원문은 tormentnexus.site에서 처음 게시되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기