의존은 어떤 객체가 다른 객체를 참조하면서 발생한다.
class A {
private b: B;
}
클래스 A는 B를 참조하고 있다. 때문에 B에 대한 정의가 없으면 A는 성립할 수 없다. 이때 A가 B에 의존한다고 한다. (A -> B)
의존 관계는 인터페이스와 이를 구현하는 클래스 사이에도 발생한다.
interface IUserRepository {
findOneById(id: number): Promise<User | null>;
}
class UserRepository implements IUserRepository {
constructor(...) {}
async findOneById(id: number): Promise<User | null> {
const user = await this.userRepository.findOne({ where: { id }});
return user;
}
}
UserRepository 클래스는 IUserRepository 인터페이스를 구현한다. 만약 IUserRepository 가 정의되지 않았다면 컴파일 에러가 발생하며 UserRepository 의 정의는 성립되지 않는다. 따라서 의존 관계가 성립한다. (UserRepository -> IUserRepository)
지금까지 DDD를 공부하며 살펴본 애플리케이션 서비스에도 의존 관계가 존재한다.
class UserApplicationService {
constructor(private readonly userRepository: UserRepository) {
...
}
...
}
UserApplicationService 는 UserRepository 객체를 속성으로 갖고 있다. 따라서 UserApplicationService 는 UserRepository 에 의존하고 있다.
여기에는 한 가지 문제점이 있다. 애플리케이션 서비스가 UserRepository 에 의존하고 있고, UserRepository 는 특정 퍼시스턴시 기술에 의존한다는 점이다. 유연한 소프트웨어가 되려면 개발이나 테스트 아무 때나 코드를 실행할 수 있어야 하는데, 특정 데이터베이스와 밀접하게 엮여 있다면 이것이 힘들어진다.
이렇게 프로그램을 만들다 보면 자연스럽게 의존 관계가 발생하고 이를 막을 수는 없다. 의존 관계는 객체를 사용하는 것만으로도 발생하기 때문이다. 따라서 의존 자체를 피하는 것보다 이를 잘 제어하는 것이 중요하다. 이제 의존 관계를 제어하는 방법들에 대해서 살펴보자.
의존 관계 역전 원칙(DIP, Dependency Inversion Principle)이란,
이렇게 추상 타입을 이용해 의존 관계를 제어하는 방법을 '의존 관계 역전 원칙'이라고 한다.
이전에 UserApplicationService 에서 발생하던 의존 관계의 문제점을 다음과 같이 개선할 수 있다.
class UserApplicationService {
// 인스턴스 변수의 타입을 인터페이스로 변경
constructor(private readonly userRepository: IUserRepository) {
...
}
...
}
이전과 달리 UserApplicationService 가 구현체인 UserRepository가 아닌 리포지토리 인터페이스인 IUserRepository 를 참조하도록 변경했다.
이렇게 하면 UserApplicationService 는 추상 타입(IUserRepository)을 구현한 어떠한 클래스도 인자로 받을 수 있다. 이제 UserApplicationService 가 더 이상 UserRepository 라는 특정 구현체와 특정 데이터베이스에만 묶여 있지 않게 된 것이다.
예를 들어, 테스트를 위한 인-메모리 리포지토리를 인자로 전달해 단위 테스트를 할 수 있다. 또한 다른 데이터 스토어를 사용하는 리포지토리를 만들어 주요 로직을 수정하지 않고도 데이터 스토어를 교체할 수 있게 되었다.
앞서 살펴본 예제에서 의존 관계는 다음과 같이 변화했다.
UserApplicationService -> UserRepository
이렇게 추상화 수준이 낮은 모듈에 의존하던 것이
UserApplicationService -> IUserRepository
↑
UserRepository
UserApplicationService, UserRepository 모두 추상 타입인 IUserRepository 를 향한 의존 관계를 갖게 되었다. 이렇게 의존 관계가 역전되었고, 추상화 수준이 높은 모듈이 추상화 수준이 낮은 모듈에 의존하는 상황이 해소되었다.
추상 타입이 세부 사항에 의존하면, 낮은 추상화 수준의 모듈의 변경 사항이 높은 추상화 수준의 모듈까지 영향을 미치게 된다.
이는 중요도가 높은 도메인 규칙이 추상화 수준이 높은 쪽에 기술되기에 바람직하지 않다.(예를 들어, 데이터 스토어 변경 때문에 비지니스 로직을 변경)
앞서 본 것 처럼 추상화 수준이 낮은 모듈을 인터페이스와 함께 구현하면, 추상화 수준이 높은 모듈(고차원적인 개념)에 주도권을 넘길 수 있다.
사용 중이던 리포지토리를 테스트를 위해 인메모리 리포지토리로 교체하고 싶다면 아래와 같이 교체할 수 있다.
class UserApplicationService {
private readonly userRepository: IUserRepository;
constructor() {
// this.userRepository = new UserRepository();
this.userRepository = new InMemoryUserRepository();
}
...
}
userRepository 가 추상 타입으로 정의되어 있지만 생성자에서 구상 클래스의 객체를 만들면서 InMemoryUserRepository 에 의존 관계가 발생한다. 또한 개발자는 테스트, 운영 등 상황에 맞게 모든 리포지토리를 교체하도록 코드를 수정해야 하는 번거로운 작업을 해야 한다.
이렇듯 원하는 것을 좀 더 쉽게 선택할 수 있도록 의존 관계를 제어할 필요가 생겼다. IoC 컨테이너 패턴을 활용하면 이 문제를 해결할 수 있다.
DI(의존성 주입)이란 의존 관계에 있는 객체를 직접 생성하는 것이 아니라 외부에서 생성한 후 주입하는 방식이다.
class UserApplicationService {
private readonly userRepository: IUserRepository;
// 생성자 메서드를 통해 의존 관계를 주입
constructor(userRepository: IUserRepository) {
this.userRepository = userRepository;
}
...
}
const userRepository = new InMemoryUserRepository(); // 외부에서 모듈(객체) 생성
const userApplicationService = new UserApplicationService(userRepository);
UserApplicationService 는 이제 생성자에서 직접 의존하는 객체를 생성하지 않는다. 의존하는 객체는 외부에서 생성되며, 이를 생성자 메서드를 통해 주입받고 있다.
때문에 모듈들을 쉽게 교체할 수 있는 구조가 되고, 모듈 간 결합도가 낮아지고 유연성이 높아지며, 테스트하기 좋은 코드가 된다는 장점이 있다.
하지만 의존하는 객체를 생성하는 코드를 여기저기 작성해야 하는 불편함이 있다. 이런 문제를 해결해 주는 것이 IoC Container 패턴이다.
IoC(Inversion of Control, 제어의 역전)이란 개발자가 관리하던 객체의 생명 주기를 외부(프레임워크, 컨테이너 등)에 맡겨 프로그램의 제어권이 역전된 것을 말한다.
즉, IoC Container 패턴이란 개발자가 가지고 있던 객체의 생명 주기에 대한 제어 권한을 이러한 것들을 대신 관리해주는 컨테이너로 넘겨주는 것이다.
NestJs에서 IoC Container 는 Module에 등록된 의존성을 파악하고
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class AppModule {}
생성자를 통해 의존성을 주입한다.
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
...
}
Reference
도메인 주도 설계 철저 입문 (나루세 마사노부 저/심효섭 역)