본문으로 바로가기
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년 10월 7일 · 버전: v0.0.1

피드백 보내기

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

GitHub 이슈로 보내기