본문으로 건너뛰기
Previous
Next
[Go 2주 완성 #08] Day 14: 실전 미니 프로젝트 - REST API 서버 구축

[Go 2주 완성 #08] Day 14: 실전 미니 프로젝트 - REST API 서버 구축

[Go 2주 완성 #08] Day 14: 실전 미니 프로젝트 - REST API 서버 구축

이 글의 핵심

[Go 2주 완성 #08] Day 14: 실전 미니 프로젝트 - REST API 서버 구축. 시리즈 안내·2주의 결실.

시리즈 안내

📚 Go 2주 완성 시리즈 #08 (최종편) | 전체 목차 보기

이 글은 C++ 개발자를 위한 2주 완성 Go 언어 커리큘럼Day 14 내용입니다.

이전: #07 테스팅 ← | → 실무 심화: #09 context·우아한 종료


💡 초보자를 위한 한 줄: Go 표준 라이브러리의 net/http만으로 REST API 서버를 만듭니다. http.HandleFunc로 라우트 등록, json.Encoder/Decoder로 JSON 직렬화, 고루틴으로 백그라운드 작업. 프레임워크 없이도 충분히 동작하는 API 서버를 만들 수 있습니다.

들어가며: “프레임워크 없이 API 서버를 만든다고요?”

C++ 또는 다른 언어에서는 웹 서버를 만들 때 Express(Node.js), Flask(Python), Spring(Java) 같은 프레임워크가 필수처럼 느껴집니다.

Go는 다릅니다. 표준 라이브러리만으로 충분히 실전 API 서버를 만들 수 있습니다:

http.HandleFunc("/api/todos", handleTodos)
http.ListenAndServe(":8080", nil)

지금까지 다룬 변수, 슬라이스, 구조체, 인터페이스, 에러 처리, 고루틴, 채널, 테스팅을 묶어 실전 REST API 서버를 구성해 봅니다. 한 바퀴 완성하면 Go로 서비스 코드를 시작할 때 참고할 뼈대가 됩니다. 프로젝트 개요:

  • TODO API 서버: CRUD 기능 완비
  • 표준 라이브러리만 사용: net/http, encoding/json
  • 동시성 활용: 백그라운드 작업 처리
  • 테스트 포함: 유닛 테스트와 통합 테스트

C++ 개발자 관점: C++ 백그라운드에서 Go로 전환하며 겪은 차이점과 함정을 중심으로 설명합니다. 포인터, 동시성, 메모리 관리 등 핵심 개념을 비교하며 정리했습니다.

실무에서의 체감

C++ 위주로 서버를 다루던 환경에서 Go를 도입할 때 흔히 드는 인상은 문법과 툴체인이 단순해 보인다는 점입니다. 프로덕션에서는 그 단순함이 빌드·배포·동시성 코드 가독성으로 이어지는 경우가 많습니다. 자주 언급되는 장점:

  • 개발 속도: 팀·도메인에 따라 다르지만, 네트워크·CLI 코드를 빠르게 완성하기 쉬운 편입니다.
  • 안정성: GC가 있어 수동 할당 해제 부담이 줄어듭니다.
  • 배포: 단일 바이너리로 옮기기 쉬운 구조입니다.

1. 프로젝트 구조

# 프로젝트 초기화
mkdir todo-api
cd todo-api
go mod init todo-api
# 디렉토리 구조
todo-api/
├── go.mod
├── main.go
├── handler.go
├── handler_test.go
├── store.go
└── store_test.go

2. 데이터 모델과 저장소

모델 정의

// model.go
package main
import "time"
type Todo struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Completed bool      `json:"completed"`
    CreatedAt time.Time `json:"created_at"`
}
type CreateTodoRequest struct {
    Title string `json:"title"`
}
type UpdateTodoRequest struct {
    Completed bool `json:"completed"`
}

저장소 구현

// store.go
package main
import (
    "errors"
    "sync"
    "time"
)
var (
    ErrNotFound = errors.New("todo not found")
)
type TodoStore interface {
    Create(title string) (*Todo, error)
    GetAll() ([]*Todo, error)
    GetByID(id int) (*Todo, error)
    Update(id int, completed bool) error
    Delete(id int) error
}
type InMemoryStore struct {
    mu     sync.RWMutex
    todos  map[int]*Todo
    nextID int
}
func NewInMemoryStore() *InMemoryStore {
    return &InMemoryStore{
        todos:  make(map[int]*Todo),
        nextID: 1,
    }
}
func (s *InMemoryStore) Create(title string) (*Todo, error) {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    todo := &Todo{
        ID:        s.nextID,
        Title:     title,
        Completed: false,
        CreatedAt: time.Now(),
    }
    
    s.todos[s.nextID] = todo
    s.nextID++
    
    return todo, nil
}
func (s *InMemoryStore) GetAll() ([]*Todo, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    todos := make([]*Todo, 0, len(s.todos))
    for _, todo := range s.todos {
        todos = append(todos, todo)
    }
    
    return todos, nil
}
func (s *InMemoryStore) GetByID(id int) (*Todo, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    
    todo, ok := s.todos[id]
    if !ok {
        return nil, ErrNotFound
    }
    
    return todo, nil
}
func (s *InMemoryStore) Update(id int, completed bool) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    todo, ok := s.todos[id]
    if !ok {
        return ErrNotFound
    }
    
    todo.Completed = completed
    return nil
}
func (s *InMemoryStore) Delete(id int) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    
    if _, ok := s.todos[id]; !ok {
        return ErrNotFound
    }
    
    delete(s.todos, id)
    return nil
}

3. HTTP 핸들러 구현

// handler.go
package main
import (
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
    "strconv"
    "strings"
)
type TodoHandler struct {
    store TodoStore
}
func NewTodoHandler(store TodoStore) *TodoHandler {
    return &TodoHandler{store: store}
}
// GET /todos - 모든 TODO 조회
func (h *TodoHandler) GetTodos(w http.ResponseWriter, r *http.Request) {
    todos, err := h.store.GetAll()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(todos)
}
// GET /todos/{id} - 특정 TODO 조회
func (h *TodoHandler) GetTodo(w http.ResponseWriter, r *http.Request) {
    id, err := extractID(r.URL.Path)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }
    
    todo, err := h.store.GetByID(id)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            http.Error(w, "Todo not found", http.StatusNotFound)
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(todo)
}
// POST /todos - 새 TODO 생성
func (h *TodoHandler) CreateTodo(w http.ResponseWriter, r *http.Request) {
    var req CreateTodoRequest
    
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    if req.Title == "" {
        http.Error(w, "Title is required", http.StatusBadRequest)
        return
    }
    
    todo, err := h.store.Create(req.Title)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(todo)
}
// PUT /todos/{id} - TODO 업데이트
func (h *TodoHandler) UpdateTodo(w http.ResponseWriter, r *http.Request) {
    id, err := extractID(r.URL.Path)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }
    
    var req UpdateTodoRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    if err := h.store.Update(id, req.Completed); err != nil {
        if errors.Is(err, ErrNotFound) {
            http.Error(w, "Todo not found", http.StatusNotFound)
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.WriteHeader(http.StatusNoContent)
}
// DELETE /todos/{id} - TODO 삭제
func (h *TodoHandler) DeleteTodo(w http.ResponseWriter, r *http.Request) {
    id, err := extractID(r.URL.Path)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }
    
    if err := h.store.Delete(id); err != nil {
        if errors.Is(err, ErrNotFound) {
            http.Error(w, "Todo not found", http.StatusNotFound)
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.WriteHeader(http.StatusNoContent)
}
// 헬퍼: URL에서 ID 추출
func extractID(path string) (int, error) {
    parts := strings.Split(path, "/")
    if len(parts) < 3 {
        return 0, errors.New("invalid path")
    }
    
    id, err := strconv.Atoi(parts[len(parts)-1])
    if err != nil {
        return 0, err
    }
    
    return id, nil
}

4. 라우팅과 미들웨어

// main.go
package main
import (
    "fmt"
    "log"
    "net/http"
    "time"
)
// 로깅 미들웨어
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        log.Printf("%s %s", r.Method, r.URL.Path)
        
        next(w, r)
        
        log.Printf("Completed in %v", time.Since(start))
    }
}
// CORS 미들웨어
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
        
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }
        
        next(w, r)
    }
}
func main() {
    store := NewInMemoryStore()
    handler := NewTodoHandler(store)
    
    // 라우팅
    mux := http.NewServeMux()
    
    // /todos 엔드포인트
    mux.HandleFunc("/todos", corsMiddleware(loggingMiddleware(
        func(w http.ResponseWriter, r *http.Request) {
            switch r.Method {
            case http.MethodGet:
                handler.GetTodos(w, r)
            case http.MethodPost:
                handler.CreateTodo(w, r)
            default:
                http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            }
        },
    )))
    
    // /todos/{id} 엔드포인트
    mux.HandleFunc("/todos/", corsMiddleware(loggingMiddleware(
        func(w http.ResponseWriter, r *http.Request) {
            switch r.Method {
            case http.MethodGet:
                handler.GetTodo(w, r)
            case http.MethodPut:
                handler.UpdateTodo(w, r)
            case http.MethodDelete:
                handler.DeleteTodo(w, r)
            default:
                http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
            }
        },
    )))
    
    // 헬스 체크
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprintln(w, "OK")
    })
    
    // 백그라운드 작업 시작
    go backgroundWorker(store)
    
    // 서버 시작
    addr := ":8080"
    log.Printf("Server starting on %s", addr)
    if err := http.ListenAndServe(addr, mux); err != nil {
        log.Fatal(err)
    }
}

