애플리케이션 구조 설계
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