[Spring] DI와 IoC 그리고 컨테이너
김영한님의 스프링 핵심 원리 - 기본편을 바탕으로 작성하였습니다.
DI
DI는 의존성 주입(Dependency Injection)이란 뜻으로,
의존 관계를 외부에서 결정 하여 주입한다는 의미 이다.
의존관계란?
A클래스와 B클래스가 있을 때,
B클래스의 변경이 A클래스에 영향을 미칠 때 A는 B와 의존 관계라고 한다.
DI를 통해 모듈간의 결합도가 낮아지고 유연성이 높아진다.
IoC
IoC는 제어의 역전(Inversion of Control) 이라는 의미이다.
메소드나 객체의 호출 작업을 개발자가 결정하는 것이 아니라, 외부에서 결정되는 것을 의미한다.
대부분의 프레임워크에서 사용하는 방식이다.
덕분에 개발자는 프레임워크에 필요한 부품을 개발하고, 조립하는 방식의 개발을 하게 된다.
이렇게 조립된 코드의 최종호출은 개발자가 아닌 프레임워크 내부에서 결정된 대로 이뤄지는데,
이러한 현상을 제어의 역전이라고 한다.
이에 따라 스프링에서는 다음과 같은 순서로 객체가 만들어지고 실행된다.
- 객체 생성
- 의존성 객체 주입(스스로가 만드는것이 아니라 제어권을 스프링에게 위임하여 스프링이 만들어놓은 객체를 주입한다.)
- 의존성 객체 메소드 호출
DI 컨테이너 만들기
나에게는 위의 두내용을 텍스트만으로 이해하기에는 조금 역부족이었다.
스프링을 사용하지 않고,
간략한 서비스를 구현하고, DI컨테이너를 이용해 리팩토링하는 과정을 통해
DI와 IoC의 개념을 다시 한번 정리하고,
필요성을 느껴보려 한다.
전체 코드는 깃헙에 올려놨음.
예제 코드
구현한 서비스는 물건을 구매할 시, 회원 등급에 따라 할인정책을 적용하는 간단한 서비스이다.
현재 적용된 할인 정책은 VIP일 경우 각 품목에 대한 1000원의 할인이 적용되는 정책이다.
단, 현재의 정책이 완전히 픽스된것이 아니라, 추후 변경이 있을 수 있다.
// interface
public interface DiscountPolicy {
int discount(Member member, int price);
}
// FixPolicy
public class FixDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
//RatePolicy
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
위의 코드를 보면,
현재는 FixDiscountPolicy
가 적용 되어 있지만,
추후 RatePolicy
로 바뀔 수도 있다는 내용이다.
다형성을 이용하여 구현하면, 크게 문제 될 것 같지 않아서 후딱 구현을 해보면
// interface
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
// implements
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl() {
this.memberRepository = new MemoryMemberRepository();
this.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);
}
}
이런 식으로 구현이 가능하다.
문제점
사실 별 문제 없어 보인다.
현재는 FixDiscountPolicy
가 적용 되어 있지만,
추후 RateDiscountPolicy
로 변경하고 싶다면
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl() {
this.memberRepository = new MemoryMemberRepository();
this.discountPolicy = new RateDiscountPolicy(); // 여기 변경됨
}
@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);
}
}
이렇게 변경해주면 되니, 크게 문제가 있어 보이지 않는다.
하지만 이 코드는 객체지향 SOLID 설계원칙 중 두가지나 어기고 있다.
객체지향 프로그래밍에 대한 포스팅은 아래글을 참고
https://kong-dev.tistory.com/208
OCP 원칙위반
OCP
- 소프트웨어 요소는 확장에는 열려있으나, 변경에는 닫혀 있어야 한다.
- 즉, 기존의 코드는 변경하지 않으면서도, 확장은 쉽게 할 수 있어야 한다.
현재 Fix -> Rate로 확장에 열려 있긴 하지만, 확장을 하기 위해선OrderServiceImpl
코드 역시 변경이 필요하다.
즉, 확장과 변경 모두에게 열려 있다는 의미이다.
DIP원칙 위반
DIP
- 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게하는 원칙
- 쉽게 말해, 구현클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻
- 역할과, 구현 중 역할에 의존하게 해야 한다.
DIP 원칙에 따르자면, 구현클래스(implements)에 의존하지 않고, 인터페이스에만 의존해야 하는데,
위의 코드에서 생성자와 필드만 가져와서 다시 살펴보면..
private final MemberRepository memberRepository; // interface에 의존
private final DiscountPolicy discountPolicy; // interface에 의존
public OrderServiceImpl() {
this.memberRepository = new MemoryMemberRepository(); // 구현체에 의존
this.discountPolicy = new RateDiscountPolicy(); // 구현체에 의존
}
인터페이스 뿐만 아니라, 구현체에도 의존하고 있는 것을 확인 할 수 있다.
결론적으로 위의 코드는 객체 지향 스럽지 않다.
리팩토링
위 코드를 객체지향스럽게 리팩토링 해보자.
(전체코드는 깃헙에 올려놓음)
현재 코드가 객체지향스럽지 않은 가장 큰 원인은
생성자 내부에서 구현체를 생성하기 때문이다.
이 부분을 의존성 주입(DI)를 통해 변경해준다면,
위의 문제들이 모두 깔끔하게 해결된다.
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);
}
}
이렇게만 변경했을 뿐인데,
벌써 모든게 해결되었다.
구현체에 의존하지 않고, 인터페이스에만 의존하고 있으며
확장은 유연하게 가능하고, 변경은 할 필요가없다.
즉, 위에서 위반하였던, DIP와 OCP 원칙 두가지를 모두 지키고 있다.
그런데 저 인자값을 넣어주는 곳에서 어차피 똑같이 new
키워드로 객체생성해서 주입해주면 똑같은거 아닌가..
라는 생각이 들 수 있는데,
그 역할을 해주는 친구가 컨테이너이다.
컨테이너
컨테이너, IoC컨테이너, DI컨테이너 등등 으로 불리기도 하지만,
주로 DI컨테이너라고 불리는 이 친구를 통해 의존성을 주입할 수 있다.
클래스 이름은 AppConfig로 지어주자.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public static DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
AppConfig는 이렇게 작성을 해주고,
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", 20000);
System.out.println(order);
System.out.println(order.calculatePrice());
}
}
실행을 시킬 때는 위와 같이 작성을 하면 된다.
그리고 할인 정책을 변경하고 싶다면, 다른 코드를 손댈 필요없이
AppConfig만 변경해주면된다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public static DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy(); // 여기만 변경
}
}
위 처럼 변경을 해주면 다른 코드는 어디 손대지 않아도 된다.
애플리케이션에서 구성과 구현이라는 관심사를 분리하여,
구성의 역할만 담당하게 한것이 AppConfig 클래스이다.
OrderServiceImpl
을 다시 한번 보면,
필요한 인터페이스들을 AppConfig로 부터 모두 호출하지만,
어떤 구현 객체들이 실행될지 알 수 없다.
즉, 프로그램의 제어 흐름에 대한 모든 권한은 AppConfig
가 지니고 있다.
이처럼 프로그램의 제어 흐름을 내부(OrderServiceImpl
)가 아닌,
외부(AppConfig
)에서 관리하는 것을 IoC라고 한다.
또한, 의존관계를 클래스 내부에서 정의 하는 것이 아니라,
외부(AppContainer
)에서 주입 받는 것을 DI라고 한다.
그리고 이러한 역할을 해주는 AppConfig
와 같은 것을
IoC 컨테이너 또는 DI 컨테이너라고 한다.
(거의 DI컨테이너라고 함)