제어의 역전(IoC)과 의존관계 주입(DI)이란?

gorapaduckoo·2023년 6월 24일
0

스프링 기본편

목록 보기
3/10
post-thumbnail

인프런 김영한님의 스프링 핵심 원리 - 기본편 강의 내용을 바탕으로 작성한 글입니다.


이전 글에서 다형성만으로는 OCP, DIP를 지킬 수 없다는 것까지 알아보았다. 그렇다면 어떻게 OCP, DIP를 지킬 수 있을까? 바로 DI와 IoC를 이용하면 지킬 수 있다. 간단한 예시를 통해 알아보자.

클래스 의존관계

여기 주문 서비스가 있다. 주문 서비스는 할인 정책을 받아서 할인 금액을 계산해야 한다. 할인 정책은 크게 2종류가 있는데, 고정금액 할인 정책(FixDiscountPolicy)과 고정비율 할인 정책(RateDiscountPolicy)이 있다.

public class OrderServiceImpl {
	
//	private final DiscountPolicy discountPolicy = new FixDiscountPolicy(); // 기존 코드
		private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 변경된 코드
}

고정금액 할인 정책을 고정비율 할인 정책으로 변경하려는 상황이라고 가정하자. 예를 들어, 이전에는 2000원을 할인해주었다면 이제는 주문금액의 10%를 할인해주는 것이다. 이전 코드에서는 할인 정책의 구현체를 변경하려면 위와 같이 클라이언트인 OrderServiceImpl의 코드까지 변경해주어야 했다.

클라이언트(OrderServiceImpl)가 구현체(FixDiscountPolicy)에도 의존하기 때문에 위와 같은 문제가 발생했다. 그렇다면 구현체에 의존하는 부분을 빼면 되지 않을까?

public class OrderServiceImpl {
	private DiscountPolicy discountPolicy;
}

DIP를 위반하지 않도록, 클라이언트가 인터페이스에만 의존하게끔 의존관계를 변경했다. (final 키워드가 있으면 반드시 값을 할당해줘야 하므로 함께 지워주었다.)

그런데 위와 같이 참조변수만 선언하면 주문 서비스에서는 아무것도 할 수 없다. 누군가는 반드시 할인 정책의 구현체를 생성해서 클라이언트에게 갖다주어야 한다. 그렇다면 이제 그 '누군가'에 해당하는, 설정 클래스를 만들어 볼 차례이다.

1. 설정 클래스 생성

설정 클래스는 구현체를 생성한 뒤, 클라이언트와 구현체를 연결해준다.
이전에는 OrderServiceImpl이 직접 구현체를 선택했다면, 이제는 AppConfig라는 설정 클래스가 구현체를 선택한 뒤 생성자를 통해 OrderServiceImpl에게 구현체를 전달해 줄 것이다.

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

public class AppConfig {
	public OrderService orderService() {
    	return new OrderServiceImpl(new FixDiscountPolicy());
    }
}

위의 코드에서 객체를 생성하고 연결해주는 것은 AppConfig가 담당하고, OrderServiceImpl은 어떤 구현체가 들어오는지 알지 못한 채로, AppConfig가 갖다준 구현체를 받아다 사용하기만 한다.

만약 할인 정책을 고정 비율 할인 정책으로 변경하고 싶다면 어떻게 해야할까?

public class AppConfig {
	public OrderService orderService() {
    	return new OrderServiceImpl(new RateDiscountPolicy());
    }
}

AppConfigFixDiscountPolicy()RateDiscountPolicy()로 변경하면 된다. 이전과 달리, 할인 정책이 변경되어도 OrderServiceImpl은 영향을 받지 않는다는 것을 확인할 수 있다.

이처럼 AppConfig의 등장으로 애플리케이션이 실제 객체를 사용하는 부분과, 객체를 생성하고 연결하는 부분으로 분리되었다.

