1) 새로운 할인 정책 추가 기존에 구현된 고정 할인 정책에서 정률 할인 정책(주문한 금액의 %를 할인)을 새롭게 추가 2) 문제점 - 할인 정책을 변경 시, 클라이언트(OrderServiceImpl)에서 코드 변경이 이루어진다. - OCP, DIP 객체 지향 설계 원칙을 준수하지 못함
3) 해결안 - DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경 - 클라이언트에 구현 객체를 대신 생성하고 주입해줄 수 있는 클래스를 생성한다. (AppConfig Class의 등장)
■ 클라이언트 코드 변경
import com.example.demo.member.Member;
import com.example.demo.member.MemberRepository;
import com.sun.org.apache.xpath.internal.operations.Or;
public class OrderServiceImpl implements OrderService{
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
//memberRepository 객체와 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.java
import com.example.demo.member.MemberRepository;
import com.example.demo.member.MemberService;
import com.example.demo.member.MemberServiceImpl;
import com.example.demo.member.MemoryMemberRepository;
import com.example.demo.order.DiscountPolicy;
import com.example.demo.order.OrderService;
import com.example.demo.order.OrderServiceImpl;
import com.example.demo.order.RateDiscountPolicy;
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
private DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
■ OrderApp.java
import com.example.demo.member.Grade;
import com.example.demo.member.Member;
import com.example.demo.member.MemberService;
import com.example.demo.order.Order;
import com.example.demo.order.OrderService;
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(member.getMemberId(), "itemA", 20000);
System.out.println("할인 가격 = " + order.calculate());
}
}
■ OrderService Test
import com.example.demo.App.AppConfig;
import com.example.demo.member.Grade;
import com.example.demo.member.Member;
import com.example.demo.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class OrderServiceTest {
private MemberService memberService;
private OrderService orderService;
//@BeforeEach는 테스트를 실행하기 전에 호출
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder(){
//given
Member member = new Member(1L, "memberA", Grade.VIP);
//when
memberService.join(member);
Member findMember = memberService.findMember(1L);
//then
Order order = orderService.createOrder(findMember.getMemberId(), "itemA", 20000);
int price = order.calculate();
Assertions.assertThat(price).isEqualTo(18000);
}
}
AppConfig - AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성 - AppConfig는 생성한 객체 인스턴스의 참조값을 생성자를 통해 주입(연결) - AppConfig는 애플리케이션의 구성 역할을 담당
OrderServiceImpl - OrderServiceImpl은 DiscountPolicy 인터페이스에만 의존 - OrderServiceImpl은 생성자를 통해 어떤 구현 객체가 주입될 지 알 수 없다. 어떤 구현 객체를 주입할지는 AppConfig에서 결정 - OrderServiceImpl은 실행에만 집중
■ 좋은 객체 지향 설계의 5가지 원칙 적용
SRP 단일 책임 원칙 : 한 클래스는 하나의 책임만 가지면 된다. - 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당 - 클라이언트 객체는 실행하는 책임만 담당
DIP 의존관계 역전 원칙 : 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나이다. - AppConfig가 RateDiscountPolicy 객체 인스턴스를 생성해서 클라이언트에 의존관계를 주입, DIP원칙을 지킴
OCP : 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다. - 다형성을 사용하며, 클라이언트가 DIP를 지킴 - 애플리케이션이 사용 영역가 구성 영역으로 나뉨 - AppConfig가 의존관계를 FixedDiscountPolicy에서 RateDiscountPolicy로 변경해서 클라이언트에 주입하므로 클라이언트 코드는 변경할 필요가 없다.
■ 제어의 역전, IoC, Inversion of Control
제어의 역전, IoC, Inversion of Control - 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행하였다. - AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당 - 프로그램에 대한 제어 흐름 권한은 AppConfig가 가져가게 되었다. - 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)라고 한다.
프레임워크 VS 라이브러리 - 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 라이브러리 (ex : JsonObject 라이브러리) - 내가 작정한 코드를 제어하고 대신 실행한다면 그것은 프레임워크(ex : JUnit 프레임워크)
■ 의존관계 주입, DI, Dependency Injection
의존관계 주입, DI, Dependency Injection - OrderServiceImpl은 DiscountPolicy 인터페이스에 의존, 실제 어떤 구현 객체가 사용될지는 알지 못한다. - 의존관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 이 둘을 분리해서 생각해야 한다.
[정적인 클래스 의존관계] - 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석이 가능 - OrderServiceImpl은 MemberRepository, DiscountPolicy 인터페이스에 의존한다는 것을 알 수 있다. 하지만 이러한 클래스 의존관계만으로는 실제 어떤 객체가 OrderServiceImpl에 주입되었는지는 알 수 없다.
[동적인 클래스 의존관계] - 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결 되는 것을 의존관계 주입(DI)이라고 한다. - 객체 인스턴스를 생성하고, 그 참조값을 (생성자를 통해) 전달해서 연결하게 된다. - 의존관계 주입을 사용하면, 클라이언트 코드는 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다. - 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
■ DI 컨테이너, IoC 컨테이너
DI 컨테이너, IoC 컨테이너 - AppConfig처럼 객체를 생성하고 관리하면서 의존 관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다.