Choorai
Lv.3 성능 Go Chi Router

Go로 성능 중심 백엔드 만들기

Go는 컴파일 언어로, 빠른 실행 속도와 낮은 메모리 사용량이 특징입니다. 단일 바이너리로 배포하여 Docker 이미지 크기를 20MB 이하로 줄일 수 있습니다.

왜 Go인가?

  • 빠른 실행 속도 (컴파일 언어)
  • 단일 바이너리 배포 (의존성 없음)
  • 낮은 메모리 사용량 (Cold Start 빠름)
  • Docker 이미지 ~15MB
  • 동시성 처리에 강함 (goroutine)

1. 프로젝트 생성

터미널
mkdir my-api && cd my-api
go mod init my-api
go get github.com/go-chi/chi/v5
go get github.com/go-chi/cors
go get github.com/google/uuid

Chi는 가볍고 빠른 Go 라우터입니다. Express.js와 비슷한 미들웨어 패턴을 사용합니다.

2. 첫 API 작성

main.go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    "github.com/go-chi/cors"
)

func main() {
    r := chi.NewRouter()

    // Middleware
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(cors.Handler(cors.Options{
        AllowedOrigins:   []string{"http://localhost:*"},
        AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE"},
        AllowedHeaders:   []string{"Content-Type"},
        AllowCredentials: true,
    }))

    // Routes
    r.Get("/health", healthHandler)
    r.Get("/api/hello", helloHandler)

    // Start server
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    log.Printf("Server starting on port %s", port)
    http.ListenAndServe(":"+port, r)
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "status": "healthy",
    })
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "World"
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "message": "Hello, " + name + "!",
    })
}

실행

터미널
go run main.go

# 테스트
curl http://localhost:8080/health
curl http://localhost:8080/api/hello?name=Choorai

3. CRUD API 추가

handlers.go
package main

import (
    "encoding/json"
    "net/http"
    "sync"
    "time"

    "github.com/go-chi/chi/v5"
    "github.com/google/uuid"
)

// Project model
type Project struct {
    ID          string    `json:"id"`
    Name        string    `json:"name"`
    Description string    `json:"description"`
    CreatedAt   time.Time `json:"created_at"`
}

// In-memory storage
var (
    projects = make(map[string]*Project)
    mu       sync.RWMutex
)

// List projects
func listProjects(w http.ResponseWriter, r *http.Request) {
    mu.RLock()
    defer mu.RUnlock()

    items := make([]*Project, 0, len(projects))
    for _, p := range projects {
        items = append(items, p)
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]interface{}{
        "items": items,
        "total": len(items),
    })
}

// Create project
func createProject(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Name        string `json:"name"`
        Description string `json:"description"`
    }
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, `{"error":"invalid body"}`, http.StatusBadRequest)
        return
    }

    project := &Project{
        ID:          uuid.New().String(),
        Name:        req.Name,
        Description: req.Description,
        CreatedAt:   time.Now(),
    }

    mu.Lock()
    projects[project.ID] = project
    mu.Unlock()

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(project)
}

// Get project
func getProject(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")

    mu.RLock()
    project, exists := projects[id]
    mu.RUnlock()

    if !exists {
        http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(project)
}

// Delete project
func deleteProject(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")

    mu.Lock()
    defer mu.Unlock()

    if _, exists := projects[id]; !exists {
        http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
        return
    }

    delete(projects, id)
    w.WriteHeader(http.StatusNoContent)
}

// Router setup
func setupRoutes(r chi.Router) {
    r.Route("/api/v1/projects", func(r chi.Router) {
        r.Get("/", listProjects)
        r.Post("/", createProject)
        r.Get("/{id}", getProject)
        r.Delete("/{id}", deleteProject)
    })
}

sync.RWMutex

Go에서 동시성 안전을 위해 sync.RWMutex를 사용합니다. 읽기는 여러 goroutine이 동시에, 쓰기는 하나만 가능합니다.

4. Docker로 배포

Dockerfile
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server .

# Runtime stage
FROM alpine:3.20
WORKDIR /app
RUN adduser -D -g '' appuser
COPY --from=builder /app/server .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
CMD ["./server"]

멀티스테이지 빌드로 최종 이미지에는 바이너리만 포함됩니다. 결과 이미지 크기는 약 15-20MB입니다.

터미널
# 로컬 테스트
docker build -t my-go-api .
docker run -p 8080:8080 my-go-api

# 이미지 크기 확인 (< 20MB!)
docker images my-go-api

# Cloud Run 배포
gcloud run deploy my-go-api \
  --source . \
  --region asia-northeast3 \
  --allow-unauthenticated \
  --min-instances 0 \
  --max-instances 1

프레임워크 비교

항목 Go (Chi) Hono FastAPI
이미지 크기 ~15MB ~150MB ~200MB
Cold Start ~100ms ~300ms ~500ms
메모리 사용 ~20MB ~50MB ~80MB
학습 곡선 중간 쉬움 쉬움
타입 시스템 정적 동적 (TS) 동적 (Pydantic)

언제 Go를 선택할까?

Go 추천

  • ✅ Cold Start가 중요한 서버리스
  • ✅ 비용 최적화가 필요한 경우
  • ✅ 높은 동시 처리량
  • ✅ 마이크로서비스 아키텍처

다른 선택 고려

  • 빠른 프로토타입 → Hono
  • 자동 문서화 → FastAPI
  • 엔터프라이즈 구조 → NestJS

전체 예제 코드

B2B Admin API의 Go 버전을 확인하세요: examples/b2b-admin/api-go

마지막 업데이트: 2026년 2월 22일 · 버전: v0.0.1

피드백 보내기

입력한 내용으로 새 이슈 페이지를 엽니다.

GitHub 이슈로 보내기