[Spring] DI 컨테이너란? DI 컨테이너를 사용하는 이유

Jaeyoo (유재형)·2022년 2월 17일
0
post-thumbnail
post-custom-banner

스프링의 컨테이너와 스프링 빈을 이해하기위해 먼저 DI컨테이너가 무엇이고 왜 생겨났는지 객체지향과 SOLID 원칙의 관점에서 정리를 해보려 한다.


DI 컨테이너란?


DI

  • DI는 Dependency Injection으로 의존관계 주입이라고한다.
  • 의존관계 주입은 애플리케이션 런타임에 외부에서 실제 구현 객체를 생성하고 Client에 전달해서 Client와 Server의 실제 의존관계가 연결되는것을 말한다.
  • DI 를 사용하면 Client 코드를 변경하지않고 Client가 호출하는 대상의 인스턴스를 변경할수있다.

    서버와 클라이언트

    • A 객체가 B 객체의 메서드를 호출하면, A 객체를 클라이언트 B 객체를 서버라고한다.
    • 이러한 개념이 웹 브라우저(client)와 애플리케이션 서버(Server) 개념까지 확장된다.

DI 컨테이너

  • 외부에서 객체를 생성하고 관리하면서 의존관계를 연결해주는것을 DI 컨테이너라고 한다.
  • IoC 컨테이너라고도 하는데 의존관계 주입에 초점을 맞추어 주로 DI 컨테이너라 한다.

    IoC(Inversion of Control)

    • 제어의 역전
    • 프로그램의 제어 흐름을 직접 제어하는것이 아니라 외부에서 관리하는것을 IoC라고함

DI 컨테이너를 사용하는 이유


예시를 통해 DI 컨테이너를 왜 사용하는지 알아보자.

1. 주문 서비스와 할인 정책 구현

그림처럼 할인 정책을 적용할수있는 주문을 구현해보자

1) 할인 정책 인터페이스 (DiscountPolicy)

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

2) 정액 할인 구현체 (FixDiscountPolicy)

public class FixDiscountPolicy implements DiscountPolicy {
    
    private int discountFixAmount = 1000; //1000원 할인
    
    @Override
    public int discount(Member member, int price) {
         if (member.getGrade() == Grade.VIP) {
              return discountFixAmount;
         } else {
             return 0;
    	} 
    }
}
  • VIP면 1000원 할인

3) 주문 서비스 구현체 (OrderServiceImpl)

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);
    }
}
  • 주문 생성 요청오면, 회원 정보를 조회하고 할인정책을 적용한다음 주문 객체를 생성해서 반환한다.
  • MemoryMemberRepositoy와 FixDiscountPolicy를 구현체로 생성한다.

만약 기존의 정액 할인 정책뿐 아니라 새로운 정률할인 정책을 추가하면 어떻게 될까?


2. 새로운 할인 정책 추가

1) 정률 할인 정책 (RateDiscountPolicy)

public class RateDiscountPolicy implements DiscountPolicy{

    private int discountPercent = 10; //10% 할인

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP){
            return price * discountPercent / 100;
        }
        else {
            return 0;
        }
    }
}
  • VIP면 10% 할인

2) 주문 서비스 구현체 변경 (OrderServiceImpl)

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    ...
}
  • 할인 정책의 변경을 위해 OrderServiceImpl 코드의 변경이 일어났다.
  • discountPolicy의 구현체를 RateDiscountPolicy로 바꿔준다.

3. 문제점 발견

SOLID 원칙의 관점에서 두가지 문제가 있다.

SOLID 원칙에 대한 내용을 아래의 링크에서 설명했다.
SOLID란?

  1. DIP 위반
  • OrderServiceImpl를 보면 추상 인터페이스에도 의존하고 구체 클래스에도 의존한다.
    • 추상 인터페이스 : DiscountPolicy
    • 구체 클래스 : FixDiscountPolicy , RateDiscountPolicy
  1. OCP 위반
  • 코드를 변경하지 않고서 확장이 불가능하다.
  • OrderServiceImpl의 코드 변경이 일어남

4. DI 컨테이너 도입

  • 위와 같은 문제를 해결하기위해 구현 클래스의 의존을 제거하고 인터페이스에만 의존하도록 설계를 변경해보자
  • 그러기 위해서는 외부에서 OrderServiceImplDiscountPolicy의 구현객체를 대신 생성하고 주입해주어야한다. => DI 컨테이너

1) AppConfig

DI 컨테이너 : 구현 객체를 생성하고 주입해주는 별도의 설정 클래스

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

2) OrderServiceImpl 생성자 추가

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;
    }

    @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);
    }
}
  • AppConfig에서 생성자를 주입하기위해 OrderServiceImpl에 생성자를 추가한다.

3) 사용 클래스

public class OrderApp {
    public static void main(String[] args) {

        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        
        System.out.println("Order : " + order);
    }
}

5. DI 컨테이너 도입을 통한 SOLID 원칙 적용

SRP : 단일 책임 원칙

  • 관심사 분리
  • 구현 객체를 생성하고 연결하는 책임을 AppConfig가 담당
  • 클라이언트 객체는 실행하는 책임만 담당

DIP : 의존관계 역전 원칙

  • 추상화에 의존하고 구체화에 의존하지않는다.
  • AppConfig 객체로 생성자 주입

OCP : 개방 폐쇄 원칙

  • 소프트웨어 요소를 새롭게 확장해도 변경에 닫혀있다.
  • 기능을 추가해도 코드 변경을 하지않는다.
profile
기록과 반복
post-custom-banner

0개의 댓글