조금은 익숙했던 spring MVC 패턴 구조를 생각했었는데
역시 golang 스러운 구조는 아니었던 것 같다.
아래 구조가 실무형이라고 하는데.. 글쎄..
좋습니다 👍 정우님이 지금 가진 handler → service → storage 구조를
“Feature-based Modular” (실무형) 으로 리팩토링한 예시를 아래에 정리해드리겠습니다.
이 구조는 Go스럽고 실무에서 가장 많이 쓰이는 형태입니다.
(특히 Gin, Fiber, Echo 기반 프로젝트에서 공통적으로 사용됨.)
myapp/
├── cmd/
│ └── main.go
├── internal/
│ ├── user/
│ │ ├── handler.go
│ │ ├── service.go
│ │ ├── storage.go
│ │ └── model.go
│ ├── product/
│ │ ├── handler.go
│ │ ├── service.go
│ │ ├── storage.go
│ │ └── model.go
│ └── config/
│ └── config.go
└── go.mod
👉 각 기능(user, product)이 독립적인 패키지입니다.
즉, “모듈 단위로 완결된 구조”를 유지합니다.
internal/user/model.gopackage user
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
internal/user/storage.gopackage user
import "errors"
type Storage interface {
FindAll() ([]User, error)
FindByID(id int64) (*User, error)
Create(u *User) error
Delete(id int64) error
}
type memoryStorage struct {
data map[int64]User
}
func NewMemoryStorage() Storage {
return &memoryStorage{
data: make(map[int64]User),
}
}
func (s *memoryStorage) FindAll() ([]User, error) {
users := make([]User, 0, len(s.data))
for _, u := range s.data {
users = append(users, u)
}
return users, nil
}
func (s *memoryStorage) FindByID(id int64) (*User, error) {
u, ok := s.data[id]
if !ok {
return nil, errors.New("user not found")
}
return &u, nil
}
func (s *memoryStorage) Create(u *User) error {
if _, exists := s.data[u.ID]; exists {
return errors.New("user already exists")
}
s.data[u.ID] = *u
return nil
}
func (s *memoryStorage) Delete(id int64) error {
delete(s.data, id)
return nil
}
internal/user/service.gopackage user
import "errors"
type Service interface {
GetAll() ([]User, error)
GetByID(id int64) (*User, error)
Create(u *User) error
Delete(id int64) error
}
type service struct {
storage Storage
}
func NewService(s Storage) Service {
return &service{storage: s}
}
func (svc *service) GetAll() ([]User, error) {
return svc.storage.FindAll()
}
func (svc *service) GetByID(id int64) (*User, error) {
return svc.storage.FindByID(id)
}
func (svc *service) Create(u *User) error {
if u.Name == "" || u.Email == "" {
return errors.New("invalid user data")
}
return svc.storage.Create(u)
}
func (svc *service) Delete(id int64) error {
return svc.storage.Delete(id)
}
internal/user/handler.gopackage user
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type Handler struct {
service Service
}
func NewHandler(r *gin.Engine, svc Service) {
h := &Handler{service: svc}
group := r.Group("/users")
{
group.GET("", h.GetAll)
group.GET("/:id", h.GetByID)
group.POST("", h.Create)
group.DELETE("/:id", h.Delete)
}
}
func (h *Handler) GetAll(c *gin.Context) {
users, err := h.service.GetAll()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, users)
}
func (h *Handler) GetByID(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
user, err := h.service.GetByID(id)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
func (h *Handler) Create(c *gin.Context) {
var u User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.service.Create(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, u)
}
func (h *Handler) Delete(c *gin.Context) {
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
if err := h.service.Delete(id); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.Status(http.StatusNoContent)
}
cmd/main.gopackage main
import (
"github.com/gin-gonic/gin"
"myapp/internal/user"
)
func main() {
r := gin.Default()
// user 모듈 초기화
store := user.NewMemoryStorage()
svc := user.NewService(store)
user.NewHandler(r, svc)
r.Run(":8080")
}
| 특징 | 설명 |
|---|---|
| 기능 단위 독립 | user, product, auth 등 각각 완결된 모듈로 분리 |
| Go module-friendly | 다른 모듈로 쉽게 분리, 마이크로서비스 전환 쉬움 |
| 테스트 용이 | 각 모듈 단위로 단위 테스트 가능 |
| 의존성 방향이 명확 | handler → service → storage (단방향) |
| Go스럽다 | 불필요한 계층 없이 실제 기능 중심 설계 |
user, auth, file, video)가 독립적으로 동작internal/user/user_test.go 등)예를 들어 product 모듈을 추가할 때는 👇
internal/
└── product/
├── handler.go
├── service.go
├── storage.go
└── model.go
그냥 cmd/main.go에 아래 한 줄만 추가하면 됩니다:
product.NewHandler(r, product.NewService(product.NewMemoryStorage()))
이게 바로 Go스럽고 실무에서 유지보수가 쉬운 구조입니다.
원하신다면 👉 이 구조를 그대로 유지하면서
PostgreSQL (pgx)을 실제 storage 계층에 붙인 버전도 만들어드릴까요?
(NewMemoryStorage() 대신 NewPostgresStorage())