jisung-02notes
← Archive

의존성 역전

의존성 역전 원칙(DIP)을 언어별 배치까지 정리

jisung-02

의존성 역전 (Dependency Inversion Principle, DIP)

상위 모듈과 하위 모듈이 서로 직접 의존하지 않고, 둘 다 “추상(인터페이스)”에 의존하도록 만드는 설계 원칙

0. 핵심 모델 (언어 공통)

[ Consumer ] ---depends on---> [ Interface ] <---implements--- [ Producer ]

-> 아래는 consumer와 producer의 배치까지 고려한 언어별 예시다. • Consumer (상위 모듈): Service / UseCase • Producer (하위 모듈): DB, API client, external infra • Interface 위치: Consumer 쪽

Go

// consumer package

type UserRepository interface {
    FindByID(id int) (User, error)
}

type UserService struct {
    repo UserRepository
}
// producer package

type MySQLRepo struct{}

func (r *MySQLRepo) FindByID(id int) (User, error) {
    return User{}, nil
}
  • 서비스 레이어가 Consumer, Repository 레이어가 Producer다
  • Go는 암시적 인터페이스(implicit interface)를 지원한다
    • go의 duck typing
  • interface는 consumer에 위치하고, producer의 구현체는 이 interface를 명시적으로 구현하지 않고 따르면 된다
    • 결합 방향이 자연스럽게 역전된다
[[ Consumer ] ---depends on---> [ Interface ]] <---implements--- [ Producer ]
  • consumer의 생성 시점에 구현된 producer가 주입된다
var _ UserRepository = (*MySQLRepo)(nil) // optional compile-time check
  • 이 형태로 compile time에 명시적인 구현 체크가 가능하다

Java

일반적인 Spring 구조에서의 DIP

com.example.app
 ├── controller
 ├── service
 ├── repository
 ├── entity
controller → service → repository(interface) → repository impl
@Service
public class UserService {

    private final UserRepository repo;

    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}
@Repository
public interface UserRepository extends JpaRepository<User, Long> {}
  • 이때 UserRepository의 구현체는 Spring이 JpaRepository를 보고 생성해준다
  • 일반적인 Java, Spring의 interface는 consumer가 아닌 producer에 위치한다

Port and Adaptor

com.example.app

 ├── user
 │   ├── application         ← consumer (service / usecase)
 │   │   ├── UserService.java
 │   │   └── port
 │   │       └── UserRepository.java   ← 인터페이스 (여기에 둠)
 │   │
 │   ├── domain
 │   │   └── User.java
 │   │
 │   ├── infrastructure      ← producer
 │   │   ├── persistence
 │   │   │   └── JpaUserRepository.java
 │   │   └── config
 │   │
 │   └── presentation
 │       └── UserController.java
  • 이때는 인터페이스가 consumer 패키지에 위치한다
  • producer는 이를 구현한다
// user/application/port/UserRepository.java

public interface UserRepository {
    User findById(Long id);
}
// user/infrastructure/persistence/JpaUserRepository.java

@Repository
public class JpaUserRepository implements UserRepository {
    @Override
    public User findById(Long id) {
        return new User();
    }
}
// user/application/UserService.java

@Service
public class UserService {

    private final UserRepository repo;

    public UserService(UserRepository repo) {
        this.repo = repo;
    }
}
  • 이 경우 consumer 패키지에서 인터페이스를 정의한다

-> producer가 interface를 정의하더라도 구현체를 숨기는 DIP는 만족한다 -> 그러나 interface는 계약이고, 이 계약이 누가 원하는 형태인지가 중요하다 - 시스템은 서비스(유스케이스)가 하고 싶은 일을 수행하는 형태여야 한다 - 그러므로 계약의 설정도 서비스(유스케이스, consumer)가 수행해야 한다

전체적인 구조

일반적 스프링

Controller → Service → Repository(interface) → JPA/DB
                        ↓
                      Entity
Presentation → Application → Infrastructure
                        ↓
                      Domain

Port & Adaptor 스프링

Controller → Application(Service)
                   ↓
                Domain
                   ↑
          Infrastructure (Repository impl)
                   ↓
          Application Port(interface)

Python

  • DIP 방식이 불분명하다

Protocol

from typing import Protocol

class UserRepository(Protocol):
    def find_by_id(self, id: int) -> "User": ...
class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo
class MySQLRepo:
    def find_by_id(self, id: int):
        return User()