이제 설정 클래스를 분리함에 따라 바뀐 부분에 대해 알아보자.


2. IoC (Inversion Of Control, 제어의 역전)

이전에는 클라이언트 구현체인 OrderServiceImpl이 스스로 할인 정책 구현체를 선택하고 사용했다. 구현체들끼리 프로그램을 제어한 것이다.

하지만 AppConfig의 등장으로 인해 구현체는 할인 정책을 받아다 사용하기만 하고, 프로그램을 제어하는 부분(=구현체를 선택하고 생성하는 부분)은 AppConfig에게 넘어갔다. OrderServiceImpl은 어떤 구현체가 들어올지 알 수 없다.

이렇게 프로그램의 제어 흐름을 외부에서 관리하는 것을 제어의 역전(IoC)이라고 한다. 스프링에서 제어의 역전이 일어난다는 것은, 스프링이 프로그램의 흐름을 제어한다는 의미이기도 하다.

💡프레임워크 vs 라이브러리
제어의 역전은 프레임워크와 라이브러리를 구분하는 기준이기도 하다. 프레임워크에서는 제어의 역전이 일어나 프레임워크가 코드를 제어하고 대신 실행해준다. 하지만 라이브러리에서는 제어의 역전이 일어나지 않아, 내가 작성한 코드가 직접 프로그램 제어의 흐름을 담당한다.


3. DI (Dependency Injection, 의존관계 주입)

의존관계 주입이란, 애플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성해 클라이언트에 전달하는 것을 말한다. 의존관계 주입을 이용하면, 정적인 클래스 의존관계를 변경하지 않고 동적인 인스턴스 의존관계를 쉽게 변경할 수 있다.

이렇게 말하면 감이 잘 안올테니 코드와 그림을 통해 살펴보자.

클래스 의존관계
위에서 봤던 그림이 맞다. 클래스 의존관계는 애플리케이션을 실행하지 않아도 파악할 수 있다. (그래서 정적이라고 한다.)
OrderServiceImplOrderService 인터페이스를 구현하고 있고, DiscountPolicy 인터페이스에 의존하고 있다. 그리고 DiscountPolicyFixDiscountPolicyRateDiscountPolicy라는 2개의 구현 클래스를 갖고 있다.

그런데 정적인 클래스 의존관계로는, 실행 시점에 OrderServiceImpl이 어떤 구현체를 주입받는지 알 수가 없다. 이것이 바로 클래스 의존관계와 인스턴스 의존관계의 차이이다. 애플리케이션을 실행하여 실제 구현체가 생성된 후 그린 인스턴스 간의 의존관계가 바로 인스턴스 의존관계이다.

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

역할과 구현이 잘 보이게끔 수정한 설정 파일이다. 위의 코드를 따라 인스턴스 사이의 의존관계를 그려보면 아래와 같이 그려질 것이다.

인스턴스 의존관계

만약 설정 파일에서 FixDiscountPolicyRateDiscountPolicy로 변경되어도, 클래스 의존관계는 변하지 않고 인스턴스 의존관계만 변한다. 이게 바로 DI의 장점이다.

지금까지 DI와 IoC에 대해 알아보았다. AppConfig처럼 DI와 IoC를 해주는 것을 DI 컨테이너라고 하는데, DI 컨테이너는 객체를 생성하고 관리하면서 연결해준다. 스프링을 배워본 적이 있는 사람이라면 스프링의 특징으로 DI와 IoC를 많이 들어봤을텐데, 스프링이 바로 DI 컨테이너 역할을 해주기 때문이다.


4. 정리

  • IoC(Inversion of Control, 제어의 역전): 프로그램의 제어 흐름을 외부에서 관리하는 것
  • DI(Dependency Injection, 의존관계 주입): 애플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성해 클라이언트에 전달하는 것
  • DI 컨테이너: DI를 해주는 컨테이너. 객체를 생성하고 관리하면서 연결해줌

0개의 댓글