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개 중에서 MinwonFactoryMinwonRepository라는 것을 사용한 이유가 민원 등록 때문이었다고 가정을 해봅시다.
    • 그러면 이를 AutoDebitMinwonRegister로 기능을 추상화함으로써 AutoDebitServece가 의존하고 있는 대상을 줄일 수 있게된다.

의존 대상 객체를 직접 생성하면?

  • 의존하는 객체를 직접 생성할 수도 있지만, 이 경우에 생성할 클래스가 바뀌면 의존하는 코드도 바뀐다는걸 우라는 앞에서 추상화 시간에 설명드린적이 있다.

  • 생성 클래스가 바뀌면 의존하는 코드도 바뀜
    • 추상화에서 언급
  • 의존 대상 객체를 직접 생성하지 않는 방법
    • 팩토리, 빌더
    • 의존 주입(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이다.

  • 의존 객체는 주입받도록 코드 작성하는 습관