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;
}
}
}
💻 Test
ctrl + shift + T : Create Test
📒 회원 등급이 VIP인 경우
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다.")
void vip_o() {
// given
Member member = new Member(1L, "memberVIP", Grade.VIP);
// when
int discount = discountPolicy.discount(member, 10000);
// then
Assertions.assertThat(discount).isEqualTo(1000);
}
실행 결과 : SUCCESSFUL
📘 회원 등급이 BASIC인 경우
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x() {
// given
Member member = new Member(1L, "memberBASIC", Grade.BASIC);
// when
int discount = discountPolicy.discount(member, 10000);
// then
Assertions.assertThat(discount).isEqualTo(1000);
}
실행 결과 : FAILED
💡 실패 test도 해야 한다!!!
📖 새로운 할인 정책 적용과 문제점
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
새로운 할인 정책을 변경하려는 경우, 할인 정책의 클라이언트인 OrderServiceImpl의 코드가 변경되어야 한다.
❓ DIP, OCP를 준수하고 있다고 생각했지만 실상 그게 아니다.
DIP : 구현체가 아닌 인터페이스에 의존해야 한다.
인터페이스인 DiscountPolicy와 구현체인 FixDiscountPolicy, RateDiscountPolicy에 의존하고 있다.
OCP : 소프트웨어의 확장에는 열려있어야 하지만 변경에는 닫혀있어야 한다.
구현체를 변경하려는 경우 코드를 변경해야 한다.
💡 해결 방법
DIP
OrderServiceImpl에 DiscountPolicy 구현 객체를 대신 생성하고 주입해주어야 한다.
📖 관심사의 분리 🌟🌟🌟
💻 AppConfig
애플리케이션의 전체 동작 방식을 구성하기 위해,구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스
이전에는 구현체가 의존하는 인터페이스의 구현 객체를 직접 선택했다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
😎 이러한 선택을 앞으로는 전적으로 AppConfig가 한다!!!
AppConfig는 애플리케이션의 실제 동작에 필요한구현 객체를 생성한다.
MemberServiceImpl
MemoryMemberRepository
OrderServiceImpl
FixDiscountPolicy
AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를생성자를 통해 주입해준다.
MemberServiceImpl이 직접 구현 객체 MemoryMemberRepository를 선택 ▶ MemberServiceServiceImpl이 MemberRepository 인터페이스에만 의존
OrderServiceImpl이 직접 구현 객체인 MemoryMemberRepository와 FixDiscountPolicy를 선택 ▶ OrderSErviceImpl이 MemberRepository와 DiscountPolicy 인터페이스에만 의존
💡 생성자 주입과 의존관계 주입
AppConfig입장 생성자를 통해 구현 객체를 선택하여 생성, 주입해주어 의존관계를 연결, 실행해준다.
클라이언트입장 의존관계를 외부에서 주입해주기 때문에 실행에만 집중할 수 있다.
💻 관심사의 분리
객체의 생성과 연결은 AppConfig가 담당
DIP 완성 : 구현 클래스는 인터페이스에만 의존하면 된다!
객체를 생성하고 연결, 실행하는 역할이 명확히 분리됨
◾ MemberService의 구현 객체 주입 (repository 구현 객체 주입)
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
Appconfig가 구체 클래스를 선택하여 애플리케이션이 어떻게 동작할지 전체 구성을 책임진다.
📖 AppConfig 리팩터링
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
중복 제거 : 구현체를 변경할 경우 해당 부분의 코드만 변경
역할 구분 : 애플리케이션의 전체 구성을 빠르게 파악 가능
📖 새로운 구조와 할인 정책 적용
AppConfig가 객체의 생성과 연결을 담당하면서 사용 영역과 구성 영역이 분리됨 ▶ 사용 영역의 변경 없이 구성 영역의 변경만으로 작동 방법을 변경할 수 있음
📖 좋은 객체 지향 설계의 5가지 원칙 적용
우리는 5가지 원칙 중 SRP, DIP, OCP 적용!
◾ SRP : 단일 책임 원칙
한 클래스는 하나의 책이만 가져야 한다.
◾ DIP : 의존관계 역전 원칙
프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.
◾ OCP : 개방-폐쇄 원칙
소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.
📖 IoC, DI, 그리고 컨테이너
💻 제어의 역전, IoC (Inversion of Control)
프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것
💥 프레임워크 vs 라이브러리
프레임워크
프레임워크가 코드를 제어하고 대신 실행 (예. JUnit)
라이브러리
코드가 직접 제어 흐름을 담당
💻 의존관계 주입, DI(Dependency Injection)
애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을의존관계 주입이라 한다.
🌟
클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
정적인클래스 의존 관계와 실행 시점에 결정되는동적인객체(인스턴스) 의존 관계를 분리해서 생각해야 한다.
📒 정적인 의존관계 확인 : 클래스 다이어그램
📘 동적인 의존관계 확인 :
💻 IoC 컨테이너, DI 컨테이너
AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을IoC 컨테이너또는DI 컨테이너라고 한다.
📖 스프링으로 전환하기
💻 스프링 컨테이너
스프링 컨테이너 : ApplicationContext
스프링 컨테이너는@Configuration이 붙은 AppConfig를 설정(구성)정보로 사용한다.
AppConfig에@Bean이라 적힌 메서드를 모두 호출해 반환된 객체를 스프링 컨테이너에 등록한다. (스프링 빈)