[되새김질] 스프링 기본 - 스프링 핵심 원래 이해#2

jeyong·2023년 7월 29일
0

- 해당 게시물은 인프런 "스프링 핵심 원리 - 기본편" 강의를 참고하여 작성한 글 입니다.

스프링 핵심 원리 - 기본편

1. 새로운 할인 정책 개발

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);

2. 새로운 할인 정책 적용과 문제점

할인 정책을 적용하면 DIP, OCP를 못 지키는 문제가 발생한다.

1) 방금 추가한 정률% 할인 정책을 적용하자
할인 정책을 적용하려면 아래와 같이 클라이언트인 OrderServiceImpl 코드를 고쳐야한다.

2)문제점 발견

  • 우리는 역할과 구현을 충실하게 분리했다. OK
  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. OK
  • OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다
    - 그렇게 보이지만 사실은 아니다.
    - DIP: 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP 지킨 것 같은데?
    - 클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
    - 추상(인터페이스) 의존: DiscountPolicy
    - 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
    - OCP: 변경하지 않고 확장할 수 있다고 했는데!

-> 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다.

즉, 정리하자면 DIP를 위반하면 OCP를 위반하게 된다.

3) 문제 해결
원래 의도했던 것처럼 추상에만 의존하도록 변경하자. 클라이언트가 인터페이스에만 의존하도록 하고 싶다.

이전 코드

수정 코드

-> 이제 인터페이스에만 의존하게 됐다
But.. 주문 생성 테스트를 돌려보면 NPE(NullPointerException) 발생.

5) 해결방안
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다!

3. 관심사의 분리

  • 애플리케이션을 하나의 공연이라 생각해보자.
  • 각각의 인터페이스를 배역(배우 역할)이라 생각하자.
  • 여기서 배역은 누가 선택할까 ? 로미오 역할을 누가할지, 줄리엣 역할을 누가할지 배우들이 정하는 걸까?
  • 줄리엣 역할을 누가 맡을지는 배우들이 정하지 않는다. 섭외 담당이 따로 있어야 한다.
  • 그런데 이전 코드는 마치 로미오 역할(인터페이스)을 하는 디카프리오(구현체)가 줄리엣 역할을 할 스칼렛 요한슨(구현체)를 직접 섭외하는 것과 같다.
    - 배우가 연기만 해야하는데 섭외까지 맡아버리는 다양한 책임을 가지고 있다.
    - "구현체가 줄리엣 역할을 할 구현체를 직접 섭외" 한다는 것이 어떤 코드를 말하는거지?
    -> = OrderServiceImpl(구현체)가 RateDiscountPolicy(구현체)를 직접 선택하고 있다.


즉 정리하자면 구현체가 구현체를 선택하도록 구현하지말자는 것이다.

  • 배우는 배역만 수행하고, 공연 기획자가 나와서 배우를 지정하자!
  • 배우는 배역을 수행하는 것에만 집중해야 한다.
  • 디카프리오는 김태희가 와도 전지현이 와도 똑같이 공연할 수 있어야한다. 구현체에 상관없이 역할을 수행해야 한다.
  • 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 나올 시점이다.

구현 객체를 생성하고 연결하는 공연 기획자 만들기

애플리케이션의 전체 동작 방식을 구성(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 구현체는 어떤 구현체가 주입되는지 알 필요 없이, 인터페이스에만 의존하며 자신의 역할만 수행하게 된다.

공연 기획자 AppConfig 를 도입해서 DIP를 완성했다

1) 공연 기획자 AppConfig 역할 : 생성자 주입
1. 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
2. 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)한다.

2) AppConfig 효과 : 의존관계에 대한 고민을 외부에 맡기고 구현체는 실행에만 집중할 수 있게 됬다.
"구현체는 인터페이스에 맞춰서 기능 실행에만 호출할꺼야"
이제 MemberServiceImpl 구현체는 MemberRepository 인터페이스에만 의존하게 됬다.
OrderServiceImpl 구현체 은 MemberRepository, DiscountPolicy 인터페이스에만 의존하게 됬다.

AppConfig 를 넣은 클래스 다이어그램과 객체 다이어그램

1) 클래스 다이어그램 : 객체의 생성과 연결은 AppConfig가 담당한다.

-> 관심사의 분리 : 객체를 생성하고 연결하는 역할과, 실행하는 역할이 명확히 분리된다. DIP 완성

2) 회원 객체 다이어그램 : 클라이언트인 memberServiceImpl 입장에서 바라보자.

  • 클라이언트 구현체는 어떤 레포지토리 구현체가 들어올지, 어떤 할인 정책 구현체가 생성될지 모른다.
  • AppConfig가 구현체 생성 및 연결을 해주기 외부에서 다 해주기 때문이다.
  • 클라이언트 입장에서는 의존관계를 마치 외부에서 주입해주는것 같다고 해서 의존관계 주입(Dependency Injection)이라고 표현한다.

3) 정리

  • AppConfig를 통해서 관심사를 확실하게 분리했다.
  • 배역, 배우를 생각해보자.
  • AppConfig는 공연 기획자다.
  • AppConfig는 구체 클래스를 선택한다. 배역에 맞는 담당 배우를 선택한다. 애플리케이션이 어떻게
  • 동작해야 할지 전체 구성을 책임진다.
  • 이제 각 배우들은 담당 기능을 실행하는 책임만 지면 된다.
  • OrderServiceImpl 은 기능을 실행하는 책임만 지면 된다
profile
천천히 잊어가기

1개의 댓글

comment-user-thumbnail
2023년 7월 29일

공감하며 읽었습니다. 좋은 글 감사드립니다.

답글 달기