5 의존과 DI
source: categories/study/oop_programming/opp_programming_5.md
5. 의존과 DI
의존
- 기능 구현을 위해 다른 구성 요소를 사용하는 것
- 의존의 예: 객체 생성, 메서드 호출, 데이터 사용
- 의존은 변경이 전파될 가능성을 의미
- 의존하는 대상이 바뀌면 바뀔 가능성이 높아짐
- 예: 호출하는 메서드의 파라미터가 변경
- 예: 호출하는 메서드가 발생할 수 있는 익셉션 타입이 추가
- 이렇게되면 이 메서드를 사용하는 코드도 바뀌게 된다.
- 의존하는 대상이 바뀌면 바뀔 가능성이 높아짐
순환 의존
- 그래서 순환 의존은 위험하다.
- 순환 의존 -> 변경 연쇄 전파 가능성
- 클래스, 패키지, 모듈 등 모든 수준에서 순환 의존 없도록
- 아래 그림을 보면 A가 B에 의존하고 있고
- B가 C에 의존하고 있고
- 다시 C가 A에 의존하고 있다.
A -> B -> C
^ |
| |
-----------
- 이렇게 의존이 순환해서 발생할 경우, A의 변경이 B에 영향을 주고, 이는 다시 C에 영향을 주고, 이게 다시 A에 영향을 줄 수가 있습니다.
- 순환의존은 이런식으로 변경의 여파가 다른 모듈로 전파될 가능성이 매우 높기 때문에, 클래스, 패키지, 또는 모듈, 이렇게 모든 수준에서 순환 의존이 발생하지 않도록 해야한다.
의존하는 대상이 많다면?
A <--- ---> D
| |
B <------ X -------> E
| |
C <--- ---> F
- 그림에서 중앙에 있는 X는 A~F 6개 모듈을 의존하고 있는데,
- 이 경우 A가 변경되어도 X가 바뀔 수 있고,
- 이 경우 F가 변경되어도 X가 바뀔 수 있고…
- 그래서 의존하는 대상은 적을 수록 좋다.
- 의존하는 대상이 적어야 내가 바뀔 가능성이 줄어드는 것이다.
의존 대상 많을 때 1, 기능 많은 경우
- 한 클래스에서 많은 기능을 제공하는 경우, 의존하는 대상이 많아질 수 있다.
public class UserService {
public void regist(RegReq regReq) {
// ...
}
public void changePw(ChangeReq chgReq) {
// ...
}
public void blockUser(String id, String reason) {
// ...
}
// ...
}
- 각 기능마다 의존하는 대상이 다를 수 있음
- 한 기능 변경이 다른 기능에 영향을 줄 수 있음
changePw
메소드를 변경하는데blockUser
메서드의 내용을 변경해야하는 그런 상황이 발생할 수 있다.- 게다가 한 메소드만 테스트하고 싶어도, 나머지 기능에서 필요로하는 의존 대상까지 함께 초기화해야하는 그런 경우도 발생하게 된다.
- 즉, 테스트하기 힘들어지는 것.
- 그래서 한 클래스가 제공하는 기능이 많으면 이를 기능별로 분리할 것을 고려하는 것이 좋다.
- 기능 별로 분리 고려
- 클래스 갯수는 증가하지만 각 클래스마다 필요로하는 의존이 줄어들게 되고,
- 한 기능을 수정할 때, 다른 기능과 관련된 코드를 수정하는 일이 발생하지 않게 된다.
- 더불어서 개별 기능을 테스트하는 것도 조금 더 수월해지게 된다.
public class UserRegistService {
public void regist(...) {
// ...
}
}
public class ChangePwService {
public void changePw(...) {
// ...
}
}
public class UserBlockService {
public void blockUser(...) {
// ...
}
}
의존 대상 많을 때 2, 묶어보기
- 의존 대상을 줄이는 또 다른 방법은 여러 의존 대상을 단일 기능으로 묶어볼 수 있는지 검토해 보는 것이다.
- 몇 가지 의존 대상을 단일 기능으로 묶어서 생각해보면 의존 대상을 줄일 수 있음
AutoDebitServece
는 의존하고 있는 대상이 4개였다.- 이 의존하고 있는 대상 4개 중에서
MinwonFactory
와MinwonRepository
라는 것을 사용한 이유가 민원 등록 때문이었다고 가정을 해봅시다. - 그러면 이를
AutoDebitMinwonRegister
로 기능을 추상화함으로써AutoDebitServece
가 의존하고 있는 대상을 줄일 수 있게된다.
- 이 의존하고 있는 대상 4개 중에서
의존 대상 객체를 직접 생성하면?
- 의존하는 객체를 직접 생성할 수도 있지만, 이 경우에 생성할 클래스가 바뀌면 의존하는 코드도 바뀐다는걸 우라는 앞에서 추상화 시간에 설명드린적이 있다.
- 생성 클래스가 바뀌면 의존하는 코드도 바뀜
- 추상화에서 언급
- 의존 대상 객체를 직접 생성하지 않는 방법
- 팩토리, 빌더
- 의존 주입(Dependency Injection, DI)
- 서비스 로케이터(Service Locator)
의존 주입(Dependency Injection)
- 외부에서 의존 객체를 주입
- 생성자나 메서드를 이용해서 주입
- 이것이 DI의 전부이다.
public class ScheduleService {
private UserRepository repository;
private Calculator cal;
public ScheduleService(UserRepository repository) {
this.repository = repository;
}
public void setCalculator(Calculator cal) {
this.cal = cal;
}
}
// 초기화 코드
UserRepository userRepo = new DbUserRepository();
Calculator cal = new Calculator();
ScheduleService schSvc = new ScheduleService(userRepo);
schSvc.setCalculator(cal);
조립기(Assembler)
- 프로그램을 시작하는 메인 메소드에서 의존 객체를 생성하고 주입을 할 수도 있지만, 보통은 조립기를 사용해서 객체를 생성하고, 의존 주입을 처리합니다.
- 조립기가 객체 생성, 의존 주입을 처리
- 예: 스프링 프레임워크
@Configuration
public class Config {
@Bean
public ScheduleService scheduleSvc() {
ScheduleService svc = new ScheduleService(repo());
svc.setCalculator(expCal());
return svc;
}
@Bean
public UserRepository repo() {...}
@Bean
public Calculator expCal() {...}
}
// 초기화 (스프링에서 제공하는 조립기)
ctx = new AnnotationConfigApplicationContext(Config.class);
// 사용할 객체 구함
ScheduleService svc = ctx.getBean(ScheduleService.class);
// 사용
svc.getSchedule(...);
DI 장점 1
- 상위 타입을 사용할 경우 의존 대상이 바뀌면 조립기(설정)만 변경하면 됨
DI 장점 2
- 의존하는 객체 없이 대역 객체를 사용해서 테스트 가능
DI를 습관처럼 사용하기
- 프로그램 구성 요소를 조립하는 방법으로 가장 많이 사용되는 방법이 DI이다.
- 의존 객체는 주입받도록 코드 작성하는 습관