5. 백그라운드 작업

// background.go
package main
import (
    "log"
    "time"
)
func backgroundWorker(store TodoStore) {
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            // 주기적 작업: 통계 출력
            todos, err := store.GetAll()
            if err != nil {
                log.Printf("Background worker error: %v", err)
                continue
            }
            
            completed := 0
            for _, todo := range todos {
                if todo.Completed {
                    completed++
                }
            }
            
            log.Printf("Stats: Total=%d, Completed=%d, Pending=%d",
                len(todos), completed, len(todos)-completed)
        }
    }
}
// 만료된 TODO 정리 (예시)
func cleanupWorker(store TodoStore) {
    ticker := time.NewTicker(1 * time.Hour)
    defer ticker.Stop()
    
    for range ticker.C {
        todos, err := store.GetAll()
        if err != nil {
            log.Printf("Cleanup error: %v", err)
            continue
        }
        
        now := time.Now()
        for _, todo := range todos {
            // 7일 이상 된 완료 항목 삭제
            if todo.Completed && now.Sub(todo.CreatedAt) > 7*24*time.Hour {
                if err := store.Delete(todo.ID); err != nil {
                    log.Printf("Delete error: %v", err)
                }
            }
        }
    }
}

6. 테스트 작성

저장소 테스트

// store_test.go
package main
import (
    "errors"
    "testing"
)
func TestInMemoryStore_Create(t *testing.T) {
    store := NewInMemoryStore()
    
    todo, err := store.Create("Test todo")
    if err != nil {
        t.Fatal(err)
    }
    
    if todo.ID != 1 {
        t.Errorf("got ID %d; want 1", todo.ID)
    }
    
    if todo.Title != "Test todo" {
        t.Errorf("got title %s; want 'Test todo'", todo.Title)
    }
    
    if todo.Completed {
        t.Error("new todo should not be completed")
    }
}
func TestInMemoryStore_GetAll(t *testing.T) {
    store := NewInMemoryStore()
    
    // 초기에는 비어있음
    todos, err := store.GetAll()
    if err != nil {
        t.Fatal(err)
    }
    if len(todos) != 0 {
        t.Errorf("got %d todos; want 0", len(todos))
    }
    
    // 3개 추가
    store.Create("Todo 1")
    store.Create("Todo 2")
    store.Create("Todo 3")
    
    todos, err = store.GetAll()
    if err != nil {
        t.Fatal(err)
    }
    if len(todos) != 3 {
        t.Errorf("got %d todos; want 3", len(todos))
    }
}
func TestInMemoryStore_Update(t *testing.T) {
    store := NewInMemoryStore()
    
    todo, _ := store.Create("Test")
    
    // 완료로 업데이트
    err := store.Update(todo.ID, true)
    if err != nil {
        t.Fatal(err)
    }
    
    // 확인
    updated, _ := store.GetByID(todo.ID)
    if !updated.Completed {
        t.Error("todo should be completed")
    }
}
func TestInMemoryStore_Delete(t *testing.T) {
    store := NewInMemoryStore()
    
    todo, _ := store.Create("Test")
    
    // 삭제
    err := store.Delete(todo.ID)
    if err != nil {
        t.Fatal(err)
    }
    
    // 확인
    _, err = store.GetByID(todo.ID)
    if !errors.Is(err, ErrNotFound) {
        t.Error("deleted todo should not be found")
    }
}

