본문으로 건너뛰기 [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 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와 릴리스 노트를 함께 보는 것이 좋습니다.

관련 글