본문 바로가기
Dev/DDD

[의존 관계 역전 원칙] 의존 관계 방향성 제어를 통해 소프트웨어 유연성을 확보하자

by 싯벨트 2025. 1. 25.
728x90

도메인 로직을 기술적 요소에서 분리소프트웨어를 유연하게 만드는 의존 관계 제어에 대해 알아보겠습니다.

의존이란 무엇인가

의존은 어떤 객체가 다른 객체를 참조하면서 발생합니다. 아래 코드를 살펴보겠습니다.

class ObjectA {
  private objectB: ObjectB
}

ObjectA는 ObjectB를 속성으로 가지면서 참조합니다. 즉, ObjectB에 대한 정의가 선행되지 않으면 ObjectA의 정의는 성립할 수 없습니다. 이럴 때, ObjectA는 ObjectB에 의존한다고 말합니다. 의존 관계는 다이어그램으로 나타낼 때 아래처럼 의존하는 객체에서 의존의 대상이 되는 객체 쪽으로 향하는 화살표로 표현합니다.

의존 관계 클래스 다이어그램

 

class Concrete implements Interface { }

추상 타입을 구현하는 일반화 관계도 한 객체가 다른 객체를 의존하는 관계입니다. 예를 들면 리포지토리 인터페이스와 리포지토리 구상 클래스 같은 관계가 있습니다. 이때 구상 클래스가 인터페이스에 의존한다고 말하며, 다이어그램으로는 속이 빈 화살표로 표현합니다.

일반화 관계 클래스 다이어그램

그럼 리포지토리 모듈을 사용하는 애플리케이션 서비스 코드에서의 의존을 살펴보겠습니다. UserTORepository는 TypeORM을 사용하는 리포지터리 객체입니다.

export class UserApplicationService {
    constructor(
        private userRepository: UserTORepository,
    ) {}
    //.. 생략 ..
}

UserApplicationService는 UserTORepository 객체를 속성으로 갖도록 정의되었기 때문에 UserTORepository에 의존하는 상태입니다. 그런데 이렇게 되면 UserApplicationService는 TypeORM이라는 특정 기술에 의존하게 되며 코드의 실행을 어렵게 하고 유연성을 해칩니다. 코드 실행을 위해 디비를 갖추고 필요한 테이블을 만들어여 하며, 테스트 코드 실행을 위해 인메모리 디비를 연결하는 경우나, 팀 내 협의를 통해 다른 ORM을 적용할 경우 코드의 수정이 불가피해집니다.

 

그래서 리포지토리의 구상 클래스가 아닌 추상 타입을 참조하는 리포지토리 패턴으로 이러한 문제를 해결합니다. 아래 코드처럼 UserApplicationService가 구상 클래스 대신 UserRepository 인터페이스를 참조하게 하는 것입니다. 이렇게 하면 UserApplicationService는 UserRepository을 구현한 구상 클래스라면 무엇이든 인자로 받을 수 있습니다. 의존 관계의 방향을 제어해서 비즈니스 로직이 특정 구현에 의존하는 것을 해결하는 것이죠.

export class UserApplicationService {
    constructor(
        private userRepository: UserRepository,
    ) {}
    //.. 생략 ..
}

리포지토리 패턴 클래스 다이어그램

의존 관계 역전 원칙이란 무엇인가

의존 관계 역전 원칙(Dependency Inversion Principle)은 다음과 같이 정의합니다.

  1. 추상화 수준이 높은 모듈이 낮은 모듈에 의존해서는 안 되며 두 모듈 모두 추상 타입에 의존해야 한다.
  2. 추상 타입이 구현의 세부 사항에 의존해서는 안 된다. 구현의 세부 사항이 추상 타입에 의존해야 한다.

추상화 수준이란 입출력으로부터의 거리를 뜻합니다. 추상화 수준이 높을수록 사람과 더 가깝다는 것을 의미합니다. 데이터스토어를 다루는 UserTORepository는 처리 내용이 기계와 더 가깝기 때문에 추상화 수준이 낮고, UserApplicationService은 사람과 더 가깝기 때문에 추상화 수준이 높은 모듈입니다. 그렇기 때문에 UserApplicationService는 낮은 추상화 수준의 모듈인 UserTORepository에 의존하면 안 됩니다.

앞선 코드를 보면, 추상 타입인 UserRepository는 자신을 사용하는 클라이언트인 UserApplicationService가 요구하는 정의입니다. 그리고 UserTORepository는 클라이언트의 요구를 준수해서 구현한 구상 클래스입니다. 즉, 고수준 모듈인 UserApplicationService가 주도권을 가지고 저수준 모듈인 UserTORepository이 구현되며 의존 관계 역전 원칙이 준수됩니다.

의존 관계 제어하기

typescript에서 의존 관계 역전 원칙을 구현하는 데는 tsyringe 라이브러리를 사용하면 좋습니다. 이를 구현하는 패턴에는 Service Locator 패턴IoC Container(DI container) 패턴이 있으며, IoC Container 패턴이 권장됩니다.

Service Locator 패턴

Service Locator 패턴은 의존성을 직접 컨테이너에서 가져와 사용하는 방식입니다. 인스턴스가 필요한 곳은 컨테이너에서 직접 인스턴스를 받아 사용합니다. 하지만 이 패턴은 의존 관계를 외부에서 확인하기 어렵다는 단점이 있습니다. 아래 코드에서 확인할 수 있듯이, UserApplicationService가 바르게 동작하려면 UserRepository 객체를 미리 등록해야 한다는 정보를 알 수가 없습니다. 그래서 새로운 의존 관계가 추가되는 등의 변경이 발생했을 때, 테스트 코드처럼 UserApplicationService을 사용하는 곳에서는 그 사실을 코드를 보기 전까지 알 수가 없습니다.

import "reflect-metadata";
import { container, injectable } from "tsyringe";

@injectable()
class UserRepository { }

class UserApplicationService {
  private userRepository: UserRepository;

  constructor() {
    this.userRepository = container.resolve(UserRepository);
  }
}

IoC Container(DI container) 패턴

IoC Container 패턴은 의존성을 컨테이너가 주입하는 방식입니다. 생성자 메서드를 사용하여 의존 관계를 주입하기 때문에 생성자 주입이라고도 합니다. 이렇게 구성을 하면 해당 모듈을 사용하는 곳에서 인스턴스를 생성할 때 무엇에 의존하는지 알 수 있게 됩니다. 의존 관계에 변경이 생겨도 컴파일 에러가 발생하며 변경 사항을 파악할 수 있습니다.

import "reflect-metadata";
import { injectable, singleton, container } from "tsyringe";

@injectable()
class UserRepository { }

@singleton()
class UserApplicationService {
  constructor(private userRepository: UserRepository) {}
}

참고자료

도메인 주도 설계 철저 입문, 위키북스