의존성 주입을 학습하다가 이건 집합 관계로 봐야하지 않나, 라는 의문을 풀기 위해 객체의 관계에 대해 고민해봤습니다. Car와 Engine 클래스로 구성된 코드를 예시로 들었습니다. 물론, 두 모델은 구성 관계가 가장 어울리지만 설명을 위해 다소 억지스럽게 객체의 관계에 대한 해석을 붙여가며 글을 개진해봤습니다.
의존 관계 (Dependency) vs 연관 관계 (Association)
class Engine {
start(): void {
console.log("Engine started");
}
}
class Car {
constructor(private engine: Engine) {}
startCar(): void {
this.engine.start();
}
}
위의 코드는 의존 관계, 연관 관계 중 어떤 관계일까요? 정답은 “둘 다 가능하다”입니다.
일반적으로 어떤 객체를 생성자의 파라미터로 받고 이를 클래스의 필드에 할당하여 사용하는 경우는 집합 관계라고 해석합니다. 그러나 설계 의도에 따라서 생성자에 파라미터로 받은 경우라도, 의존 관계 또는 연관 관계로 해석될 수 있습니다.
의존 관계 (Dependency)
우리에게 익숙한 의존 관계 코드는 다음과 같이 구성됩니다. 메서드에서 다른 객체를 파라미터로 받아 사용하는 것이죠. startCar라는 메서드를 실행할 때 두 객체는 일시적인 관계를 맺습니다. Car가 Engine을 일시적으로 참조한다고 말하며, 메서드가 종료되면 Engine에 대한 참조는 사라집니다. 그리고 두 객체는 서로 무관한 생명주기를 갖습니다.
class Engine {
start(): void {
console.log("Engine started");
}
}
class Car {
startCar(engine: Engine): void {
engine.start();
}
}
그렇다면 맨 처음 공유한 코드는 어떻게 의존 관계로 해석할 수 있다는 걸까요? 이를 이해하기 위해서 리포지토리를 주입받아 사용하는 애플리케이션 서비스의 경우를 살펴보겠습니다.
export class UserApplicationService {
constructor(
private userRepository: UserRepository,
) {}
async getUser(command: UserGetCommand): Promise<UserDataModel> {
const { id } = command;
const user = await this.userRepository.findById(new UserId(id));
if (!user) throw new ResourceNotFoundError();
return new UserDataModel(user);
}
}
이렇게 구현한 코드는 집합 관계가 아닌 의존 관계라고 말합니다. 앞서 공유한 코드와 형태가 똑같은데 말이죠. 헷갈립니다.. 이럴 때는 정의를 살펴보는 게 도움이 됩니다. 의존 관계의 의미를 다시 살펴보겠습니다.
- 객체가 다른 객체를 일시적으로 사용합니다.
UserApplicationService는 userRepository를 주입받아 유저를 찾는 로직에서만 일시적으로 사용합니다. 메서드가 종료되면 userRepository와의 상호작용이 종료됩니다. - 생명 주기가 독립적이고, 해당 객체를 직접 소유하지 않습니다.
UserApplicationService는 userRepository를 소유하거나 관리하지 않습니다. 더불어, userRepository는 이미 외부에서 생성되어 주입되고 있기에 두 객체는 서로 독립적인 생명 주기를 갖습니다. userRepository는 여러 서비스에서 재사용 될 수 있습니다.
이것을 DI(Dependency Injection)를 활용한 설계라고 하며, 목적 자체가 의존 관계를 통해 객체간 결합도를 낮추고 재사용성을 높이는 것에 있습니다. 이제 다시 처음의 코드를 살펴볼까요.
class Engine {
start(): void {
console.log("Engine started");
}
}
class Car {
constructor(private engine: Engine) {}
startCar(): void{
this.engine.start();
}
}
이렇게도 해석할 수 있을 것 같습니다. Engine 객체는 Car 객체에서 일시적으로 참조되며, startCar 메서드를 실행할 때만 일시적으로 사용되고, 다른 자동차에서도 사용될 수 있다고 말이죠. 상식과 벗어난 해석이지만 코드만 보자면 이렇게도 가능하구나, 정도로만 봐주시면 될 것 같습니다.
연관 관계 (Association)
이어서 연관 관계로 해석하는 경우를 생각해보겠습니다. 일반적으로 연관 관계로 해석되는 코드는 참조되는 객체가 다른 객체의 필드로 존재하며, 생성자가 아닌 메서드에서 사용하는 식으로 구성됩니다. Engine은 Car의 필드로 존재하기 때문에 지속적인 참조를 하지만, startCar 메서드를 실행할 때만 상호작용을 합니다. 그리고 addEngine 메서드를 통해 외부에서 Engine 객체를 주입받으므로 별개의 생명 주기를 갖습니다. 처음 코드는 addEngine이란 메서드의 역할을 생성자에서 한다고 이해하면 될 것 같습니다.
class Engine {
start(): void {
console.log("Engine started");
}
}
class Car {
private engine: Engine
addEngine(engine: Engine): void {
this.engine = engine
}
startCar(): void {
this.engine.start();
}
}
관계는 설계 의도로 결정된다
이처럼 동일한 코드더라도 객체들이 어떤 관계로 결정될지는 설계 의도에 따라 정해집니다. 그리고 그 의도는 사용 맥락에 따라 달라집니다. 그렇기 때문에 설계에서 선행되어야 할 것은 본인이 다루는 도메인에서 어떤 개념과 맥락을 표현하고 싶은지를 명확하게 정리하는 것이라고 생각됩니다.
일반적으로 자동차와 엔진에 가장 적합한 관계인 구성 관계를 나타낸 코드를 적어보며 글을 마치겠습니다.
class Engine {
start(): void {
console.log("Engine started");
}
}
class Car {
private engine: Engine;
constructor() {
this.engine = new Engine();
}
startCar() {
this.engine.start();
}
}
참고자료
'Dev > DDD' 카테고리의 다른 글
서클 모델을 추가하여 애플리케이션 구성하기 (0) | 2025.02.08 |
---|---|
[의존 관계 역전 원칙] 의존 관계 방향성 제어를 통해 소프트웨어 유연성을 확보하자 (0) | 2025.01.25 |
유저 CRUD 코드 예시로 도메인 개념 살펴보기: 도메인 서비스, 리포지토리, 애플리케이션 서비스 (0) | 2025.01.22 |
엔티티 - 생애주기를 갖는 객체 (0) | 2025.01.20 |
도메인 주도 설계 - 값 객체 (0) | 2025.01.19 |