Spring DI

스프링의 3대 프로그래밍 모델 중 IoC/DI에 대해 정리하는 글입니다.

DI (Dependency Injection)

스프링의 IoC 컨테이너는 스프링 Bean의 생성과 소멸 객체의 생명주기를 대신 관리해준다. 또한 스프링 Bean을 싱글 톤 방식으로 하나의 인스턴스만 생성을 하고 Bean을 재활용 할 수 있는 DI(Dependency Injection) 기능을 제공해준다. 이로써 모듈 간의 결합도가 낮아지고 유연성이 높아지게된다.

그렇다면 DI를 하는 것이 어떻게 결합도를 낮출 수 있을까? 즉, 왜 DI가 필요할까?

실제 요구사항에 적용하여 생각해보자

요구사항으로 클라이언트가 무언가를 주문하는 시스템을 만들고자 한다. 그래서 결과적으로 클라이언트가 주문을 요청하고 주문한 결과를 받는 서비스를 만들어야 한다고 생각해보자.

그렇다면 역할과 구현을 분리하여 자유롭게 구현 객체를 조립할 수 있게 설계를 아래와 같이 했다고 해보자.

역할과 구현을 분리한 설계

위에 그림을 토대로 클래스 다이어그램을 만들면 아래와 같이 만들 수 있을 것이다.

주문 서비스 클래스 다이어그램

이제 OrderServiceImpl의 코드를 작성해보자.

public interface OrderService {
  int discount(Member member, int price)
}

public class OrderSErviceImpl implements OrderService {
	private final MemberRepository memberRepository = new MemoryMemberRepository();
	private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
  
  @Override
  public Order createOrder(Long memberId, String itemName, int itemPrice) {
    Member member = memberRepository.findById(memberId);
    int discountPrice = discountPolicy.discount(member, itemPrice);
    
    return new Order(memberId, itemName, itemPrice, discountPrice);
  }
}

위와 같이 직접 MemberRepository와 DiscountPolicy를 new를 통해 원하는 구현체로 조립을 해줬다.

이렇게 조립을 해준 순간 MemberRepository 역할에만 의존해야하는 OrderService는 실제 구현체인 MemoryMemberRepository에도 의존하게 되었다. 결국 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 생기게 되었고 DIP를 위반하게 되었다.

위의 문제점을 떠안고 코드를 작성하게되면 매번 원하는 할인 정책과 회원 저장소의 구현체를 바꾸어 껴줘야하고 코드를 건들게된다. 만약 DiscountPolicy 기능을 확장한 TimeRateDiscountPolicy를 만들어서 변경한다고 해보자.DiscountPolicy 구현체를 변경하기위해 OrderServiceImpl을 아래와 같이 변경해야 할 것이다.

public interface OrderService {
  int discount(Member member, int price)
}

public class OrderSErviceImpl implements OrderService {
	private final MemberRepository memberRepository = new MemoryMemberRepository();

  // 코드를 변경하게 되었다 !!
	private final DiscountPolicy discountPolicy = new TimeRateDiscountPolicy();
  
  ...
}

이는 결국 OCP(개방 폐쇄 원칙)을 어기게 된다.

그렇다면 어떻게 해결해야할까?

바로 관심사를 분리하면 된다. 애플리케이션을 하나의 공연이라고 생각해보자. 또한, 각각의 인터페이스를 배역(배우 역할)이라 생각하자. 그렇다면 실제 배역 맞는 배우를 선택하는 것은 누가 하는가?

누가 어떤 역할을 할지는 배역이 정하는 것이 아니다. 로미오 역할을 하는 레오나르도 디카프리오가 줄리엣의 역할을 누가 할지 정하는 것이 아니지 않는가? 하지만 위에 코드는 로미오 역할(인터페이스)을 하는 레오나르도 디카프리오(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다.

주문 서비스의 할인 정책과 회원 저장소를 선택할 수 있는 설정파일로 관심사를 분리해보자. 즉, 공연 기획자를 만들고 배우와 기획자의 책임을 확실히 분리해보자.

public class AppConfig {
  public MemberRepository memberRepository() {
    return new MemoryMemberRepository();
  }
  
  public DiscountPolicy discountPolicy() {
    return new FixDiscountPolicy();
  }
  
  public OrderService orderService(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    return new OrderServiceImpl(memberRepository, discountPolicy);
  }
}

public class OrderServiceImpl implements OrderService {
  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;
 
  public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
  discountPolicy) {
          this.memberRepository = memberRepository;
          this.discountPolicy = discountPolicy;
      }
}

AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다. 또한 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)해준다. 이것이 생성자 주입이다.

AppConfig를 통해 DI

OrderServiceImpl 입장에서 보면 외부에서 MemberRepositoryFixDiscountPolicy를 생성하여 인자로 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라고 한다.

결과적으로 DI를 함으로써 OCP, DIP 원칙을 지키며 객체지향 프로그래밍을 할 수 있는 것이다.

그래서 스프링 IoC 컨테이너가 xml 또는 컴포넌트 스캔을 하여 Bean을 생성하고 Bean의 의존성에 따라 DI를 해주도록 설계가 된 것이다.

정리

  • Spring IoC 컨테이너가 DI를 해준다.
  • 역할과 구현을 분리하여 코드를 작성하다보면 구현체를 넣는 과정에서 OCP, DIP 위반이 생기게된다.
  • 이를 해결하기위해 관심사를 분리하여 AppConfig를 나누어 의존 관계에 필요한 구현체를 따로 관리(생성과 주입)한다.
  • 이것이 Spring IoC/DI의 필요성이다.

Reference

⤧  Next post 42서울 오픈스튜디오 프로젝트 개발기 - 5 ⤧  Previous post Spring Aop와 Java Dynamic Proxy, CGLib