목표
1. 이전 시간에 만든 예제 프로젝트에 객체 지향 원리를 적용한다.
2. '의존관계 주입'이 필요한 이유를 배운다.
'객체 지향 원리 적용' 목차
1. 새로운 할인 정책 개발 (이번 포스팅)
2. 새로운 할인 정책 적용과 문제점
3. 관심사의 분리 (DI)
4. AppConfig 리팩터링
5. 새로운 구조와 할인 정책 적용
6. 전체 흐름 정리
7. 좋은 객체 지향 설계의 5가지 원칙의 적용
8. IoC, DI, 그리고 컨테이너
9. 스프링으로 전환하기
1. 새로운 할인 정책 개발
1) 기획자가 새로운 할인 정책을 요청한다.
기획자: "서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 정률% 할인으로 변경하고 싶어요."
순진 개발자(우리들): "제가 처음부터 고정 금액 할인은 아니라고 했잖아요."
악덕 기획자: "애자일 소프트웨어 개발 선언 몰라요? '계획을 따르기보다 변화에 대응하기를'"
순진 개발자: ... (하지만 난 유연한 설계가 가능하도록 객체지향 설계 원칙을 준수했지 후후)
2) 정률 %할인으로 정책을 만들자. RateDiscountPolicy() 구현
DiscountPolicy 인터페이스의 구현체 RateDiscountPolicy()를 만든다.
discount() 메서드를 작성하고 10% 할인 가격을 제대로 계산하는지 JUnit테스트 코드를 작성하자.
[ JUnit 테스트 생성 Tip ]
RateDiscountPolicy()의 discount()의 테스트 코드를 작성하고 싶다.
discount() 함수에 커서를 클릭하고, command + Shift + T 를 누르면 "Create New Test" 가 뜬다!
클릭하면 클래스 이름에 'Test'를 붙여서 자동으로 만들어져 있다. "OK" 를 누르고 JUnit 테스트 파일을 만들자.
3) 성공 케이스, 실패 케이스를 만들어서 확인한다.
성공 케이스 : VIP등급이 만원을 주문했을 때, 천원을 할인해주어야 성공하는 케이스 [성공을 기대]
실패 케이스 : BASIC 등급이 만원을 주문했을 때, 천원을 할인해주면 실패하는 케이스 [실패를 기대]
BASIC 등급은 할인이 없다.
걀과 값이 0원인지 확인해야 하는데 일부러 '실패'를 발생시키는 케이스다.
테스트가 실패하면, JUnit이 Expected 값과 Actual 값을 비교해준다.
우리는 Actual 이 0원인지 알지만, 일부러 Expected 를 1000이라고 넣었다.
[ JUnit Assertions Tip ]
static import 해서 assertThat을 간결하게 쓰자.
// 적용 전
Assertions.assertThat(discount).isEqualTo(1000);
// 적용 후
assertThat(discount).isEqualTo(1000);
2. 새로운 할인 정책 적용과 문제점
할인 정책을 적용하면 DIP, OCP를 못 지키는 문제가 발생한다.
문제를 해결하는 (관심사의 분리, AppConfig 리팩터링, 새로운 구조 적용)과정에서 스프링 컨테이너가 탄생한 이유를 이해하게 된다.
1) 방금 추가한 정률% 할인 정책을 적용하자
할인 정책을 적용하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야한다.
[ OK ] 역할과 구현을 충실하게 분리했다.
[ OK ] 다형성도 활용하고 인터페이스와 구현 객체를 분리했다.
[ Fail ] OCP, DIP 같은 객체지향 설계 원칙을 준수했..나?
-> 그렇게 보이지만 사실은 아니다. 주문서비스 클라이언트 OrderServiceImpl 코드를 보자.
2) 문제점 발견
OrderServiceImpl이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 구체 클래스도 함께 의존하고 있다.
단순히 DiscountPolicy 인터페이스만 의존한다고 생각했었는데. 실제로는 구체 클래스도 의존하고 있었다.
-> DIP 위반
"DIP :구현체에 의존하지 말고, 인터페이스에만 의존해야 한다."
[ DIP를 위반하면 어떤 문제가 생기지? ]
고정 할인 정책을 정률 할인 정책으로 바꿔주세요
-> FixDiscountPolicy 를 RateDiscountPolicy 로 변경. 즉, DIP 위반
-> 클라이언트인 OrderServiceImpl 코드를 고쳐야한다. -> OCP를 위반하게 된다.
3) 어떻게 문제를 해결할 수 있을까?
원래 의도했던 것처럼 추상에만 의존하도록 변경하자. 클라이언트가 인터페이스에만 의존하도록 하고 싶다.
이전 코드
변경 코드 "좋아! 인터페이스에만 의존하게 됬어." 깃헙 코드
4) 그런데. 구현체가 없는데 어떻게 코드가 돌아가지?
주문 생성 테스트를 돌려보면 NPE(NullPointerException) 발생.
5) 해결방안
누군가 OrderServiceImple 클라이언트에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다!
[ 중요 ] 3. 관심사의 분리
애플리케이션을 하나의 공연이라 생각해보자.
각각의 인터페이스를 배역(배우 역할)이라 생각하자.
여기서 배역은 누가 선택할까 ? 로미오 역할을 누가할지, 줄리엣 역할을 누가할지 배우들이 정하는 걸까?
줄리엣 역할을 누가 맡을지는 배우들이 정하지 않는다. 섭외 담당이 따로 있어야 한다.
그런데 이전 코드는 마치 로미오 역할(인터페이스)을 하는 디카프리오(구현체)가 줄리엣 역할을 할 스칼렛 요한슨(구현체)를 직접 섭외하는 것과 같다.
배우가 연기만 해야하는데 섭외까지 맡아버리는 다양한 책임을 가지고 있다.
"구현체가 줄리엣 역할을 할 구현체를 직접 섭외" 한다는 것이 어떤 코드를 말하는거지?
= OrderServiceImpl(구현체)가 RateDiscountPolicy(구현체)를 직접 선택하고 있다.
디카프리오 왈 : "'줄리엣 역할'에는 스칼렛 요한슨이 해주세요."
배우는 배역만 수행하고, 공연 기획자가 나와서 배우를 지정하자!
배우는 배역을 수행하는 것에만 집중해야 한다.
디카프리오는 김태희가 와도 전지현이 와도 똑같이 공연할 수 있어야한다. 구현체에 상관없이 역할을 수행해야 한다.
배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 나올 시점이다.
구현 객체를 생성하고 연결하는 공연 기획자 만들기
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만들자.
1) MemberServiceImpl 구현체가 Repository 구현체를 선택하고 있는 문제를 해결하자
MemberServiceImpl 에서 레포지토리를 MemoryMemberRepository()를 직접 선택하고 있다.
2) AppConfig가 구현 객체 MemoryMemberRepository 를 생성한다.
그리고 참조를 MemberServiceImpl 생성자에 연결해준다.
3) MemberServiceImpl 의 생성자 만들기
MemberServiceImpl의 생성자를 통해 MemberRepository의 구현체를 생성하도록 변경한다.
이렇게 되면 MemberServiceImpl 구현체가 MemberRepository 인터페이스에만 의존하게 된다.
DIP를 지키게 되었다. Good!
주문 쪽 구현체의 문제도 해결하러가자
4) OrderServiceImpl 구현체가 Repository 구현체와 DiscountPolicy 구현체를 선택하고 있는 문제를 해결하자
구현체가 인터페이스에만 의존하도록 바꾸자.
5) AppConfig 가 실제 동작에 필요한 FixDiscountPolicy 구현 객체를 생성한다.
구현체의 참조를 OrderServiceImpl의 생성자에 연결(주입)해준다.
6) OrderServiceImpl 의 생성자 만들기
이제 OrderServiceImpl 구현체는 어떤 구현체가 주입되는지 알 필요 없이, 인터페이스에만 의존하며 자신의 역할만 수행하게 된다.
DIP를 지키게 되었다. Good!
자.. 이제 정리해보자.
공연 기획자 AppConfig 를 도입해서 DIP를 완성했다
7) 공연 기획자 AppConfig 역할 : 생성자 주입
1. 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
2. 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)한다.
8) AppConfig 효과 : 의존관계에 대한 고민을 외부에 맡기고 구현체는 실행에만 집중할 수 있게 됬다.
"구현체는 인터페이스에 맞춰서 기능 실행에만 호출할꺼야"
이제 MemberServiceImpl 구현체는 MemberRepository 인터페이스에만 의존하게 됬다.
OrderServiceImpl 구현체 은 MemberRepository, DiscountPolicy 인터페이스에만 의존하게 됬다.
AppConfig 를 넣은 클래스 다이어그램과 객체 다이어그램
9) 클래스 다이어그램 : 객체의 생성과 연결은 AppConfig가 담당한다.
관심사의 분리 : 객체를 생성하고 연결하는 역할과, 실행하는 역할이 명확히 분리된다. DIP 완성
10) 회원 객체 다이어그램 : 클라이언트인 memberServiceImpl 입장에서 바라보자.
클라이언트 구현체는 어떤 레포지토리 구현체가 들어올지, 어떤 할인 정책 구현체가 생성될지 모른다.
AppConfig가 구현체 생성 및 연결을 해주기 외부에서 다 해주기 때문이다.
클라이언트 입장에서는 의존관계를 마치 외부에서 주입해주는것 같다고 해서 의존관계 주입(Dependency Injection)이라고 표현한다.
11) AppConfig 넣고 테스트 코드 작성
테스트 코드에 AppConfig를 생성하고, AppConfig가 인터페이스에 의존관계를 주입한다.
가입, 주문 생성 모두 테스트 성공이다.
다음 강의에서는 AppConfig를 리팩토링한다.
공부 내용 출처 : 스프링 핵심 원리 기본편
'프로그래밍 > Spring Basic' 카테고리의 다른 글
6. 객체 지향 원리 적용 - IoC, DI, 그리고 컨테이너 (0) | 2021.12.21 |
---|---|
5. 객체 지향 원리 적용 - AppConfig 리팩터링 (0) | 2021.12.21 |
3. 예제 프로젝트 만들기 (0) | 2021.12.20 |
2. 객체 지향 설계와 스프링 - 좋은 객체 지향 설계의 5가지 원칙 (SOLID) (0) | 2021.12.20 |
1. 객체 지향 설계와 스프링 - 역할과 구현을 분리하기 (0) | 2021.12.20 |