프로그래밍 언어 활용/SPRING

[SPRING] 3. 의존관계 주입

프린이8549 2024. 9. 4. 21:47

<1편> 스프링의 시작 https://yangpro8549.tistory.com/58

<2편> 객체 지향 설계와 스프링 https://yangpro8549.tistory.com/59?category=1185843

 

본 정리는 김영한님의 인프런 강의 <스프링 핵심 원리 - 기본편>을 바탕으로 작성하였습니다.

 

0.  들어가기 전에

 

 예컨대 할인 정책에 관한 로직을 정액 할인에서 정률 할인으로 변경하고자 할 때 우리는 아래와 같은 흐름에 따라 코드를 수정할 수 있다. 

 

 

public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
 private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

 

위 코드는 다형성을 충실히 사용하고 있다.

 

그러나 좋은 객체 지향 설계 원칙(SOILD)의 관점에서

 

할인 정책 변경 시 코드가 수정되기 때문에 변경에 닫혀 있어야 한다OCP 원칙을 위배하고

 

또한 할인 정책 객체 생성 시 RateDiscountPolicy() 라는 구현체에 의존함으로써 프로그램은 구체화가 아닌 추상화에 의존해야한다는 DIP 원칙을 위배했다고 볼 수 있다.

 

결국 실제 의존관계는 아래와 같다고 할 수 있다.

 

이를 통해 우리는 다형성을 통해 인터페이스를 구현하는 과정에서 다형성만으로는 OCP와 DIP를 준수할 수 없음을 알 수 있다.

 

그렇다면 어떻게 위 문제를 해결할 수 있을까?

 

본 고에서는 어떻게 OCP와 DIP를 준수하면서 실행 시점의 상황에 맞게 유연하게 정책을 변경할 수 있을지 알아보고자 한다.

 

1. 인터페이스에만 의존하도록 하자

 

서론에서 제기한 문제 상황을 정리하면 아래와 같다.

 

- 클라이언트 코드인 OrderServiceImpl은 DiscountPolicy의 인터페이스 뿐만 아니라 구체 클래스도 함
께 의존한다.

- 따라서  구체 클래스 변경 시 필연적으로 클라이언트 코드의 변경이 수반된다(DIP, OCP 위반)

 

결국 위 문제 상황을 해결하기 위해서는 OrderServiceImpl가 DiscountPolicy의 인터페이스에만 의존하도록 의존관계를 변경하면 된다.

 

그렇다면 OrderServiceImpl의 멤버는 아래와 같을 것이다.

 

public class OrderServiceImpl implements OrderService {
 //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
 private DiscountPolicy discountPolicy;
}

 

그러나 위 구현 클래스를 실행해보면 DiscountPolicy의 구현체가 없기 때문에 코드를 실행하지 못하고 NULL POINTER EXCEPTION이 발생할 것이다.

 

결국 이 문제를 해결하기 위해서는 외부에서 DiscountPolicy의 구현 객체를 대신 생성하고 이를 OrderServiceImpl 에 주입해주는 과정을 거쳐야만 한다.

 

즉, 공연 무대로 비유하자면 배우는 본인의 역할인 배역을 수행하는 것에만 집중하게 하고, 공연을 구성하고 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 지니는 별도의 공연 기획자를 지정하는 것이다.

 

그리하여 우리는 애플리케이션의 공연 기획자로 AppConfig라는 클래스를 섭외할 것이다.

 

2. AppConfig의 등장

 

우리는 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 지니는 별도의 설정 클래스 AppConfig를 생성할 것이다.

 

 

해당 클래스의 구성은 아래와 같다.

 

package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

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 RateDiscountPolicy();
 	}
}

 

 

 이를 통해 AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체(이하의 목록)를 생성하고

  • MemberServiceImpl
  • MemoryMemberRepository
  • OrderServiceImpl
  • FixDiscountPolicy

