ADR-01: 백엔드 언어로 Go 선택
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2024-12-18 | @KubrickCode | web |
Context
언어 선택 문제
웹 플랫폼은 백엔드 언어 선택이 필요했음. 두 가지 주요 후보가 있었음:
- Go: 기존 collector와 core 서비스와 일치
- NestJS (TypeScript): Next.js 프론트엔드와 일치
기존 아키텍처
시스템은 이미 다음에서 Go를 사용 중이었음:
- Core 라이브러리: 파서 엔진, crypto 유틸리티, 도메인 모델
- Collector 서비스: River를 통한 분석 작업 처리 백그라운드 워커
- 공유 인프라: PostgreSQL 기반 태스크 큐 (River)
핵심 고려사항
웹 백엔드는 다음이 필요했음:
- collector 처리를 위한 분석 태스크 enqueue
- collector와의 암호화 작업 공유 (OAuth 토큰 암호화)
- 필요시 core 라이브러리 기능 접근
Decision
기존 서비스와의 기술 스택 통합을 극대화하기 위해 Go를 웹 백엔드 언어로 채택함.
핵심 원칙:
- 단일 큐 시스템: collector와 River 공유 (별도 BullMQ 불필요)
- 직접 라이브러리 접근: RPC 오버헤드 없이 core 라이브러리 import
- 통합 도구: 단일 언어 CI/CD, 모니터링, 배포
- 암호화 공유: OAuth 토큰에 대해 동일한 암호화/복호화
Options Considered
Option A: Go 백엔드 (선택됨)
작동 방식:
- Go HTTP 서버 (Chi/Gin/Echo)가 REST API 제공
github.com/specvital/core패키지 직접 import- 태스크 enqueue를 위한 공유 River 클라이언트
- collector와 동일한 PostgreSQL 인스턴스
장점:
- 통합 오버헤드 없음: core 라이브러리 직접 import
- 공유 큐 인프라: 단일 PostgreSQL 인스턴스, 통합 River 프로토콜
- 일관된 암호화: 서비스 간 동일한 암호화
- 운영 단순성: 관리할 언어 런타임이 하나
- 리소스 효율성: Node.js보다 낮은 메모리 사용량
단점:
- 프론트엔드 개발자가 Go 기초를 배워야 함
- npm에 비해 작은 생태계
- TypeScript 프론트엔드와 타입 공유 불가
Option B: NestJS + TypeScript
작동 방식:
- TypeScript 기반 NestJS 프레임워크
- 태스크 큐로 BullMQ (River와 별도)
- gRPC 래퍼 또는 TypeScript 재작성을 통한 core 라이브러리 접근
장점:
- Next.js 프론트엔드와 언어 공유
- 더 큰 패키지 생태계 (npm)
- 프론트엔드와 백엔드 간 타입 공유
단점:
- Core 라이브러리 비호환: Go core를 직접 사용 불가
- 이중 큐 시스템: BullMQ (web) + River (collector) = 복잡한 브릿징
- 암호화 재구현: NaCl 암호화를 TypeScript로 재작성 필요
- 운영 복잡성: 두 언어 런타임, 별도 CI/CD
Option C: NestJS BFF + Go Core API
작동 방식:
- NestJS를 Backend-for-Frontend 레이어로
- core 라이브러리를 래핑하는 Go 서비스
- 레이어 간 gRPC 통신
평가:
- 3단계 레이턴시 오버헤드
- 복잡한 배포 토폴로지
- 현재 규모에 과도한 최적화
- 기각: 요구사항 대비 과잉
Implementation Considerations
Core 라이브러리 통합
Web Service (Go) Collector (Go)
│ │
└─── import core/pkg/crypto ────────┘
│ │
└─── import core/pkg/domain ────────┘공유되는 핵심 패키지:
core/pkg/crypto: OAuth 토큰용 NaCl SecretBox 암호화core/pkg/domain: 타입 안전 도메인 모델
큐 아키텍처
Web (Producer) PostgreSQL (NeonDB) Collector (Consumer)
│ │ │
├─ river.Insert() ────→ task_queue ────→ river.Work()
│ │ │
└──────────── shared River protocol ─────────────┘이점:
- 단일 PostgreSQL 인스턴스 (비용 절감)
- 타입 안전 태스크 페이로드
- 내장 재시도, 스케줄링, dead-letter queue
암호화 공유
OAuth 플로우 요구사항:
- Web이 DB 저장 전 GitHub 토큰 암호화
- Collector가 GitHub API 접근 시 토큰 복호화
Go 사용 시:
- 동일한
crypto.Encryptor인터페이스 - 동일한 암호화 키 (환경 변수)
- 호환성 보장
NestJS 사용 시:
- TypeScript로 NaCl SecretBox 재구현 필요
- 미묘한 암호화 비호환 위험
- 추가 테스트 부담
Consequences
Positive
인프라 효율성:
- 단일 PostgreSQL 인스턴스가 web과 collector 모두 지원
- 통합 모니터링 및 알림
- 공유 배포 패턴
개발 속도:
- web과 core 간 직렬화 레이어 없음
- 서비스 간 컴파일 타임 타입 안전성
- 일관된 에러 처리 패턴
운영 단순성:
- 백엔드 서비스에 단일 언어
- 단일 CI/CD 파이프라인 패턴
- 통합 의존성 관리 (go.mod)
Negative
학습 곡선:
- 프론트엔드 개발자가 Go에 익숙해져야 함
- 완화책: 집중 교육, 페어 프로그래밍
타입 공유:
- 프론트엔드용 자동 타입 생성 불가
- 완화책: TypeScript 클라이언트용 OpenAPI codegen
생태계:
- npm보다 적은 패키지
- 완화책: 기능 시작 전 대안 평가
