Feature-based Modular 구조

백엔드&인프라 추종자·2025년 11월 4일

개인생각

조금은 익숙했던 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)이 독립적인 패키지입니다.
즉, “모듈 단위로 완결된 구조”를 유지합니다.


🧩 1️⃣ internal/user/model.go

package user

type User struct {
	ID    int64  `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

🧩 2️⃣ internal/user/storage.go

package 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
}

🧩 3️⃣ internal/user/service.go

package 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)
}

🧩 4️⃣ internal/user/handler.go

package 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)
}

🧩 5️⃣ cmd/main.go

package 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스럽다불필요한 계층 없이 실제 기능 중심 설계

🧠 실무에서 이 구조가 쓰이는 이유

  • Gin, Fiber, Echo 모두 이런 feature-based 구조를 채택함
  • 각 기능 단위(user, auth, file, video)가 독립적으로 동작
  • 도메인 크면 microservice로 쉽게 쪼갤 수 있음
  • 유지보수 시 “user 기능만 수정” 가능
  • 테스트 코드도 모듈별로 작성 용이 (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())

profile
AI 답변 글을 주로 올립니다.

0개의 댓글