또한 아래에서 보듯 객체 인스턴스의 참조를 생성자를 통해 주입(연결)시켜주는 역할을 수행하게 된다.

  • MemberServiceImpl -> MemoryMemberRepository
  • OrderServiceImpl -> MemoryMemberRepository , FixDiscountPolicy

 

위 과정을 거친 뒤에는 OrderServiceImpl를 아래와 같이 수정할 수 있다.

 

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;

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 를 생성하는 역할을 대신 수행함으로써 OrderServiceImpl는 DiscountPolicy 인터페이스에만 의존할 수 있게 되었다.(DIP 원칙 준수)

 

상기 코드들에 따르면  OrderServiceImpl 에는 MemoryMemberRepository , FixDiscountPolicy 객체의 의존관계가 주
입되는데,  결국 OrderServiceImpl의 입장에서는 오직 외부의 AppConfig를 통해서만 어떤 구현 객체가 주입될지 알 수 것이다.

 

따라서 위와 같은 형태를 의존관계/의존성 주입(Dependency Injection; DI) 라고 한다.

 

만약 AppConfig에게 의존관계를 주입하도록 한 상황에서 서론에서와 같이 할인 정책을 변경해야한다면 어떻게 해야할까?

 

그럴 때는 하단의 클래스 다이어그램처럼 AppConfig 내에서 할인 정책에 관한 구현 객체를 수정해주면 된다.

 

 

...
public DiscountPolicy discountPolicy() {
    // return new FixDiscountPolicy();
     return new RateDiscountPolicy();
 }

 

즉 이후로는 변경 사항 발생 시 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 되며

 

클라이언트  코드인 OrderServiceImpl 등 사용 영역의 어떤 코드도 변경할 필요가 없다(OCP 원칙 준수).

 

이상으로 우리는 AppConfig라는 기획자를 섭외하여 구성 영역과 사용 영역을 구분함으로써 변경 사항 발생 시에도 OCP 와 DIP를 준수할 수 있게 되었다.

 

추가적으로 AppConfig는 구현 객체를 생성하고 연결하는 책임을 지고, 대신 클라이언트 코드는 실행에 관한 책임만 보유하게 됨으로써 SRP 원칙까지 적용할 수 있게 되었다.

 

3. DI, 컨테이너 그리고 IoC

 

 결론적으로 DI애플리케이션 실행 시점(런타임)에 외부에서 실 구현 객체를 생성하고 클라이언트에 전달해서-객체 인스턴스를 생성하고 그 참조값을 전달- 클라이언트와 서버의 실제 의존관계가 연결되는 것이라고 할 수 있다.

 

 이를 통해 우리는 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있게 되는 것이다.(정적인 클래스 의존관계는 변경하지 않고 동적인 객체 인스턴스 의존관계만 변경)

 

그리고 앞서 AppConfig의 예시처럼 객체를 생성하고 관리하며 의존관계를 연결해주는 역할을 수행하는 것을 DI 컨테이너라고 한다.

 

한편, AppConfig의 등장과 함께 특기할만한 것은, 예컨대 OrderServiceImpl의 입장에서 이전과는 달리 특정 인터페이스 호출 시 어떤 구현 객체가 실행될지 모른다는 점이다. 즉 클라이언트 구현 객체가 자신이 필요한 구현 객체를 직접 생성하고 연결하던 것에서 자신의 로직을 실행하는 역할만 수행하게 됨으로써 애플리케이션의 제어 흐름이 전적으로 AppConfig에게 넘어가버렸다.

 

위처럼 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 우리는 제어의 역전Inversion Of Control; IoC)라고 한다.

 

바로 이 IoC를 통해 프레임워크와 라이브러리를 구분할 수 있게 되는데, 

 

프레임워크는 내가 작성한 코드를 제어하고 대신 실행해주는 반면,

라이브러리는 개발자가 작성한 코드가 직접 제어의 흐름을 담당한다.