DDD : 7. 의존관계제어
- 카테고리 없음
- 2023. 11. 1.
7.1 특정 구현체에 의존했을 때 단점
- 프로그램에서 객체 간의 의존을 막을 수 있는 방법은 없다. 그렇지만 객체 간의 의존을 약화시키는 방법은 있다.
- 객체끼리 강하게 결합하면 한 객체가 변화할 때 마다 다른 객체가 변화해야하는 것을 의미한다.
의존 관계 제어를 통해 객체 간의 결합, 특히 비즈니스 로직과 기술적 요소를 분리해서 어플리케이션을 유연하게 만들 수 있다.
7.2 객체 간의 의존이란 무엇인가?
한 객체가 다른 객체를 알고 있는 것만으로도 '의존'이 발생한다고 볼 수 있다.
- Solution 클래스는 Comparator, Stream 클래스에 의존함.
- Solution 클래스는 MyClass에 의존함.
import java.util.Comparator;
import java.util.stream.Stream;
class Solution {
private MyClass myClass;
...
}
이처럼 다른 객체를 알고 있는 것만으로도 '의존 관계'가 발생한다고 볼 수 있다. 프로그래밍에서는 2가지 의존 관계가 발생한다. 아래 그림을 참고하자.
- 서비스 계층이 인터페이스를 참조한다. (알고 있음)
- 리포지토리 구현체가 리포지토리 인터페이스를 알고 있음. (implemnts)
서비스 A가 Repository 구현체를 바로 참조하는 것은 지양해야한다. 이것은 비즈니스 로직을 나타내는 계층이 특정 DB 접근 기술을 다루는 클래스와 강하게 결합했기 때문이다. 이 부분을 해결해주는 것이 '인터페이스다'.
7.3 의존 관계 역전 원칙이란 무엇인가?
의존 관계 역전 원칙은 다음을 의미한다.
- 고수준 추상화 타입이 저수준 추상화 타입에 의존하면 안됨.
- 추상 타입이 구현 세부사항에 의존하면 안됨. 구현 세부사항이 추상 타입에 의존해야함.
여기서 '추상화'라는 것은 '사람에 가까울수록' 추상화 되었다고 하고, '컴퓨터에 가까울수록' 저추상화 되었다고 볼 수 있다.
이런 관점에서 이 그림을 다시 보자.
- 1. 추상화 정도
- 서비스는 고수준 추상화임. (사용자에 가까움)
- 리포지토리는 저수준 추상화임. (DB에 접근함)
- 2. ServiceA → Repository MySQL 구현체 참조한다면?
- 고수준 추상화 타입인 ServiceA가 구현체를 직접 참조하므로 의존 관계 역전 원칙을 모두 위배함.
Repository 인터페이스는 '구현 세부사항이 추상 타입에 의존하도록 한다'를 실현시켜준다. Repository 인터페이스는 Service가 사용할 메서드를 선언한 곳이다. 그리고 Repository 클래스는 Service가 사용할 메서드를 Service가 원하는대로 구현한다. 즉, 저수준 Repository 구현체가 고수준 Service에 의존하도록 바뀐 것이다.
7.4 의존 관계 제어하기
DIP (의존 관계 역전 원칙)을 통해 모두 추상화 타입에 의존하도록 해서 객체 간의 결합을 줄이는 방법을 알아보았다. 이후에는 의존관계를 제어하는 수단을 사용해 소프트웨어에 유연성을 더할 수 있다.
의존관계를 제어하는 수단은 두 가지 스텝으로 처리할 수 있다.
1. 의존관계 주입 (Dependency Injection) → 객체가 의존할 구현체를 외부에서 정함.
2. IoC 컨테이너 (Inversion of Control Container) → 의존관계 주입을 편하게 하도록 도와줌.
// Dependency Injection
public class ServiceA{
private final RepositoryA repository;
public ServiceA(RepositoryA repository){
this.repository = repository;
}
...
}
Dependency Injection은 객체가 생성되는 시점(생성자 주입인 경우)에 어떤 객체를 사용할지를 정하게 된다. 즉, Service 객체가 생성될 때 의존성을 주입받아 ServiceA가 생성되는 것이다. 이런 구성이라면 사용자는 테스트 코드에서 언제든지 MySQL DB를 사용하던 것을 InMemoryDB로 바꿀 수 있다.
// 의존성 주입이나 컨테이너 사용하지 않음.
public ServiceA helloService(){
RepositoryA repository = new RepositoryA();
ServiceA service = new ServiceA(repository);
return service
}
public ServiceA helloService2(){
RepositoryA repository = new RepositoryA();
ServiceA service = new ServiceA(repository);
return service
}
그런데 Dependency Injection만 사용했을 때의 단점은 주입하려는 의존성을 바꾸고 싶을 때 생성자 주입을 하는 모든 코드를 찾아서 변경해야 한다는 것이다. 즉, 변경지점이 넓어진다. 이 부분을 개선하기 위해 IoC Container(DI Container)를 사용한다. 요지는 DI Container에 미리 필요한 객체들을 생성해두고 Dependency Injection 하는 시점에 필요한 객체를 가져다 쓸 수 있도록 하는 것이다.
결과적으로 DI를 이용하면 '소프트웨어를 좀 더 유연하게 만들 수 있고', DI 자체만으로는 해결할 수 없는 DI 변경지점 부분을 'IoC Container'를 이용해서 해결할 수 있게 된다.
정리
- 의존관계는 다음의 경우 발생함.
- 특정 타입이 다른 타입을 아는 경우.
- 특정 타입이 인터페이스를 구현하는 경우
- 객체끼리 의존하지만 약결합을 위해 DIP(의존 관계 역전 원칙)을 지켜라.
- 고수준 추상화 타입이 저수준 추상화 타입에 의존하면 안됨. 모두 고수준 추상화 타입에 의존하도록 해야함.
- 고수준 추상화 타입이 구현에 의존하지 말고, 구현이 고수준 추상화 타입에 의존해야 함. (인터페이스를 통해 할 수 있음.)
- DI를 이용하면 소프트웨어를 좀 더 유연하게 할 수 있음.
- DI는 객체가 스스로 사용할 객체를 선택하지 않고, 외부에서 주입해주는 객체를 사용함.
- DI 자체만 사용했을 때는 변경지점이 넓을 수 있음. (외부에서 주입하는 객체를 바꾸고 싶을 때)
- 이 부분을 해결하기 위해 IoC 컨테이너에 필요한 객체를 등록한 후 가져다 쓴다)