1) 의존이란 무엇인가?
(1) ObjectA 가 ObjectB 에 의존하는 관계의 예
public class ObjectA {
private ObjectB objectb;
}
ObjectA 는 ObjectB 를 참조한다. 다시말해 ObjectB 가 없으면 ObjectA 는 정의될 수 없다. 이 때 ObjectA 가 ObjectB 에 의존한다고 한다.
(2) 구현체가 인터페이스에 의존하는 관계의 예
public interface UserRepository {
User find(UserId id);
}
public class UserRepositoryImpl implements UserRepository {
@Override
public User find(UserId Id) {
(...생략...)
}
}
만약 UserRepository 가 정의되어 있지 않다면, 클래스 선언부에 컴파일 에러가 발생하며 UserRepositoryImpl 의 정의가 성립하지 않는다. 따라서 UserRepositoryImpl 는 UserRepository 에 의존한다.
(3) 모듈간 의존하는 관계의 예
public class UserApplicationService {
private fianl UserRepositoryImpl userRepository; // UserRepository 인터페이스가 바람직
public UserApplicationService(UserRepositoryImpl userRepository) {
this.userRepository = userRepository;
}
(...생략...)
}
UserApplicationService 는 UserRepositoryImpl 을 의존한다. 하지만 직접적인 구현체(인프라)를 의존하는 것은 좋지 않다. 특정 데이터베이스와 항상 붙어있으므로, 가볍게 로컬머신에서 돌리거나 테스트를 한다거나 혹은 데이터베이스를 변경해야 할 때, 코드를 다 변경해주어야 한다. 생각보다 이런일은 빈번하다. 따라서 변수의 타입을 인터페이스로 변경하는 것이 바람직하다. 생성자는 인터페이스를 구현한 클래스라면 어떤 구현체라도 인자로 받을 수 있다.
2) 의존 관계 역전 원칙이란?
- 추상화 수준이 높은 모듈이 낮은 모듈에 의존해서는 안되며 두 모듈 모두 추상 타입에 의존해야 한다.
- 추상 타입이 구현의 세부 사항에 의존해서는 안된다. 구현의 세부 사항이 추상 타입에 의존해야 한다.
(1) 추상 타입에 의존하라
- 추상화 수준 : 추상화 수준이 낮을수록 기계와 가까운 구체적인 처리, 높을수록 사람과 가까운 추상적인 처리
UserApplicationService 와 UserRepository 두 클래스 모두 추상 타입인 UserRepository 를 향한 의존관계를 갖는다. 낮은 추상타입에 의존하는 문제가 해결되고, 두 모듈 모두 추상타입을 의존하게 된다. 원래 구현체에 의존하던 것이 추상 타입을 의존하게 되면서 의존 관계가 역전된다.
(2) 주도권을 추상 타입에 둬라
중요도가 높은 도메인 규칙은 항상 추상화 수준이 높은 쪽에 기술된다. 하지만 추상화 수준이 높은 모듈이 낮은 모듈을 의존하게 되면, 나중에 기술적인 변경사항이 생겼을 때 그에 맞춰 도메인 규칙을 변경해야하는 이상한 일이 생긴다. 주도권이 구현체에게 있는 셈이다. 따라서 항상 주도권은 추상화 수준이 높은 추상타입에게 있어야한다.
3) 의존 관계 제어하기
(1)노가다? 패턴
개발 초기에는 데이터베이스 서버가 아직 구축되지 않은 경우가 있다. 그럴 때는 인메모리 방식의 레파지토리를 이용해서 개발을 시작해야한다. 그러다가 추후에 배포를 하고 운영 서버를 이용할 때에는, 해당 db 에 맞는 레파지토리로 변경시켜 주어야한다. 아래와 같은 상황이 벌어질 수 있다.
public class UserApplicationService {
private fianl UserRepository userRepository;
public UserApplicationService() {
// this.userRepository = new InMemoryUserRepository;
this.userRepository = new MySqlUserRepository;
}
(...생략...)
}
마치 추상 타입만을 잘 의존하고 있는 것 같지만, 생성자 내부에서 직접 구현체를 생성함으로써 해당 구현체에 의존하고 있는 셈이다. 또 프로젝트 다른곳에서 빠짐없이 MySqlUserRepository 로 교체해주어야 하는 만만치 않은 작업이 된다.
겨우 교체를 완료했어도 인메모리 레파지토리를 다시 사용해야할 상황이 생길 수도 있다. (버그를 재현해야하는 경우 등)
(2) Service Locator 패턴
ServiceLocator 객체에 의존 해소 대상이 되는 객체를 미리 등록해 둔 다음, 인스턴스가 필요한 곳에서 ServiceLocator 객체에게 인스턴스를 받아 사용하는 패턴이다.
public class UserApplicationService {
private fianl UserRepository userRepository;
public UserApplicationService() {
this.userRepository = ServiceLocator.resolve<UserRepository>();
}
(...생략...)
}
위와 같이 사용할 수 있는데, 어플리케이션이 처음 시작할 때 미리 해당 정보를 등록해두는 방식이다.
// 처음 어플리케이션이 실행되는 시점에 등록
ServiceLocator.register<UserRepository, InMemoryUserRepository>();
하지만 이 패턴은 두가지 단점이 있다.
1. 의존관계를 외부에서 보기 어렵다
: 클래스 정의만 보고 'UserApplicationService 가 바르게 동작하려면 UserRepository 의 구현체를 미리 등록해야한다' 라는 정보를 알 수 없다. 이는 바람직한 코드가 아니다.
// 외부에서 본 클래스 정의
public class UserApplicationService {
public UserApplicationService(); // 생성자 형태로는 의존관계를 알아볼 수 없다.
public void registerUser(Dto dto);
(...생략...)
}
2. 테스트 유지가 어렵다.
: 나중에 서비스 코드에 변화가 생겨서 아래와 같이 다른 의존관계가 추가되었다고 해보자. 하지만 테스트 코드에는 의존관계를 미리 등록해놓지 않아서 테스트가 실패하게 된다. 물론 서비스 코드가 변경되었을 때 테스트가 깨지는 것은 당연하다. 하지만 의존관계를 테스트에도 바로 반영할 수 있게 해주지 못하면, 테스트를 돌려서 깨진 것을 확인하고 수정하는 "테스트 코드 유지 작업" 에 계속 손이 갈 수 밖에 없다.
public class UserApplicationService {
private fianl UserRepository userRepository;
private fianl RoleRepository roleRepository; // 추가
public UserApplicationService() {
this.userRepository = ServiceLocator.resolve<UserRepository>();
this.roleRepository = ServiceLocator.resolve<RoleRepository>(); // 추가
}
(...생략...)
}
(3) IoC Container 패턴
: IoC Container 패턴은 IoC 컨테이너가 의존성을 생성자로 주입해주는 것을 말한다.
public class UserApplicationService {
private fianl UserRepository userRepository;
private fianl RoleRepository roleRepository;
public UserApplicationService(UserRepository userRepository, RoleRepository roleRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
}
(...생략...)
}
아래와 같이 인스턴스를 IoC 컨테이너가 주입해준다.
new UserApplicationService(userRepository, roleRepository); // 주입
보통 해당 구현체를 시작하기 전에 미리 설정정보로 등록을 해놓으면, 서비스가 시작하면서 IoC 컨테이너에 필요한 의존 인스턴스들이 모인다. 그래서 프로그램 실행 중 추상 타입의 구현체를 찾아서 알아서 주입해주는 방식이다. 보통 IoC 컨테이너에 해당 인스턴스가 없으면 생성자로 해당 인스턴스가 전달되지 않기 때문에, 테스트를 실행하지 않아도 프로그램을 실행하면서 오류가 난다.
'책 > 도메인 주도 설계 철저 입문' 카테고리의 다른 글
[도메인 주도 설계 철저 입문] 12. 도메인의 규칙을 지키는 "어그리게이트" (0) | 2022.04.29 |
---|---|
[도메인 주도 설계 철저 입문] 10. 데이터의 무결성 유지하기 (0) | 2022.03.23 |
[도메인 주도 설계 철저 입문] 6. 어플리케이션 서비스 - 도메인 서비스와의 분리! (0) | 2022.03.10 |
[도메인 주도 설계 철저 입문] 5. 레파지토리 - 데이터와 관계된 처리를 분리하자 (0) | 2022.03.08 |
[도메인 주도 설계 철저 입문] 4. 도메인 서비스 - 부자연스러운 도메인 객체의 행동을 맡기자 (0) | 2022.03.08 |