Go μΈμ΄λ‘ ꡬμΆν RESTful API μΉ μ ν리μΌμ΄μ μλ²μ λλ€. Clean Architectureλ₯Ό μ μ©νμ¬ μ μ§λ³΄μμ±κ³Ό ν μ€νΈ μ©μ΄μ±μ ν보νμ΅λλ€.
- κΈ°μ μ€ν
- μν€ν μ²
- νλ‘μ νΈ κ΅¬μ‘°
- μμνκΈ°
- API λ¬Έμ
- κ°λ° κ°μ΄λ
- λ§μ΄κ·Έλ μ΄μ
| λΌμ΄λΈλ¬λ¦¬ | μ©λ | μ ν μ΄μ |
|---|---|---|
| Gin | HTTP μΉ νλ μμν¬ | λΉ λ₯Έ μ±λ₯, λ―Έλ€μ¨μ΄ μ§μ, νλΆν μνκ³ |
| ent | ORM (λ°μ΄ν°λ² μ΄μ€) | νμ μμ μ±, μ½λ μμ±, κ΄κ³ κ΄λ¦¬ μ©μ΄ |
| zerolog | ꡬ쑰νλ λ‘κΉ | κ³ μ±λ₯, JSON μΆλ ₯, μ λ‘ ν λΉ |
| swag | Swagger λ¬Έμ μμ± | μ£Όμ κΈ°λ° μλ μμ±, Gin ν΅ν© |
| godotenv | νκ²½ λ³μ κ΄λ¦¬ | .env νμΌ λ‘λ, μ€μ λΆλ¦¬ |
| valkey-go | μΊμ ν΄λΌμ΄μΈνΈ | Redis νΈν, κ³ μ±λ₯ |
| μλΉμ€ | μ©λ | ν¬νΈ |
|---|---|---|
| PostgreSQL 16 | λ©μΈ λ°μ΄ν°λ² μ΄μ€ | 5433 |
| Valkey 8 | μΊμ (Redis νΈν) | 6380 |
μ΄ νλ‘μ νΈλ Clean Architectureλ₯Ό μ μ©νμ΅λλ€. κ° κ³μΈ΅μ λͺ νν μ± μμ κ°μ§λ©°, μμ‘΄μ±μ νμ μμͺ½(λλ©μΈ)μ ν₯ν©λλ€.
graph TB
subgraph "μΈλΆ κ³μΈ΅"
A[HTTP Request] --> B[Middleware]
B --> C[Handler]
end
subgraph "μ ν리μΌμ΄μ
κ³μΈ΅"
C --> D[Usecase]
end
subgraph "μΈνλΌ κ³μΈ΅"
D --> E[Repository]
E --> F[ent Client]
F --> G[(PostgreSQL)]
end
subgraph "λλ©μΈ κ³μΈ΅"
H[DTO / Entity]
end
D -.-> H
E -.-> H
C -.-> H
flowchart LR
subgraph Handler["Handler (Presentation)"]
direction TB
H1[HTTP μμ²/μλ΅ μ²λ¦¬]
H2[μ
λ ₯ μ ν¨μ± κ²μ¬]
H3[DTO λ³ν]
end
subgraph Usecase["Usecase (Business Logic)"]
direction TB
U1[λΉμ¦λμ€ κ·μΉ μ μ©]
U2[νΈλμμ
κ΄λ¦¬]
U3[μλ¬ μ²λ¦¬]
end
subgraph Repository["Repository (Data Access)"]
direction TB
R1[λ°μ΄ν° CRUD]
R2[쿼리 λΉλ]
R3[μΊμ μ²λ¦¬]
end
Handler --> Usecase --> Repository
| μ₯μ | μ€λͺ |
|---|---|
| ν μ€νΈ μ©μ΄μ± | κ° κ³μΈ΅μ λ 립μ μΌλ‘ ν μ€νΈ κ°λ₯ (Mock μ£Όμ ) |
| μ μ§λ³΄μμ± | λ³κ²½ μν₯ λ²μκ° ν΄λΉ κ³μΈ΅μ κ΅νλ¨ |
| νμ₯μ± | μλ‘μ΄ κΈ°λ₯ μΆκ° μ κΈ°μ‘΄ μ½λ μμ μ΅μν |
| μμ‘΄μ± μμ | μΈν°νμ΄μ€λ₯Ό ν΅ν΄ ꡬν체 κ΅μ²΄ μ©μ΄ |
μ¬μ©μ μμ± μμ²μ μ 체 νλ¦:
sequenceDiagram
participant Client
participant Middleware
participant Handler
participant Usecase
participant Repository
participant Database
Client->>Middleware: POST /api/v1/users
Middleware->>Middleware: Trace ID μμ±
Middleware->>Middleware: λ‘κΉ
μμ
Middleware->>Handler: μμ² μ λ¬
Handler->>Handler: JSON λ°μΈλ© & μ ν¨μ± κ²μ¬
Handler->>Usecase: Create(ctx, CreateRequest)
Usecase->>Repository: GetByEmail(ctx, email)
Repository->>Database: SELECT * FROM users WHERE email = ?
Database-->>Repository: κ²°κ³Ό (μμ)
Repository-->>Usecase: nil
Usecase->>Usecase: λΉλ°λ²νΈ ν΄μ±
Usecase->>Repository: Create(ctx, name, email, password)
Repository->>Database: INSERT INTO users ...
Database-->>Repository: μμ±λ User
Repository-->>Usecase: *ent.User
Usecase->>Usecase: DTO λ³ν
Usecase-->>Handler: *user.Response
Handler-->>Middleware: JSON μλ΅
Middleware->>Middleware: λ‘κΉ
μλ£ (latency ν¬ν¨)
Middleware-->>Client: 201 Created + X-Response-Time
go-webserver/
βββ cmd/
β βββ server/
β βββ main.go # μ ν리μΌμ΄μ
μ§μ
μ
βββ internal/ # λΉκ³΅κ° μ ν리μΌμ΄μ
μ½λ
β βββ config/
β β βββ config.go # νκ²½ μ€μ λ‘λ
β βββ domain/ # λλ©μΈ DTO
β β βββ user/
β β β βββ dto.go # μ¬μ©μ μμ²/μλ΅ DTO
β β βββ product/
β β β βββ dto.go
β β βββ order/
β β βββ dto.go
β βββ handler/ # HTTP νΈλ€λ¬ (Controller)
β β βββ router.go # λΌμ°ν° μ€μ
β β βββ user_handler.go
β β βββ product_handler.go
β β βββ order_handler.go
β β βββ health_handler.go
β βββ usecase/ # λΉμ¦λμ€ λ‘μ§
β β βββ user_usecase.go
β β βββ product_usecase.go
β β βββ order_usecase.go
β βββ repository/ # λ°μ΄ν° μ κ·Ό κ³μΈ΅
β β βββ user_repository.go
β β βββ product_repository.go
β β βββ order_repository.go
β βββ middleware/ # HTTP λ―Έλ€μ¨μ΄
β β βββ logger.go # μμ² λ‘κΉ
+ Latency
β β βββ trace.go # Trace ID μμ±
β β βββ cors.go # CORS μ€μ
β β βββ recovery.go # ν¨λ 볡ꡬ
β βββ infrastructure/ # μΈλΆ μλΉμ€ μ°κ²°
β βββ database/
β β βββ database.go # PostgreSQL μ°κ²°
β βββ cache/
β β βββ cache.go # Valkey μ°κ²°
β βββ logger/
β βββ logger.go # zerolog μ€μ
βββ ent/ # ent ORM (μλ μμ±)
β βββ schema/ # μ€ν€λ§ μ μ (μ§μ μμ±)
β β βββ user.go
β β βββ product.go
β β βββ order.go
β β βββ orderitem.go
β βββ migrate/
β βββ main.go # λ§μ΄κ·Έλ μ΄μ
μμ±κΈ°
βββ migrations/ # SQL λ§μ΄κ·Έλ μ΄μ
νμΌ
βββ docs/ # Swagger λ¬Έμ (μλ μμ±)
βββ .air.toml # Hot reload μ€μ
βββ .env # νκ²½ λ³μ
βββ docker-compose.yml # Docker μ€μ
βββ Makefile # λΉλ μ€ν¬λ¦½νΈ
βββ go.mod # Go λͺ¨λ μ μ
| λλ ν 리 | κ·μΉ |
|---|---|
cmd/ |
μ€ν νμΌλ³ μ§μ μ . μ΅μνμ μ½λλ§ ν¬ν¨ |
internal/ |
μΈλΆ ν¨ν€μ§μμ import λΆκ°. μ ν리μΌμ΄μ μ μ© μ½λ |
ent/schema/ |
μ§μ μμ±. λλ¨Έμ§ ent μ½λλ μλ μμ± |
docs/ |
swag λͺ λ ΉμΌλ‘ μλ μμ±. μ§μ μμ κΈμ§ |
- Go 1.21+
- Docker & Docker Compose
- Make
# 1. μ μ₯μ ν΄λ‘
git clone <repository-url>
cd go-webserver
# 2. νκ²½ λ³μ μ€μ
cp .env.example .env # λλ κΈ°μ‘΄ .env μ¬μ©
# 3. Docker 컨ν
μ΄λ μμ (PostgreSQL, Valkey)
make docker-up
# 4. μμ‘΄μ± μ€μΉ λ° μ½λ μμ±
make setup
# 5. λ§μ΄κ·Έλ μ΄μ
μ μ©
make migrate-up
# 6. μλ² μ€ν
make run# air μ€μΉ (μ΅μ΄ 1ν)
go install github.com/air-verse/air@latest
# κ°λ° μλ² μ€ν
make dev
# λλ
make airmake build # λ°μ΄λ리 λΉλ
make run # μλ² μ€ν
make dev # κ°λ° λͺ¨λ (hot reload)
make test # ν
μ€νΈ μ€ν
make test-coverage # ν
μ€νΈ 컀λ²λ¦¬μ§ 리ν¬νΈ
make docker-up # Docker 컨ν
μ΄λ μμ
make docker-down # Docker 컨ν
μ΄λ μ’
λ£
make docker-logs # Docker λ‘κ·Έ νμΈ
make generate # ent μ½λ μμ±
make swagger # Swagger λ¬Έμ μμ±
make lint # μ½λ λ¦°νΈ
make migrate name=init # λ§μ΄κ·Έλ μ΄μ
νμΌ μμ±
make migrate-up # λ§μ΄κ·Έλ μ΄μ
μ μ©
make migrate-down # λ§μ΄κ·Έλ μ΄μ
λ‘€λ°±
make migrate-status # λ§μ΄κ·Έλ μ΄μ
μν νμΈμλ² μ€ν ν Swagger UIμμ APIλ₯Ό νμΈνκ³ ν μ€νΈν μ μμ΅λλ€:
http://localhost:8080/swagger/index.html
| λ©μλ | κ²½λ‘ | μ€λͺ |
|---|---|---|
| GET | /api/v1/health |
ν¬μ€ μ²΄ν¬ |
| Users | ||
| POST | /api/v1/users |
μ¬μ©μ μμ± |
| GET | /api/v1/users |
μ¬μ©μ λͺ©λ‘ μ‘°ν |
| GET | /api/v1/users/:id |
μ¬μ©μ μμΈ μ‘°ν |
| PUT | /api/v1/users/:id |
μ¬μ©μ μμ |
| DELETE | /api/v1/users/:id |
μ¬μ©μ μμ |
| Products | ||
| POST | /api/v1/products |
μν μμ± |
| GET | /api/v1/products |
μν λͺ©λ‘ μ‘°ν |
| GET | /api/v1/products/:id |
μν μμΈ μ‘°ν |
| PUT | /api/v1/products/:id |
μν μμ |
| DELETE | /api/v1/products/:id |
μν μμ |
| Orders | ||
| POST | /api/v1/orders |
μ£Όλ¬Έ μμ± |
| GET | /api/v1/orders/:id |
μ£Όλ¬Έ μμΈ μ‘°ν |
| PATCH | /api/v1/orders/:id/status |
μ£Όλ¬Έ μν λ³κ²½ |
| DELETE | /api/v1/orders/:id |
μ£Όλ¬Έ μμ |
| GET | /api/v1/users/:id/orders |
μ¬μ©μλ³ μ£Όλ¬Έ μ‘°ν |
- ent μ€ν€λ§ μ μ (
ent/schema/)
// ent/schema/category.go
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/field"
)
type Category struct {
ent.Schema
}
func (Category) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
field.String("description").Optional(),
}
}- ent μ½λ μμ±
make generate- DTO μ μ (
internal/domain/category/)
// internal/domain/category/dto.go
package category
type CreateRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
}
type Response struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
}-
Repository μμ± (
internal/repository/) -
Usecase μμ± (
internal/usecase/) -
Handler μμ± (
internal/handler/) -
λΌμ°ν° λ±λ‘ (
internal/handler/router.go) -
Swagger λ¬Έμ μμ±
make swaggerGoμμλ context.Contextλ₯Ό λͺ¨λ κ³μΈ΅μ λͺ
μμ μΌλ‘ μ λ¬ν©λλ€:
// Handler
func (h *UserHandler) GetByID(c *gin.Context) {
ctx := c.Request.Context() // gin.Contextμμ μΆμΆ
user, err := h.usecase.GetByID(ctx, id)
// ...
}
// Usecase
func (u *userUsecase) GetByID(ctx context.Context, id int) (*user.Response, error) {
found, err := u.repo.GetByID(ctx, id)
// ...
}
// Repository
func (r *userRepository) GetByID(ctx context.Context, id int) (*ent.User, error) {
return r.client.User.Get(ctx, id) // entμ μ λ¬
}μ΄λ κ² νλ©΄:
- Trace IDκ° λͺ¨λ λ‘κ·Έμ μλ ν¬ν¨
- μμ² μ·¨μκ° DB 쿼리κΉμ§ μ ν
- νμμμ μ€μ κ°λ₯
import "go-webserver/internal/infrastructure/logger"
// κΈ°λ³Έ λ‘κΉ
logger.Info().Str("key", "value").Msg("λ©μμ§")
// Context κΈ°λ° λ‘κΉ
(Trace ID μλ ν¬ν¨)
logger.Ctx(ctx).Debug().Interface("user", user).Msg("μ¬μ©μ μ‘°ν")
// λ‘κ·Έ λ 벨
logger.Debug() // κ°λ°μ© μμΈ μ 보
logger.Info() // μΌλ° μ 보
logger.Warn() // κ²½κ³
logger.Error() // μλ¬
logger.Fatal() // μΉλͺ
μ μλ¬ (νλ‘κ·Έλ¨ μ’
λ£)// usecase ν
μ€νΈ μμ
func TestUserUsecase_Create(t *testing.T) {
// Mock repository μ€μ
mockRepo := new(MockUserRepository)
usecase := NewUserUsecase(mockRepo)
// Given
req := user.CreateRequest{
Name: "ν
μ€νΈ",
Email: "test@example.com",
Password: "password123",
}
// When
result, err := usecase.Create(context.Background(), req)
// Then
assert.NoError(t, err)
assert.Equal(t, "ν
μ€νΈ", result.Name)
}μ΄ νλ‘μ νΈλ golang-migrateλ₯Ό μ¬μ©ν©λλ€.
# ent μ€ν€λ§λ₯Ό κΈ°λ°μΌλ‘ SQL μμ±
make migrate name=add_categoriesμμ±λλ νμΌ:
migrations/20240118120000_add_categories.up.sqlmigrations/20240118120000_add_categories.down.sql
# μλ‘μ΄ λ§μ΄κ·Έλ μ΄μ
λ§ μ μ© (μ΄λ―Έ μ μ©λ κ²μ μ€ν΅)
make migrate-up
# 1λ¨κ³ λ‘€λ°±
make migrate-down
# νμ¬ λ²μ νμΈ
make migrate-statusschema_migrations ν
μ΄λΈμμ μ μ©λ λ§μ΄κ·Έλ μ΄μ
μ μΆμ ν©λλ€:
SELECT * FROM schema_migrations;
-- version | dirty
-- 20240118120000 | falseλͺ¨λ API μλ΅μ λ€μ ν€λκ° ν¬ν¨λ©λλ€:
| ν€λ | μ€λͺ | μμ |
|---|---|---|
X-Trace-ID |
μμ² μΆμ ID | a838ac04 |
X-Response-Time |
μλ΅ μκ° (ms) | 1.234 |
MIT License