[Spring] 제어의 역전(IoC)과 의존성 주입(DI)이란?
안녕하세요? 제이온입니다.
오늘은 스프링 핵심 개념 중 하나인 제어의 역전(IoC)와 의존성 주입(DI)에 대해서 알아 보겠습니다.
제어의 역전(IoC)이란?
흔히, 스프링에서 제어의 역전은 스프링 컨테이너가 필요에 따라 Bean들을 관리하거나 제어하는 행위라고 합니다. 하지만 아직 스프링 컨테이너와 빈에 대한 설명은 하지 않았고, 보다 근본적인 의미와 예제를 통해 IoC를 이해해 보겠습니다.
제어의 역전은 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 의미합니다. 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고 실행하였습니다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종한 것이죠. 여기까지 들으면 도통 무슨 말인지 이해하기 어려울 것입니다. 해당 설명을 예제를 통해 알아 보겠습니다.
이번 예제는 할인 정책이 들어간 제품을 구매하는 상황입니다.
public class Order {
private final String itemName;
private final int itemPrice;
private final int discountPrice;
public Order(String itemName, int itemPrice, int discountPrice) {
this.itemName = itemName;
this.itemPrice = itemPrice;
this.discountPrice = discountPrice;
}
public int getDiscountPrice() {
return discountPrice;
}
}
public interface OrderService {
Order createOrder(int age, String itemName, int itemPrice);
}
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@Override
public Order createOrder(int age, String itemName, int itemPrice) {
int discountPrice = discountPolicy.discount(age, itemPrice);
return new Order(itemName, itemPrice, discountPrice);
}
}
실질적으로 OrderService의 구현 객체인 OrderService에서 주문이 발생합니다. 인자로 구매자의 나이, 제품의 이름, 제품의 가격을 입력하면 Order 객체를 반환합니다. 다만, OrderServiceImpl은 DiscountPolicy를 의존합니다. 그리고 이 할인 정책 종류에 따라 할인 금액이 정해집니다. DiscountPolicy와 관련된 객체들은 아래와 같습니다.
public interface DiscountPolicy {
int discount(int age, int price);
}
public class FixDiscountPolicy implements DiscountPolicy {
private static final int ADULT = 20;
private static final int DISCOUNT_FIX_AMOUNT = 1000;
@Override
public int discount(int age, int price) {
if (age < ADULT) {
return DISCOUNT_FIX_AMOUNT;
}
return 0;
}
}
public class RateDiscountPolicy implements DiscountPolicy {
private static final int ADULT = 20;
private static final int DISCOUNT_PERCENT = 10;
@Override
public int discount(int age, int price) {
if (age < ADULT) {
return price * DISCOUNT_PERCENT / 100;
}
return 0;
}
}
FixDiscountPolicy는 20살 미만인 이용자에게 1000원 할인을, RateDiscountPolicy는 20살 미만인 이용자에게 10% 할인을 적용합니다. 우선, 현재 서비스에서는 고정 할인 정책을 수용하고 있다고 가정하겠습니다.
public class Main {
public static void main(String[] args) {
final OrderService orderService = new OrderServiceImpl();
final Order order = orderService.createOrder(10, "샤프", 3000);
System.out.println(order.getDiscountPrice());
}
}
그렇다면 사용자 입장에서는 OrderService 객체를 만들고 주문을 진행하여 할인된 금액을 알아낼 수 있습니다. 지금까지의 도메인 구조는 아래와 같습니다.
그런데, 여기서 OrderServiceImpl은 DiscounPolicy와 FixDiscountPolicy를 모두 의존하고 있습니다.
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@Override
public Order createOrder(int age, String itemName, int itemPrice) {
int discountPrice = discountPolicy.discount(age, itemPrice);
return new Order(itemName, itemPrice, discountPrice);
}
}
만약 할인 정책이 고정 할인 정책이 아니라 정률 할인 정책으로 바뀐다면 OrderServiceImpl의 코드를 수정해야 합니다. 지금은 코드가 짧아서 단순히 'new FixDiscountPolicy()' 부분을 'new RateDiscountPolicy()'로 고치기만 하면 됩니다. 하지만, 만약에 해당 고정 할인 정책을 의존하는 객체가 많고 로직이 많다면 그 부분을 싹다 고쳐줘야 합니다. OCP 법칙에 어긋나는 행위죠.
이를 해결하기 위한 가장 좋은 방법은 OrderServiceImpl 내에서 DiscounPolicy의 구현 객체를 정해주지 않는 것입니다. 그렇다고 아래와 같이 코드를 작성하면 NullPointerException이 발생합니다.
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy;
@Override
public Order createOrder(int age, String itemName, int itemPrice) {
int discountPrice = discountPolicy.discount(age, itemPrice);
return new Order(itemName, itemPrice, discountPrice);
}
}
따라서 외부에서 DiscountPolicy를 주입해 주어야 합니다. 이때 의존성 주입(DI)이 사용되는데 이 부분은 아래에서 자세히 다루겠습니다. 현재 상황에서는 생성자를 통해 DiscountPolicy를 초기화해 주겠습니다.
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(int age, String itemName, int itemPrice) {
int discountPrice = discountPolicy.discount(age, itemPrice);
return new Order(itemName, itemPrice, discountPrice);
}
}
다만, 이렇게 하면 다른 곳에서 컴파일 에러가 발생합니다.
public class Main {
public static void main(String[] args) {
final OrderService orderService = new OrderServiceImpl();
final Order order = orderService.createOrder(10, "샤프", 3000);
System.out.println(order.getDiscountPrice());
}
}
사용자 입장에서 OrderService를 사용할 때 구현 객체를 정해줘야 하기 때문이죠. 이 부분도 매번 특정 객체를 만들어서 넘겨주어야 한다는 문제가 있습니다. 그래서 해당 객체를 주입해 주는 역할만 하는 설정 관련 객체를 새로 정의하겠습니다.
public class AppConfig {
public OrderService orderService() {
return new OrderServiceImpl(discountPolicy());
}
public FixDiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
public class Main {
public static void main(String[] args) {
final AppConfig appConfig = new AppConfig();
final OrderService orderService = appConfig.orderService();
final Order order = orderService.createOrder(10, "샤프", 3000);
System.out.println(order.getDiscountPrice());
}
}
AppConfig는 프로그램 전반적으로 사용될 객체를 미리 정의해 둡니다. 그리고 사용자 입장에서는 무슨 할인 정책을 쓸지 선택할 필요가 없습니다. 단순히 AppConfig로부터 제공받는 객체를 사용할 뿐이죠. 만약, 할인 정책이 바뀐다면 AppConfig에서 FixDiscountPolicy를 RateDiscountPolicy로만 바꾸면 됩니다.
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(int age, String itemName, int itemPrice) {
int discountPrice = discountPolicy.discount(age, itemPrice);
return new Order(itemName, itemPrice, discountPrice);
}
}
OrderSeriveImpl은 할인 정책이 바뀌어도 상관이 없습니다. 애초부터 OrderServiceImpl은 주문만 하면 되는 것이지 구체적으로 할인 정책을 알 필요가 없는 것이죠.
정리하면, 구현 객체는 자신의 로직을 실행하는 역할만 담당하고 프로그램의 제어 흐름은 AppConfig가 가져가게 되었습니다. 예를 들어서 OrderServiceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될 지는 모르고 있습니다. 또한, OrderServiceImpl 자체도 AppConfig가 생성하고 있으므로 AppConfig는 OrderServiceImpl이 아닌 OrderService의 또다른 하위 구현 객체를 정의하여 사용자에게 넘겨줄 수도 있습니다. 이렇듯 프로그램의 제어 흐름을 개발자가 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(Ioc)라고 합니다. 이 예시에서 외부는 AppConfig가 되는 것이죠.
의존성 주입(DI)이란?
사람들이 많이 쓰는 용어는 의존성 주입이지만, 저는 개인적으로 의존 관계 주입이 더 이해하기 쉽다고 생각하여 의존 관계 주입이라고 부르겠습니다.
먼저, 의존 관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체 의존 관계 둘로 분리해야 합니다. 위 예시에서 정적인 클래스 의존 관계는 아래와 같습니다.
정적인 클래스 의존 관계는 클래스가 사용하는 import 코드만 보고 판단할 수 있습니다. 여기서 OrderServiceImpl은 DiscountPolicy에 의존한다는 사실을 알 수 있습니다. 하지만, OrderServiceImpl은 해당 정적 클래스 의존 관계만으로는 구체적으로 어떠한 구현 할인 객체가 주입될 지는 알 수가 없습니다. 그래서 우리는 동적인 객체 의존 관계를 파악해야 합니다.
동적인 객체 의존 관계는 애플리케이션 실행 시점에 생성된 객체 인스턴스의 참조가 연결된 의존 관계를 의미합니다. 그리고 애플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결되는 것을 의존 관계 주입이라고 합니다. 위의 예시에서는 AppConfig에서 구현 할인 객체를 만들어서 OrderServiceImpl의 생성자로 넘겼습니다.
의존 관계 주입을 사용하면 클라리언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있습니다. 즉, 정적인 클래스 의존 관계는 유지하되 동적인 객체 의존관계만 변경되는 것이죠. 우리가 할인 정책을 고정에서 정률로 바꾼다고 해서 OrderServiceImpl과 DiscountPolicy의 의존 관계가 깨지는 것도 아니기 때문입니다.
지금 예제에서는 의존성 주입을 생성자 방식을 사용하였지만, setter문, 스프링의 경우 @Autuwired 어노테이션을 통해서도 가능합니다.
IoC 컨테이너, DI 컨테이너
이렇게 AppConfig처럼 객체를 생성하고 관리하면서 의존 관계를 연결해 주는 것을 IoC 컨테이너 혹은 DI 컨테이너라고 부릅니다. 최근에는 의존 관계 주입 자체에 초점을 맞추어 DI 컨테이너라고 부르는 편이라고 합니다.
정리
지금까지 IoC와 DI에 대해 알아 보았습니다. 사실 제목 태그의 Spring을 적어 두었지만 이번 포스팅에서는 Spring 이야기를 거의 하지 않았습니다. 그럼에도 Spring을 이해하는 데 있어서 IoC와 DI는 필수적입니다. 이후 포스팅에서는 조금씩 Spring에서 IoC와 DI를 언제 쓰고 있는지 계속 이야기를 할 예정입니다.
출처
'개발 이야기 > Spring' 카테고리의 다른 글
[Spring] 서블릿과 서블릿 컨테이너란? (0) | 2021.06.19 |
---|---|
[Spring] 빈의 생명 주기 (0) | 2021.06.17 |
[Spring] 컴포넌트 스캔과 의존 관계 자동 주입 (0) | 2021.06.17 |
[Spring] 스프링 컨테이너와 빈이란? (1) | 2021.06.16 |
[Spring] 스프링 프레임워크(Spring Framework)란? (5) | 2021.06.14 |
댓글