HTTP 핸들러 테스트

// handler_test.go
package main
import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)
func TestCreateTodo(t *testing.T) {
    store := NewInMemoryStore()
    handler := NewTodoHandler(store)
    
    reqBody := CreateTodoRequest{Title: "Test Todo"}
    body, _ := json.Marshal(reqBody)
    
    req := httptest.NewRequest(http.MethodPost, "/todos", bytes.NewBuffer(body))
    req.Header.Set("Content-Type", "application/json")
    
    w := httptest.NewRecorder()
    handler.CreateTodo(w, req)
    
    if w.Code != http.StatusCreated {
        t.Errorf("got status %d; want %d", w.Code, http.StatusCreated)
    }
    
    var todo Todo
    if err := json.NewDecoder(w.Body).Decode(&todo); err != nil {
        t.Fatal(err)
    }
    
    if todo.Title != "Test Todo" {
        t.Errorf("got title %s; want 'Test Todo'", todo.Title)
    }
}
func TestGetTodos(t *testing.T) {
    store := NewInMemoryStore()
    handler := NewTodoHandler(store)
    
    // 데이터 준비
    store.Create("Todo 1")
    store.Create("Todo 2")
    
    req := httptest.NewRequest(http.MethodGet, "/todos", nil)
    w := httptest.NewRecorder()
    
    handler.GetTodos(w, req)
    
    if w.Code != http.StatusOK {
        t.Errorf("got status %d; want %d", w.Code, http.StatusOK)
    }
    
    var todos []*Todo
    if err := json.NewDecoder(w.Body).Decode(&todos); err != nil {
        t.Fatal(err)
    }
    
    if len(todos) != 2 {
        t.Errorf("got %d todos; want 2", len(todos))
    }
}

