Go에서 MVC 및 DDD 계층 아키텍처 비교: 상세 가이드
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Go 언어 MVC 및 DDD 계층 아키텍처의 상세 비교
MVC와 DDD는 백엔드 개발에서 널리 사용되는 두 가지 계층 아키텍처 개념입니다. MVC(Model-View-Controller)는 더 쉬운 디커플링 및 계층화를 위해 사용자 인터페이스, 비즈니스 로직 및 데이터 모델을 분리하는 데 주로 사용되는 디자인 패턴인 반면, DDD(Domain-Driven Design)는 비즈니스 도메인 모델을 구축하여 복잡한 시스템의 설계 및 유지 관리 어려움을 해결하는 데 목표를 둔 아키텍처 방법론입니다.
Java 생태계에서는 많은 시스템이 점진적으로 MVC에서 DDD로 전환되었습니다. 그러나 단순성과 효율성을 옹호하는 Go, Python 및 NodeJS와 같은 언어에서는 MVC가 주류 아키텍처로 남아 있습니다. 아래에서는 Go 언어를 기반으로 MVC와 DDD 간의 디렉토리 구조 차이점을 구체적으로 논의합니다.
MVC 다이어그램 구조
+------------------+
| View | 사용자 인터페이스 레이어: 데이터 표시 및 사용자 상호 작용을 담당합니다 (예: HTML 페이지, API 응답)
+------------------+
| Controller | 컨트롤러 레이어: 사용자 요청을 처리하고, 서비스 로직을 호출하고, 모델 및 뷰를 조정합니다
+------------------+
| Model | 모델 레이어: 데이터 객체 (예: 데이터베이스 테이블 구조) 및 일부 비즈니스 로직을 포함합니다 (종종 서비스 레이어에 흩어져 있음)
+------------------+
DDD 다이어그램 구조
+--------------------+
| User Interface | 사용자 상호 작용 및 표시를 담당합니다 (예: REST API, 웹 인터페이스)
+--------------------+
| Application Layer | 비즈니스 프로세스를 조정합니다 (예: 도메인 서비스 호출, 트랜잭션 관리), 핵심 비즈니스 규칙은 포함하지 않습니다
+--------------------+
| Domain Layer | 핵심 비즈니스 로직 레이어: 집계 루트, 엔터티, 값 객체, 도메인 서비스 등을 포함하며, 비즈니스 규칙을 캡슐화합니다
+--------------------+
| Infrastructure | 기술적 구현을 제공합니다 (예: 데이터베이스 액세스, 메시지 큐, 외부 API)
+--------------------+
MVC와 DDD의 주요 차이점
-
코드 구성 로직
- MVC는 기술 기능별로 계층화(컨트롤러/서비스/DAO)하여 기술 구현에 중점을 둡니다.
- DDD는 비즈니스 도메인별로 모듈을 분할(예: 주문 도메인, 결제 도메인)하여 제한된 컨텍스트를 통해 핵심 비즈니스 로직을 격리합니다.
-
비즈니스 로직의 전달자
- MVC는 일반적으로 빈혈 모델을 채택하여 데이터(모델)와 동작(서비스)을 분리하므로 로직이 분산되어 유지 관리 비용이 높습니다.
- DDD는 집계 루트 및 도메인 서비스를 통해 풍부한 모델을 달성하여 비즈니스 로직을 도메인 레이어에 집중시키고 확장성을 향상시킵니다.
-
적용 가능성 및 비용
- MVC는 개발 비용이 낮고 요구 사항이 안정적인 중소규모 시스템에 적합합니다.
- DDD는 사전 도메인 모델링 및 통합 언어가 필요하므로 복잡한 비즈니스 및 장기적인 진화 요구 사항이 있는 대규모 시스템에 적합하지만 팀은 도메인 추상화 기능을 갖추어야 합니다. 예를 들어 전자 상거래 프로모션 규칙에서 DDD는 로직이 여러 서비스에 분산되는 것을 방지할 수 있습니다.
Go 언어 MVC 디렉토리 구조
MVC는 주로 뷰, 컨트롤러 및 모델의 세 가지 레이어로 나뉩니다.
gin-order/
├── cmd
│ └── main.go # 애플리케이션 진입점, Gin 엔진 시작
├── internal
│ ├── controllers # 컨트롤러 레이어 (HTTP 요청 처리), 핸들러라고도 함
│ │ └── order
│ │ └── order_controller.go # 주문 모듈용 컨트롤러
│ ├── services # 서비스 레이어 (비즈니스 로직 처리)
│ │ └── order
│ │ └── order_service.go # 주문 모듈용 서비스 구현
│ ├── repository # 데이터 액세스 레이어 (데이터베이스와 상호 작용)
│ │ └── order
│ │ └── order_repository.go # 주문 모듈용 데이터 액세스 인터페이스 및 구현
│ ├── models # 모델 레이어 (데이터 구조 정의)
│ │ └── order
│ │ └── order.go # 주문 모듈용 데이터 모델
│ ├── middleware # 미들웨어 (예: 인증, 로깅, 요청 인터셉션)
│ │ ├── logging.go # 로깅 미들웨어
│ │ └── auth.go # 인증 미들웨어
│ └── config # 구성 모듈 (데이터베이스, 서버 구성 등)
│ └── config.go # 애플리케이션 및 환경 구성
├── pkg # 공통 유틸리티 패키지 (예: 응답 래퍼)
│ └── response.go # 응답 처리 유틸리티 메서드
├── web # 프런트엔드 리소스 (템플릿 및 정적 자산)
│ ├── static # 정적 리소스 (CSS, JS, 이미지)
│ └── templates # 템플릿 파일 (HTML 템플릿)
│ └── order.tmpl # 주문 모듈용 뷰 템플릿 (HTML 렌더링이 필요한 경우)
├── go.mod # Go 모듈 관리 파일
└── go.sum # Go 모듈 종속성 잠금 파일
Go 언어 DDD 디렉토리 구조
DDD는 주로 인터페이스, 애플리케이션, 도메인 및 인프라의 네 가지 레이어로 나뉩니다.
go-web/
│── cmd/
│ └── main.go # 애플리케이션 진입점
│── internal/
│ ├── application/ # 애플리케이션 레이어 (도메인 로직 조정, 사용 사례 처리)
│ │ ├── services/ # 서비스 레이어, 비즈니스 로직 디렉토리
│ │ │ └── order_service.go # 주문 애플리케이션 서비스, 도메인 레이어 비즈니스 로직 호출
│ ├── domain/ # 도메인 레이어 (핵심 비즈니스 로직 및 인터페이스 정의)
│ │ ├── order/ # 주문 집계
│ │ │ ├── order.go # 주문 엔터티 (집계 루트), 핵심 비즈니스 로직 포함
│ │ ├── repository/ # 일반 리포지토리 인터페이스
│ │ │ ├── repository.go # 일반 리포지토리 인터페이스 (CRUD 작업)
│ │ │ └── order_repository.go # 주문 리포지토리 인터페이스, 주문 데이터에 대한 작업 정의
│ ├── infrastructure/ # 인프라 레이어 (도메인 레이어에서 정의된 인터페이스 구현)
│ │ ├── repository/ # 리포지토리 구현
│ │ │ └── order_repository_impl.go # 주문 리포지토리 구현, 구체적인 주문 데이터 저장
│ └── interfaces/ # 인터페이스 레이어 (HTTP 인터페이스와 같은 외부 요청 처리)
│ │ ├── handlers/ # HTTP 핸들러
│ │ │ └── order_handler.go # 주문용 HTTP 핸들러
│ │ └── routes/
│ │ │ ├── router.go # 기본 라우터 유틸리티 설정
│ │ │ └── order-routes.go # 주문 라우트 구성
│ │ │ └── order-routes-test.go # 주문 라우트 테스트
│ └── middleware/ # 미들웨어 (예: 인증, 인터셉션, 권한 부여 등)
│ │ └── logging.go # 로깅 미들웨어
│ ├── config/ # 서비스 관련 구성
│ │ └── server_config.go # 서버 구성 (예: 포트, 시간 초과 설정 등)
│── pkg/ # 재사용 가능한 공용 라이브러리
│ └── utils/ # 유틸리티 클래스 (예: 로깅, 날짜 처리 등)
Go 언어 MVC 코드 구현
Controller (인터페이스 레이어) → Service (비즈니스 로직 레이어) → Repository (데이터 액세스 레이어) → Model (데이터 모델)
계층화된 코드
컨트롤러 레이어
// internal/controller/order/order.go package order import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gin-order/internal/model" "github.com/gin-order/internal/service/order" "github.com/gin-order/internal/pkg/response" ) type OrderController struct { service *order.OrderService } func NewOrderController(service *order.OrderService) *OrderController { return &OrderController{service: service} } func (c *OrderController) GetOrder(ctx *gin.Context) { idStr := ctx.Param("id") id, _ := strconv.ParseUint(idStr, 10, 64) order, err := c.service.GetOrderByID(uint(id)) if err != nil { response.Error(ctx, http.StatusNotFound, "Order not found") return } response.Success(ctx, order) } func (c *OrderController) CreateOrder(ctx *gin.Context) { var req model.Order if err := ctx.ShouldBindJSON(&req); err != nil { response.Error(ctx, http.StatusBadRequest, "Invalid request") return } if err := c.service.CreateOrder(&req); err != nil { response.Error(ctx, http.StatusInternalServerError, "Create failed") return } response.Success(ctx, req) }
라우트 구성
// cmd/server/main.go package main import ( "github.com/gin-gonic/gin" "github.com/gin-order/internal/controller/order" "github.com/gin-order/internal/pkg/database" "github.com/gin-order/internal/repository/order" "github.com/gin-order/internal/service/order" ) func main() { // 데이터베이스 초기화 db := database.NewGORM() // 의존성 주입 orderRepo := order_repo.NewMySQLOrderRepository(db) orderService := order_service.NewOrderService(orderRepo) orderController := order_controller.NewOrderController(orderService) // 라우터 생성 r := gin.Default() // 미들웨어 등록 r.Use(middleware.Logger()) // 라우트 그룹 apiGroup := r.Group("/api") { orderGroup := apiGroup.Group("/orders") { orderGroup.GET("/:id", orderController.GetOrder) orderGroup.POST("", orderController.CreateOrder) } } // 서비스 시작 r.Run(":8080") }
서비스 레이어
// internal/service/order/service.go package order import ( "github.com/gin-order/internal/model" "github.com/gin-order/internal/repository/order" ) type OrderService struct { repo order.OrderRepository } func NewOrderService(repo order.OrderRepository) *OrderService { return &OrderService{repo: repo} } func (s *OrderService) GetOrderByID(id uint) (*model.Order, error) { return s.repo.FindByID(id) } func (s *OrderService) CreateOrder(order *model.Order) error { return s.repo.Create(order) }
데이터 액세스 레이어 (리포지토리)
// internal/repository/order/interface.go package order import "github.com/gin-order/internal/model" type OrderRepository interface { FindByID(id uint) (*model.Order, error) Create(order *model.Order) error FindByStatus(status string) ([]model.Order, error) } // internal/repository/order/mysql.go package order import ( "gorm.io/gorm" "github.com/gin-order/internal/model" ) type MySQLOrderRepository struct { db *gorm.DB } func NewMySQLOrderRepository(db *gorm.DB) OrderRepository { return &MySQLOrderRepository{db: db} } func (r *MySQLOrderRepository) FindByID(id uint) (*model.Order, error) { var order model.Order if err := r.db.First(&order, id).Error; err != nil { return nil, err } return &order, nil } func (r *MySQLOrderRepository) Create(order *model.Order) error { return r.db.Create(order).Error } func (r *MySQLOrderRepository) FindByStatus(status string) ([]model.Order, error) { var orders []model.Order if err := r.db.Where("status = ?", status).Find(&orders).Error; err != nil { return nil, err } return orders, nil }
모델 레이어
// internal/model/order.go package model import "time" type Order struct { OrderID uint `gorm:"primaryKey;column:order_id"` OrderNo string `gorm:"uniqueIndex;column:order_no"` UserID uint `gorm:"index;column:user_id"` OrderName string `gorm:"column:order_name"` Amount float64 `gorm:"type:decimal(10,2);column:amount"` Status string `gorm:"column:status"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` } func (Order) TableName() string { return "orders" }
Go 언어 MVC 모범 사례
인터페이스 분리 원칙
리포지토리 레이어는 인터페이스를 정의하여 여러 데이터베이스 구현을 지원합니다.
// Mock 구현으로 쉽게 전환 type MockOrderRepository struct {} func (m *MockOrderRepository) FindByID(id uint) (*model.Order, error) { return &model.Order{OrderNo: "mock-123"}, nil }
통합 응답 형식
// pkg/response/response.go func Success(c *gin.Context, data interface{}) { c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "success", "data": data, }) }
미들웨어 체인
// 글로벌 미들웨어 r.Use(gin.Logger(), gin.Recovery()) // 라우트 그룹 미들웨어 adminGroup := r.Group("/admin", middleware.AuthJWT())
데이터베이스 마이그레이션
GORM AutoMigrate 사용:
db.AutoMigrate(&model.Order{})
Go 언어 DDD 코드 구현 및 모범 사례
도메인 모델에 집중
DDD는 도메인 모델 구축을 강조하고 집계, 엔터티 및 값 객체를 사용하여 비즈니스 로직을 구성합니다.
Go에서 엔터티 및 값 객체는 일반적으로 struct로 정의됩니다.
// 엔터티 type User struct { ID int Name string }
계층 아키텍처
DDD는 일반적으로 계층 아키텍처를 채택합니다. Go 프로젝트는 다음 구조를 따를 수 있습니다.
- 도메인 레이어: 핵심 비즈니스 로직, 예: 도메인 디렉토리 아래의 엔터티 및 집계.
- 애플리케이션 레이어: 사용 사례 및 비즈니스 프로세스 조정.
- 인프라 레이어: 데이터베이스, 캐싱, 외부 API 등에 대한 어댑터.
- 인터페이스 레이어: HTTP, gRPC 또는 CLI 인터페이스를 제공합니다.
의존성 역전
도메인 레이어는 인프라 레이어에 직접 의존해서는 안 됩니다. 대신 의존성 역전을 위해 인터페이스에 의존합니다.
참고: DDD 아키텍처의 핵심은 의존성 역전(DIP)입니다. 도메인은 가장 안쪽의 핵심으로, 비즈니스 규칙 및 인터페이스 추상화만 정의합니다. 다른 레이어는 구현을 위해 도메인에 의존하지만 도메인은 외부 구현에 의존하지 않습니다. 육각형 아키텍처에서 도메인 레이어는 코어에 위치하는 반면 다른 레이어(예: 애플리케이션, 인프라)는 도메인에서 정의한 인터페이스를 구현하여 데이터베이스 작업, API 호출과 같은 구체적인 기술 세부 사항을 제공하여 도메인과 기술 구현 간의 디커플링을 달성합니다.
// 도메인 레이어: 인터페이스 정의 type UserRepository interface { GetByID(id int) (*User, error) }
// 인프라 레이어: 데이터베이스 구현 type userRepositoryImpl struct { db *sql.DB } func (r *userRepositoryImpl) GetByID(id int) (*User, error) { // 데이터베이스 쿼리 로직 }
집계 관리
집계 루트는 전체 집계의 라이프사이클을 관리합니다.
type Order struct { ID int Items []OrderItem Status string } func (o *Order) AddItem(item OrderItem) { o.Items = append(o.Items, item) }
애플리케이션 서비스
애플리케이션 서비스는 도메인 로직을 캡슐화하여 외부 레이어가 도메인 객체를 직접 조작하는 것을 방지합니다.
type OrderService struct { repo OrderRepository } func (s *OrderService) CreateOrder(userID int, items []OrderItem) (*Order, error) { order := Order{UserID: userID, Items: items, Status: "Pending"} return s.repo.Save(order) }
이벤트 기반
도메인 이벤트는 디커플링에 사용됩니다. Go에서는 채널 또는 Pub/Sub를 통해 이를 구현할 수 있습니다.
type OrderCreatedEvent struct { OrderID int } func publishEvent(event OrderCreatedEvent) { go func() { eventChannel <- event }() }
CQRS (명령 쿼리 책임 분리) 결합
DDD는 CQRS와 결합할 수 있습니다. Go에서는 변경 작업에 명령을 사용하고 데이터 읽기에 쿼리를 사용할 수 있습니다.
type CreateOrderCommand struct { UserID int Items []OrderItem } func (h *OrderHandler) Handle(cmd CreateOrderCommand) (*Order, error) { return h.service.CreateOrder(cmd.UserID, cmd.Items) }
요약: MVC 대 DDD 아키텍처
아키텍처의 핵심 차이점
MVC 아키텍처
-
레이어: 세 개의 레이어—컨트롤러/서비스/DAO
-
책임:
- 컨트롤러는 요청을 처리하고 서비스는 로직을 포함합니다.
- DAO는 데이터베이스를 직접 조작합니다.
-
문제점: 서비스 레이어가 비대해지고 비즈니스 로직이 데이터 작업과 결합됩니다.
DDD 아키텍처
-
레이어: 네 개의 레이어—인터페이스 레이어 / 애플리케이션 레이어 / 도메인 레이어 / 인프라 레이어
-
책임:
- 애플리케이션 레이어는 프로세스를 조정합니다(예: 도메인 서비스 호출).
- 도메인 레이어는 비즈니스 원자적 작업을 캡슐화합니다(예: 주문 생성 규칙).
- 인프라 레이어는 기술 세부 사항을 구현합니다(예: 데이터베이스 액세스).
-
문제점: 도메인 레이어는 기술 구현과 독립적이며 로직은 레이어 구조와 밀접하게 대응합니다.
모듈성과 확장성
MVC:
- 높은 결합도: 명확한 비즈니스 경계가 부족합니다. 모듈 간 호출(예: 주문 서비스가 계정 테이블에 직접 의존)은 코드 유지 관리를 어렵게 만듭니다.
- 열악한 확장성: 새로운 기능을 추가하려면 전역 변경이 필요합니다(예: 위험 제어 규칙을 추가하려면 주문 서비스를 침해해야 함). 쉽게 계단식 문제가 발생합니다.
DDD:
- 제한된 컨텍스트: 모듈은 비즈니스 기능별로 나뉩니다(예: 결제 도메인, 위험 제어 도메인). 이벤트 기반 협업(예: 주문 결제 완료 이벤트)은 디커플링에 사용됩니다.
- 독립적인 진화: 각 도메인 모듈은 독립적으로 업그레이드할 수 있습니다(예: 결제 로직 최적화는 주문 서비스에 영향을 미치지 않음). 시스템 수준 위험을 줄입니다.
적용 가능한 시나리오
- 중소규모 시스템의 경우 MVC를 선호합니다. 단순한 비즈니스(예: 블로그, CMS, 관리 백엔드)는 명확한 비즈니스 규칙과 빈번한 변경 없이 빠른 개발이 필요합니다.
- 복잡한 비즈니스의 경우 DDD를 선호합니다. 규칙 집약적(예: 금융 거래, 공급망), 다중 도메인 협업(예: 전자 상거래 주문 및 재고 연결), 비즈니스 요구 사항의 잦은 변경.
저희 Leapcell은 Go 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 차세대 서버리스 플랫폼으로 웹 호스팅, 비동기 작업 및 Redis를 지원합니다.
다중 언어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무제한 프로젝트 무료 배포
- 사용량에 따라서만 지불하세요. 요청이 없으면 요금도 없습니다.
뛰어난 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불하세요.
- 예시: 25달러로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 손쉬운 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 손쉽게 높은 동시성을 처리하기 위한 자동 확장.
- 제로 운영 오버헤드 — 구축에만 집중하십시오.
문서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