- 해당 게시물은 인프런 "스프링 핵심 원리 - 기본편" 강의를 참고하여 작성한 글 입니다.
1) 기획자가 새로운 할인 정책을 요청한다.
기획자: "서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 정률% 할인으로 변경하고 싶어요."
순진 개발자(우리들): "제가 처음부터 고정 금액 할인은 아니라고 했잖아요."
악덕 기획자: "애자일 소프트웨어 개발 선언 몰라요? '계획을 따르기보다 변화에 대응하기를'"
순진 개발자: ... (하지만 난 유연한 설계가 가능하도록 객체지향 설계 원칙을 준수했지 후후)
2) 정률 %할인으로 정책을 만들자. RateDiscountPolicy() 구현
DiscountPolicy 인터페이스의 구현체 RateDiscountPolicy()를 만든다.
discount() 메서드를 작성하고 10% 할인 가격을 제대로 계산하는지 JUnit테스트 코드를 작성하자.
3) JUnit 테스트
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다.")
void vip_o() {
//given
Member member = new Member(1L, "memberVIP", Grade.VIP);
//when
int discount = discountPolicy.discount(member, 10000);
//then
assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x() {
//given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
//when
int discount = discountPolicy.discount(member, 10000);
//then
assertThat(discount).isEqualTo(0);
}
}
아래 사진과 같이 static import 해서 assertThat을 간결하게 쓰자.
// 적용 전 Assertions.assertThat(discount).isEqualTo(1000); // 적용 후 assertThat(discount).isEqualTo(1000);
할인 정책을 적용하면 DIP, OCP를 못 지키는 문제가 발생한다.
1) 방금 추가한 정률% 할인 정책을 적용하자
할인 정책을 적용하려면 아래와 같이 클라이언트인 OrderServiceImpl 코드를 고쳐야한다.
2)문제점 발견
-> 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다.
즉, 정리하자면 DIP를 위반하면 OCP를 위반하게 된다.
3) 문제 해결
원래 의도했던 것처럼 추상에만 의존하도록 변경하자. 클라이언트가 인터페이스에만 의존하도록 하고 싶다.
이전 코드
수정 코드
-> 이제 인터페이스에만 의존하게 됐다
But.. 주문 생성 테스트를 돌려보면 NPE(NullPointerException) 발생.
5) 해결방안
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다!
즉 정리하자면 구현체가 구현체를 선택하도록 구현하지말자는 것이다.
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만들자.
1) MemberServiceImpl 구현체가 Repository 구현체를 선택하고 있는 문제를 해결하자
MemberServiceImpl 에서 레포지토리를 MemoryMemberRepository()를 직접 선택하고 있다.
2) AppConfig가 구현 객체 MemoryMemberRepository 를 생성한다. 그리고 참조를 MemberServiceImpl 생성자에 연결해준다.
3) MemberServiceImpl 의 생성자 만들기
MemberServiceImpl의 생성자를 통해 MemberRepository의 구현체를 생성하도록 변경한다.
이렇게 되면 MemberServiceImpl 구현체가 MemberRepository 인터페이스에만 의존하게 된다.
4) OrderServiceImpl 구현체가 Repository 구현체와 DiscountPolicy 구현체를 선택하고 있는 문제를 해결하자
5) AppConfig 가 실제 동작에 필요한 FixDiscountPolicy 구현 객체를 생성한다.
구현체의 참조를 OrderServiceImpl의 생성자에 연결(주입)해준다.
6) OrderServiceImpl 의 생성자 만들기
이제 OrderServiceImpl 구현체는 어떤 구현체가 주입되는지 알 필요 없이, 인터페이스에만 의존하며 자신의 역할만 수행하게 된다.
1) 공연 기획자 AppConfig 역할 : 생성자 주입
1. 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
2. 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)한다.
2) AppConfig 효과 : 의존관계에 대한 고민을 외부에 맡기고 구현체는 실행에만 집중할 수 있게 됬다.
"구현체는 인터페이스에 맞춰서 기능 실행에만 호출할꺼야"
이제 MemberServiceImpl 구현체는 MemberRepository 인터페이스에만 의존하게 됬다.
OrderServiceImpl 구현체 은 MemberRepository, DiscountPolicy 인터페이스에만 의존하게 됬다.
1) 클래스 다이어그램 : 객체의 생성과 연결은 AppConfig가 담당한다.
-> 관심사의 분리 : 객체를 생성하고 연결하는 역할과, 실행하는 역할이 명확히 분리된다. DIP 완성
2) 회원 객체 다이어그램 : 클라이언트인 memberServiceImpl 입장에서 바라보자.
3) 정리
공감하며 읽었습니다. 좋은 글 감사드립니다.