7. 실행과 배포

로컬 실행

# 개발 모드 (자동 재시작 없음)
go run main.go handler.go store.go background.go
# 또는 빌드 후 실행
go build -o todo-api
./todo-api

API 테스트

# 헬스 체크
curl http://localhost:8080/health
# TODO 생성
curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"Learn Go"}'
# 모든 TODO 조회
curl http://localhost:8080/todos
# 특정 TODO 조회
curl http://localhost:8080/todos/1
# TODO 업데이트
curl -X PUT http://localhost:8080/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"completed":true}'
# TODO 삭제
curl -X DELETE http://localhost:8080/todos/1

Docker로 배포

# Dockerfile
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o todo-api
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/todo-api .
EXPOSE 8080
CMD [./todo-api]
# Docker 빌드 및 실행
docker build -t todo-api .
docker run -p 8080:8080 todo-api

프로덕션 개선 사항

// 환경 변수로 설정 관리
package main
import (
    "os"
    "strconv"
)
type Config struct {
    Port         string
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
}
func LoadConfig() *Config {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    return &Config{
        Port:         port,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }
}
func main() {
    config := LoadConfig()
    
    server := &http.Server{
        Addr:         ":" + config.Port,
        Handler:      mux,
        ReadTimeout:  config.ReadTimeout,
        WriteTimeout: config.WriteTimeout,
    }
    
    log.Printf("Server starting on %s", server.Addr)
    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

정리: Day 14 학습 체크리스트

완료해야 할 항목

  • REST API 설계 (CRUD 엔드포인트)
  • net/http로 HTTP 서버 구현
  • JSON 직렬화/역직렬화
  • 에러 처리와 HTTP 상태 코드
  • 미들웨어 패턴 구현
  • 고루틴으로 백그라운드 작업
  • HTTP 핸들러 테스트
  • Docker로 배포

2주 완성 최종 체크리스트

1주 차:

  • ✅ Go 기본 문법과 철학
  • ✅ 포인터와 자료구조
  • ✅ 구조체와 메서드
  • ✅ 인터페이스와 다형성 2주 차:
  • ✅ 에러 처리와 defer
  • ✅ 고루틴과 채널
  • ✅ 의존성 관리와 테스팅
  • ✅ 실전 REST API 프로젝트

C++에서 Go로 전환 완료!

graph TD
    A[C++ 개발자] --> B[Day 1-2: 기본 문법]
    B --> C[Day 3-4: 자료구조]
    C --> D[Day 5-6: 객체지향]
    D --> E[Day 7: 인터페이스]
    E --> F[Day 8-9: 에러 처리]
    F --> G[Day 10-11: 동시성]
    G --> H[Day 12-13: 테스팅]
    H --> I[Day 14: 실전 프로젝트]
    I --> J[Go 개발자!]
    
    style A fill:#ffcccc
    style J fill:#ccffcc

다음 학습 방향

프로젝트를 완성했다면:

  1. 데이터베이스 연동: PostgreSQL, MySQL, MongoDB
  2. 인증/인가: JWT, OAuth2
  3. 고급 패턴: 미들웨어 체인, 의존성 주입
  4. 성능 최적화: 프로파일링, 캐싱
  5. 배포: Kubernetes, Cloud Run, AWS Lambda 추천 프로젝트:
  • CLI 도구 (cobra 라이브러리)
  • 웹 크롤러 (colly 라이브러리)
  • 채팅 서버 (WebSocket)
  • gRPC 서버
  • 마이크로서비스

마무리: 2주 완성을 축하합니다!

당신이 이룬 것

2주 전, C++만 알던 당신이 이제는:

  • ✅ Go 문법을 자유롭게 사용
  • ✅ 고루틴으로 동시성 프로그래밍
  • ✅ 채널로 안전한 통신
  • ✅ 인터페이스로 유연한 설계
  • ✅ REST API 서버 구축

C++과 Go, 언제 무엇을 쓸까?

C++를 선택해야 할 때:

  • 극저지연이 필요한 시스템 (HFT, 게임 엔진)
  • 하드웨어 제어 (임베디드, 드라이버)
  • 레거시 코드베이스 유지보수
  • 메모리 사용량이 극도로 제한적인 환경 Go를 선택해야 할 때:
  • 웹 API, 마이크로서비스
  • CLI 도구, DevOps 도구
  • 클라우드 네이티브 애플리케이션
  • 동시성이 중요한 서버
  • 빠른 개발과 배포가 중요한 프로젝트

계속 성장하기

// Go 개발자로서의 다음 단계
package main
func main() {
    skills := []string{
        "Go 고급 패턴",
        "데이터베이스 연동",
        "gRPC와 Protobuf",
        "Kubernetes 연동",
        "성능 최적화",
    }
    
    for _, skill := range skills {
        go learn(skill)  // 병렬로 학습!
    }
}

📚 시리즈 네비게이션

이전 글목차완료!
← #07 테스팅📑 전체 목차🎉 시리즈 완료!
Go 2주 완성 시리즈:
커리큘럼#01 기본 문법#02 자료구조#03 객체지향#04 인터페이스#05 에러 처리#06 고루틴·채널#07 테스팅#08 REST API#09 context·우아한 종료
다음 읽을 글:

한 줄 요약: 2주간의 학습으로 C++ 개발자에서 Go 개발자로 전환 완료! 이제 클라우드 네이티브 시대의 필수 언어를 마스터했습니다.

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [Go 2주 완성 #07] Day 12~13: 의존성 관리와 테스팅 - CMake보다 쉬운 세상
  • [Go 심화 #09] context·우아한 종료
  • C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
  • C++ REST API 클라이언트 완벽 가이드 | CRUD·인증·에러 처리·프로덕션 패턴 [#21-3]

이 글에서 다루는 키워드 (관련 검색어)

Go REST API, net/http, encoding/json, Go 웹서버, 미니 프로젝트, Golang API, Go 2주 완성, C++ 개발자 Go 등으로 검색하시면 이 글이 도움이 됩니다.

🎉 시리즈 코어(2주) 완료를 축하합니다!

Go 2주 완성 시리즈 본편(#01~#08)을 마치셨습니다. 이제 실전 프로젝트에 Go를 적용해 보세요. 프로덕션으로 한 단계 올리기(강력 권장):

실전 팁 (Go)

  • go fmt, go vet, 필요 시 golangci-lint로 기본 품질을 맞춥니다.
  • if err != nil을 습관화합니다. 에러를 삼키면 운영에서 원인 추적이 어렵습니다.
  • go test ./...로 패키지 단위 회귀를 확인합니다. 성능은 go test -bench측정할 수 있을 때만 다룹니다.

실전 체크리스트 (Go)

코드

  • 외부 호출·I/O 실패 시 에러가 처리되거나 로깅되는가?
  • 고루틴·채널 사용 시 블로킹·누수 가능성을 검토했는가?

모듈

  • go.mod / go.sum이 팀과 합의된 범위인가?

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 2주간 배운 Go 언어의 모든 것을 통합하여 완전한 REST API 서버를 구축합니다. net/http, JSON 처리, 고루틴 활용, 에러 처리까지 실무에 바로 적용 가능한 프로젝트입니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. go.dev/docpkg.go.dev에서 표준 라이브러리·모듈 문서를 확인하세요. 사용 중인 서드파티 패키지는 저장소 README와 릴리스 노트를 함께 보는 것이 좋습니다.

관련 글

  • [Go 심화 #09] context.Context로 타임아웃·취소·우아한 종료 다루기 — C++와의 비교
  • Python REST API | Flask/Django로 API 서버 만들기
  • C++ 개발자를 위한 2주 완성 Go 언어(Golang) 마스터 커리큘럼
  • C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴
  • C++ JSON 파싱 완벽 가이드 | nlohmann·RapidJSON·커스텀 타입·에러 처리·프로덕션 패턴

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「[Go 2주 완성 #08] Day 14: 실전 미니 프로젝트 - REST API 서버 구축」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「[Go 2주 완성 #08] Day 14: 실전 미니 프로젝트 - REST API 서버 구축」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.