본문 바로가기

백엔드 잡학사전

[API 개발] "그래서 DI가 뭔데?" - Go와 DI, 그리고 '조립 공장'의 발견

개발을 하다 보면 코드가 뒤엉키는 순간이 옵니다. 내 라우터가 데이터베이스를 "알고" 있고, 내 서비스가 특정 로거 구현체에 "의존"합니다. 코드를 수정하면 10군데에서 에러가 터지고, 테스트 코드를 짜는 건 불가능에 가깝습니다.

 

이 문제의 핵심에는 "강한 결합도(Tightly Coupled)"가 있습니다. 그리고 이 문제를 푸는 열쇠가 바로 의존성 주입(Dependency Injection, DI)입니다. "DI"라는 용어는 무섭게 들리지만, 그 본질은 아주 간단합니다.

 

 

DI, 넌 누구냐? (OOP? DDD?)

가장 먼저 헷갈리는 질문. DI는 DDD에 대한 걸까요, OOP에 대한 걸까요?

정답: 둘 다에게 필수적인 "도구(Pattern)"입니다.

  • OOP (객체 지향 프로그래밍): DI는 OOP의 SOLID 원칙 중 하나인 "의존관계 역전 원칙(Dependency Inversion Principle)"을 구현하는 핵심 기술입니다. "구체적인 구현체에 의존하지 말고, 추상적인 인터페이스에 의존하라"는 원칙을 DI를 통해 실현합니다.
  • DDD (도메인 주도 설계): DDD는 계층형 아키텍처(Layered Architecture)를 요구합니다. domain 계층은 순수해야 하고, infra나 app 같은 외부 계층이 domain에 의존해야 하죠. DI는 바로 이 아키텍처에서 각 계층의 "부품"을 조립하는 접착제 역할을 합니다.

DI는 OOP나 DDD 자체가 아니라, 이 두 가지를 제대로 구현하기 위해 필요한 소프트웨어 디자인 패턴입니다.

 

 

DI가 없던 시절 (안티 패턴)

우리가 피하려고 했던 "안티 패턴"을 모두가 이해하기 쉬운 쇼핑몰 OrderHandler (주문 처리 핸들러) 예시로 살펴보겠습니다.

OrderHandler가 OrderRepository (주문 DB 처리기)를 필요로 할 때, 핸들러 함수 내부에서 모든 것을 직접 생성하는 방식입니다.

 

// router/order_router.go
package router

import (
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
    "gorm.io/driver/postgres"
    // 😱 핸들러(Router)가 GORM과 Postgres(Infra)를 직접 알아버렸습니다!
)

// --- Infra 계층에 있어야 할 코드 (예시를 위해 여기에 둠) ---
type OrderRepository struct { db *gorm.DB }
func (r *OrderRepository) GetOrderByID(id string) (/* ... */) { /* ... */ }
func NewOrderRepository(db *gorm.DB) *OrderRepository { /* ... */ }

// --- Router 계층 ---
// GetOrderHandler는 API 요청을 처리하는 핸들러 함수
func GetOrderHandler(c *gin.Context) {
    
    // 😫 최악: 요청이 올 때마다 DB 연결을 새로 생성!
    dsn := "host=localhost user=... dbname=orders"
    db, _ := gorm.Open(postgres.Open(dsn), &gorm.Config{})

    // 😫 안됨: 핸들러가 'NewOrderRepository'라는 "구체적인 구현체"를
    //         직접 생성하고 의존함 (강한 결합)
    orderRepo := NewOrderRepository(db)

    // 이제서야 비즈니스 로직 실행
    orderId := c.Param("id")
    order, _ := orderRepo.GetOrderByID(orderId)
    c.JSON(200, order)
}

 

