Go 언어 기반 REST API 유닛 테스트 및 Gorilla Mux 연동 가이드

애플리케이션 구조 설계

Go 언어로 견고한 REST API를 구축하기 위해서는 데이터베이스 연결과 라우팅 로직을 추상화하는 구조가 필요합니다. 아래는 Gorilla Mux와 MySQL을 활용한 애플리케이션의 핵심 구조체인 RestServer 예제입니다.

package main

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"

    _ "github.com/go-sql-driver/mysql"
    "github.com/gorilla/mux"
)

type RestServer struct {
    Router *mux.Router
    DB     *sql.DB
}

func (s *RestServer) Init(user, password, host, dbname string) {
    connStr := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True", user, password, host, dbname)
    
    var err error
    s.DB, err = sql.Open("mysql", connStr)
    if err != nil {
        log.Fatal(err)
    }

    s.Router = mux.NewRouter()
    s.registerRoutes()
}

func (s *RestServer) registerRoutes() {
    s.Router.HandleFunc("/items", s.listItems).Methods("GET")
    s.Router.HandleFunc("/item", s.addItem).Methods("POST")
    s.Router.HandleFunc("/item/{id:[0-9]+}", s.getItem).Methods("GET")
    s.Router.HandleFunc("/item/{id:[0-9]+}", s.modifyItem).Methods("PUT")
    s.Router.HandleFunc("/item/{id:[0-9]+}", s.removeItem).Methods("DELETE")
}

func (s *RestServer) Run(addr string) {
    log.Fatal(http.ListenAndServe(addr, s.Router))
}

데이터 엔티티 및 모델링

데이터베이스와 통신하는 모델 계층에서는 SQL 쿼리를 수행하고 결과를 구조체로 매핑합니다. Item 구조체를 통해 상품 정보를 관리합니다.

type Item struct {
    ID    int     `json:"id"`
    Title string  `json:"title"`
    Cost  float64 `json:"cost"`
}

func (i *Item) Fetch(db *sql.DB) error {
    return db.QueryRow("SELECT title, cost FROM items WHERE id=?", i.ID).Scan(&i.Title, &i.Cost)
}

func (i *Item) Create(db *sql.DB) error {
    _, err := db.Exec("INSERT INTO items(title, cost) VALUES(?, ?)", i.Title, i.Cost)
    return err
}

func FetchItems(db *sql.DB, start, limit int) ([]Item, error) {
    rows, err := db.Query("SELECT id, title, cost FROM items LIMIT ? OFFSET ?", limit, start)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    results := []Item{}
    for rows.Next() {
        var it Item
        if err := rows.Scan(&it.ID, &it.Title, &it.Cost); err != nil {
            return nil, err
        }
        results = append(results, it)
    }
    return results, nil
}

핸들러 및 응답 처리

HTTP 요청을 처리하고 JSON 형식의 표준 응답을 반환하는 로직입니다. 헬퍼 함수를 사용하여 코드 중복을 줄입니다.

func (s *RestServer) getItem(w http.ResponseWriter, r *http.Request) {
    params := mux.Vars(r)
    id, _ := strconv.Atoi(params["id"])

    it := Item{ID: id}
    if err := it.Fetch(s.DB); err != nil {
        if err == sql.ErrNoRows {
            sendError(w, http.StatusNotFound, "Item not found")
        } else {
            sendError(w, http.StatusInternalServerError, err.Error())
        }
        return
    }
    sendJSON(w, http.StatusOK, it)
}

func sendError(w http.ResponseWriter, code int, msg string) {
    sendJSON(w, code, map[string]string{"error": msg})
}

func sendJSON(w http.ResponseWriter, code int, data interface{}) {
    res, _ := json.Marshal(data)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(code)
    w.Write(res)
}

httptest를 활용한 단위 테스트 구현

Go의 net/http/httptest 패키지는 실제 서버를 띄우지 않고도 HTTP 핸들러를 테스트할 수 있는 강력한 기능을 제공합니다. ResponseRecorder를 통해 서버의 응답을 기록하고 검증합니다.

package main_test

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "os"
    "testing"
    "your_project/app" // 실제 경로에 맞게 수정
)

var server app.RestServer

func TestMain(m *testing.M) {
    server = app.RestServer{}
    server.Init("db_user", "password", "localhost", "test_db")
    
    // 테이블 생성 등 초기화 로직
    setupTestDB()
    
    exitVal := m.Run()
    
    // 테스트 후 정리
    os.Exit(exitVal)
}

func performRequest(req *http.Request) *httptest.ResponseRecorder {
    rr := httptest.NewRecorder()
    server.Router.ServeHTTP(rr, req)
    return rr
}

func TestGetItemHandler(t *testing.T) {
    // 임의의 데이터 생성 로직 필요
    
    req, _ := http.NewRequest("GET", "/item/1", nil)
    response := performRequest(req)

    if response.Code != http.StatusOK {
        t.Errorf("상태 코드 불일치: 예상 %d, 결과 %d", http.StatusOK, response.Code)
    }

    var result map[string]interface{}
    json.Unmarshal(response.Body.Bytes(), &result)

    if result["id"] != 1.0 { // JSON 숫자는 float64로 언마샬링됨
        t.Errorf("잘못된 아이템 ID 반환")
    }
}

func TestCreateItemHandler(t *testing.T) {
    payload := []byte(`{"title":"New Product", "cost":2500}`)
    req, _ := http.NewRequest("POST", "/item", bytes.NewBuffer(payload))
    response := performRequest(req)

    if response.Code != http.StatusCreated {
        t.Errorf("생성 실패: 예상 %d, 결과 %d", http.StatusCreated, response.Code)
    }
}

테스트 실행 및 검증

명령행에서 go test -v 명령을 실행하여 작성된 모든 테스트 케이스의 통과 여부를 확인합니다. 각 테스트는 독립적인 환경을 유지하기 위해 테이블 데이터를 초기화하거나 트랜잭션을 활용하는 것이 권장됩니다.

$ go test -v
=== RUN   TestGetItemHandler
--- PASS: TestGetItemHandler (0.15s)
=== RUN   TestCreateItemHandler
--- PASS: TestCreateItemHandler (0.12s)
PASS
ok      your_project    0.27s

태그: Golang gorilla-mux unit-testing MySQL rest-api

7월 16일 17:45에 게시됨