package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
importstatic org.junit.jupiter.api.Assertions.*;
classRateDiscountPolicyTest{
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test@DisplayName("VIP는 10% 할인이 적용되어야한다.")voidvip_o(){
// given
Member member = new Member(1L, "memberVIP", Grade.VIP);
// whenint discount = discountPolicy.discount(member, 10000);
// then
Assertions.assertThat(discount).isEqualTo(1000);
}
}
Note) 실행 결과 - 성공 케이스
Ex) 실패 케이스
java
열기
@Test@DisplayName("VIP가 아니면 할인이 적용되지 않아야한다.")voidvip_x(){
// given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
// whenint discount = discountPolicy.discount(member, 10000);
// then
Assertions.assertThat(discount).isEqualTo(0);
}
Note) 실행 결과 - 실패 케이스
3.2 새로운 할인 정책과 문재점
- 이제 OrderServiceImpl 클래스의 내용 중, FixDiscountPolicy => RateDiscountPolicy로 바꾸면 된다.
- OCP, DIP 같은 객체지향 설계 원칙을 준수하지 못했다
-> DIP: 추상 클래스 뿐만아니라 구체(구현) 클래스에도 의존하고 있다.
-> OCP: 현재 코드는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 준다. => OCP 위반
기대했던 의존 관계
실제 의존관계
정책 변경
- 정액제를 정률제로 변경하는 순간 OrderServiceImpl의 소스 코드도 함께 변경해야한다. => "OCP"위반
- 그러면 인터페이스에만 의존하도록 다음과 같이 코드를 변경하면?
-> NullPointerException이 발생한다. => 선언만 하고 생성을 하지 않았기 때문이다.
java
열기
publicclassOrderServiceImplimplementsOrderService{
privatefinal MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
...
Note) 해결 방안
- 누군가가 클라이언트인 "OrderServiceImpl"에 "DiscountPolicy"의 구현 객체를 대신 생성하고 주입해줘야한다.
3.3 관심사의 분리
- AppConfig클래스 만든다. 애플리케이션 전체에 대해 환경 설정하고 구성한다는 뜻이다.
-> AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
-> AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결) 해준다. => 'injection'
-> MemberServiceImpl은 MemoryMemberRepository에서 쓴다.
- MemberServiceImpl 클래스를 다음과 같이 수정한다.
-> MemberRepository변수를 선언만하고 생성은 하지 않는다.
-> 클래스의 생성자를 만든다. 생성자를 통해 MemberRepository에 뭐를 넣을지 정할 예정이다.
- 할인 정책을 바꾸면 AppConfig의 discountPolicy 의 반환값만 RateDiscountPolicy로 바꾸면 된다.
- OrderServiceImpl은 바꿀 필요가 없다.
- OrderApp 클래스 수정
java
열기
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
publicclassOrderApp{
publicstaticvoidmain(String[] args){
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService =appConfig.orderService();
Long memberId = 1L;
Member member = new Member(memberId, "itemA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order.toString());
}
}
Note) 실행결과
3.6 전체 흐름 정리
새로운 할인 정책 개발
새로운 할인 정책 적용과 문제점 =>클라이언트가 DiscountPolicy 뿐만 아니라 'FixDisountPolicy'에도 의존 => 'DIP'위반
관심사의 분리 => 'AppConfig'
AppConfig 리팩토링 - new를 하나만 나오도록한다(중복 제거). 역할과 구현을 분리
새로운 구조와 할인 정책 적용
3.7 좋은 객체 지향 설계의 5가지 원칙의 적용
SRP - 단일 책임 원칙(1 클래스 - 1 책임)
구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당한다.
클라이언트 객체를 실행하는 역할만 담당
DIP - 의존관계 역전 원칙(개발자는 구체화가 아닌 추상화에 의존하도록)
AppConfig가 'FixDiscountPolicy' 객체 인스턴스를 클라이언트 코드대신 생성해서 클라이언트 코드에 의존 관계를 주입해서 DIP를 지킬 수 있다.
OCP - 개방 폐쇄 원칙
애플리케이션을 사용 / 구성 영역으로 나눈다.
소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있어야 한다.
3.8 IoC, DI, 컨테이너
IoC(Inversion Of Control) - 제어의 역전
기존의 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행했다. 즉, 구현 객체가 프로그램의 제어 흐름을 스스로 컨트롤했다.
반면에, AppConfig를 사용하면 구현 객체는 자신의 로직을 실행하는 역할만 담당한다.
ex) 프로그램을 제어하는 권한은 모두 AppConfig가 가지고 있다. OrderServiceImpl 또한 AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl 가 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수 있다. 그런 사실과 무관하게 OrderServiceImpl는 자신의 로직을 실행한다.
이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다.
DI(Dependency Injection) - 의존 관계 주입
OrderServiceImpl은 DiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지 모른다.
의존 관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야한다.
정적인 클래스 의존 관계: import문만 봐도 분석 가능
동적인 객체(인스턴스) 의존 관계: 애플리케이션을 실행하고나서 분석 가능하다.
프레임워크(Framework) vs 라이브러리(Library)
프레임워크가 내 코드를 제어하고 대신 실행하면 프레임워크가 맞다. ex) JUnit
내 코드가 제어의 흐름을 직접 담당하는 경우는 라이브러리
클래스 다이어그램
객체 다이어그램
객체 다이어그램
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 되는 것을 의존관계 주입이라고 한다.
객체 인스턴스를 생성하고 그 참조값을 전달해서 연결된다.
의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경 가능하다.
IoC 컨테이너, DI 컨테이너
AppConfig 처럼 객체를 생성하고 관리하면서 의존 관계를 연결해주는 것을 'IoC 컨테이너' 또는 'DI 컨테이너' 라고 한다.
최근에는 주로 'DI 컨테이너'라고 많이 한다.
3.9 스프링으로 전환하기
Ex) 지금까지 자바 프로젝트를 만든 것과는 다르게 스프링을 사용해보자
- AppConfig 클래스에 @Configuration어노테이션을 붙인다. => 애플리케이션의 설정 정보를 담당한다.
-> AppConfig 하위의 각 메서드에 @Bean을 붙인다.
-> @Bean을 붙이면 그 메서드들이 모두 스프링 컨테이너에 모두 빈으로 등록된다. 기본적으로 메서드 이름(key)으로 등록된다. 메서드 반환값은 value이다.
-> 또한, @Bean(name="메서드명 대신 붙이고 싶은 이름")을 이용해서 객체의 이름을 정할 수도 있다.