이 코드의 치명적인 문제점:

  1. 강한 결합도 (Tightly Coupled):
    • GetOrderHandler (Router 계층)가 gorm.Open, postgres.Open, NewOrderRepository 같은 infra 계층의 구체적인 구현 코드를 "알고 있습니다."
    • 만약 OrderRepository의 생성자(NewOrderRepository)가 바뀌거나, DB를 Postgres에서 Sqlite로 바꾸면? GetOrderHandler 함수 내부 코드를 전부 수정해야 합니다.
  2. 테스트 불가능 (Untestable):
    • GetOrderHandler 함수를 테스트하려면 무조건 실제 PostgreSQL DB를 띄워야만 합니다.
    • NewOrderRepository를 가짜(Mock) 리포지토리로 바꿔치기할 방법이 전혀 없습니다.
  3. 극심한 비효율 (Inefficient):
    • HTTP 요청이 한 번 올 때마다 gorm.Open을 호출해서 새로운 DB 커넥션 풀을 만들고 있습니다. 사용자가 100명만 몰려도 DB는 바로 다운될 겁니다.

 

DI 적용: "만들지 말고, 주입받아라"

DI의 핵심은 간단합니다. "내부에서 직접 만들지(New) 말고, 외부(생성자 파라미터)에서 받아서(주입) 써라."

 

비유: GetOrderHandler(요리사)가 OrderRepository(칼)가 필요할 때, 직접 대장간(infra)에 가서 칼을 만들어 오는 게 (나쁜 결합) 아니라, main.go(레스토랑 오너)가 미리 준비한 칼(OrderRepository 객체)을 요리사에게 "건네주는(주입하는)" 것입니다.

 

// --- 1. Router 계층 (api/order_router.go) ---
package router

import (
    "github.com/gin-gonic/gin"
    "my-shopping-mall/app" // 👈 app 계층만 "앎"
)

// 라우터 구조체는 'app.OrderService' 인터페이스(또는 객체)만 알면 됨
type OrderRouter struct {
    orderService *app.OrderService 
}

// ⭐️ 생성자(New)가 필요한 부품(OrderService)을 "파라미터로 주입"받음
func NewOrderRouter(svc *app.OrderService) *OrderRouter {
    return &OrderRouter{
       orderService: svc,
    }
}

// 핸들러는 "주입받은" 객체를 그냥 "사용"함
func (r *OrderRouter) GetOrderHandler(c *gin.Context) {
    orderId := c.Param("id")
    
    // ⭐️ 주입받은 서비스 사용! (직접 생성 X)
    order, err := r.orderService.Get(orderId) 
    // ... (에러 처리 및 응답) ...
}

 

 

그래서 DI를 쓰면 뭐가 좋은데?

  1. 느슨한 결합 (Loose Coupling): router는 app만 알고, app은 domain 인터페이스만 압니다. infra가 Sqlite에서 PostgreSQL로 바뀌어도, main.go의 New... 함수 한 줄만 바꾸면 끝입니다. router와 app 코드는 수정할 필요가 없습니다.
  2. 테스트 용이성 (Testability): 이게 핵심입니다. app.NewOrderService는 infra.OrderRepository 인터페이스를 받습니다.
    • 실제 앱: infra.NewOrderRepository(db)를 주입합니다.
    • 테스트 코드: infra.NewInMemoryOrderRepository(nil) (가짜 DB)를 주입할 수 있습니다! 이제 DB 연결 없이도 app 계층의 로직을 빠르고 완벽하게 테스트할 수 있습니다.
  3. 명확한 구조: 모든 "조립" 로직은 main.go 한곳에 모여있고, 다른 패키지(app, router 등)는 자신의 책임에만 집중할 수 있습니다.

 

DI는 복잡한 개념이 아닙니다. "부품을 밖에서 만들어 건네주는" 단순한 패턴이지만, 이 패턴 하나가 우리가 만드는 애플리케이션을 견고하고, 유연하며, 테스트하기 쉬운 코드로 만들어주는 가장 강력한 무기입니다.

'백엔드 잡학사전' 카테고리의 다른 글

[API 개발] 트랜잭션 걸기  (0) 2025.11.06
"Domain Driven Development" 에 대하여  (0) 2025.10.16
SQL BASICS 뽀개기  (5) 2024.10.12
[스프링 리뷰]  (1) 2024.09.14
[스프링 핵심] 빈 스코프 & 프로토타입 빈  (4) 2024.08.20