새로운 할인 정책을 확장해보자!
우리가 열심히 개발을 하고 있는데, 요구사항을 자꾸 변경하는 😡악덕기획자😡가 와서는,
서비스 오픈 직전에 할인 정책을 주문 금액당 퍼센트로 해주는 정률할인으로 변경해달라고 요구!
순진개발자 : 제가 첨부터 고정 할인 금액은 아니라고 했잖아요🤬
악덕기획자 : ㅇㅉㄹㄱ 변화에 대응하셈
순진개발자 : (Ha 니가 그럴 줄 알았다ㅋ 그럴 줄 알고 난 객체지향 설계 원칙을 준수했음 ㅅㄱ)
❗️순진 개발자가 정말 객체지향 설계 원칙을 잘 준수했는지 확인해보고, 이번엔 주문한 금액의 %를 할인해주는 새로운 정률할인 정책을 추가하자❗️
기존에 있었던 것들은 건들지 않고 RateDiscountPolicy 만 새로 개발하면 된다!
hello.core/discount/RateDiscountPolicy.java
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
public class RateDiscountPolicy implements DiscountPolicy {
// 10% 할인
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
// 멤버 등급이 VIP인 경우에
if(member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
💡
command + shift + t
를 누르면 바로 테스트 클래스를 만들 수 있다!
test/../hello.core/discount/RateDiscountPolicyTest.java
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;
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다.")
// VIP 라면
void vip_o() {
// given
// 임의의 멤버 하나 생성
Member member = new Member(1L, "memberVIP", Grade.VIP);
// when
// 10000원 일 때는 1000이 할인되어야 함
int discount = discountPolicy.discount(member, 10000);
// then
// Assertions(org.assertj.core.api.Assertions)
// 할인 금액이 1000이 맞는지 검증
Assertions.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
Assertions.assertThat(discount).isEqualTo(1000);
}
}
성공 테스트도 중요하지만, 실패 테스트도 중요함❗️
⬆️ 두 가지 경우를 모두 돌려본 결과 👍🏻
이제, 새로 변경된 할인 정책을 적용할 차례❗️
할인정책을 변경하려면 클라이언트인 OrderServiceImpl
코드를 고쳐야 한다.
hello.core/order/OrderServiceImpl.java
코드 수정
.
.
public class OrderServiceImpl implements OrderService {
// 멤버 리포지토리에서 회원을 찾기 위함
private final MemberRepository memberRepository = new MemoryMemberRepository();
// 고정 할인 정책
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// 정률 할인 정책 ( 이 한줄만 새로 추가!! 정말 기가막히게 대단하죠❓)
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
.
.
여기서 문제점 발견🤚🏻
- 우리는 역할과 구현을 충실하게 분리했나? -> 맞다❗️
- 다형성도 활용하고, 인터페이스와 구현 객체를 분리했나? -> 맞다❗️
- OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했나 -> 그렇게 보이지만 사실 아니다❗️
DIP : 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데🤔?
아니^^? 클래스 의존관계를 분석해 봐봐. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있잖아 !- 추상(인터페이스) 의존:
DiscountPolicy
(클라이언트 입장에서 서버)- 구체(구현) 클래스:
FixDiscountPolicy
,RateDiscountPolicy
OCP: 변경하지 않고 확장할 수 있다고 했는데..🙁 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 주는구나! 따라서 OCP를 위반하게 되네!
Q. 왜 클라이언트 코드를 바꿔야 하는 거지🤔?
A. 클래스 다이어그램으로 의존관계를 분석해보자 !
지금까지 단순히
DiscountPolicy
인터페이스만 의존한다고 생각했다.
잘보면 클라이언트인OrderServiceImpl
이DiscountPolicy
인터페이스 뿐만 아니라
FixDiscountPolicy
인 구체 클래스도 함께 의존하고 있다. 실제 코드를 보면 의존하고 있다! -> DIP 위반
💡 중요 : 그래서FixDiscountPolicy
를RateDiscountPolicy
로 변경하는 순간OrderServiceImpl
의 소스 코드도 함께 변경해야 한다! OCP 위반
Q. 그럼 이 문제를 어떻게 해결할 수 있지?
A. 인터페이스에만 의존하도록 설계를 변경하면 돼!
hello.core/order/OrderServiceImpl.java
코드 수정
.
.
public class OrderServiceImpl implements OrderService {
// 멤버 리포지토리에서 회원을 찾기 위함
private final MemberRepository memberRepository = new MemoryMemberRepository();
// 고정 할인 정책
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// 정률 할인 정책 ( 이 한줄만 새로 추가!! 정말 기가막히게 대단하죠❓)
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
.
.
그런데, 그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까❓❓
실제 실행을 해보면 NPE(null pointer exception)가 발생한다❗️❗️
💡 해결 방안
OrderServiceImpl
에 DiscountPolicy
의 구현 객체를 대신 생성하고 주입해주어야 한다.🌟 되게 중요한 이야기 시-작 🌟
💡관심사를 분리하자💡
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자!
✔️ hello.core/Appconfig.java
코드 작성
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
// 어디서든 AppConfig를 통해 멤버 서비스를 불러다 쓸 수 있음
// 생성자 주입
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
// 구체적인 것을 여기서 선택하도록 함
// 생성자 주입
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
MemberServiceImpl
MemoryMemberRepository
OrderServiceImpl
FixDiscountPolicy
MemberServiceImpl
➡️ MemoryMemberRepository
OrderServiceImpl
➡️ MemoryMemberRepository
, FixDiscountPolicy
✔️ hello.core/member/MemberServiceImpl.java
코드 수정 (생성자 주입)
package hello.core.member;
public class MemberServiceImpl implements MemberService{
// 가입을 하고 회원을 찾으려면 앞서 만든 MemberRepository 인터페이스가 필요함
// private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
MemberServiceImpl
은 MemoryMemberRepository
를 의존하지 않는다!MemberRepository
인터페이스만 의존한다.MemberServiceImpl
입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될 지)는 알 수 없다. MemberServiceImpl
의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부 (AppConfig)에서 결정된다.MemberServiceImpl
은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.❗️클래스 다이어그램❗️
- 객체의 생성과 연결은
AppConfig
가 담당한다.- DIP 완성 :
MemberServiceImpl
은MemberRepository
인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다.- 관심사의 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
❗️회원 객체 인스턴스 다이어그램❗️
appConfig
객체는memoryMemberRepository
객체를 생성하고 그 참조값을memberServiceImpl
을 생성하면서 생성자로 전달한다.- 클라이언트인
memberServiceImpl
입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다.
✔️ hello.core/order/OrderServiceImpl.java
코드 수정 (생성자 주입)
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
// 멤버를 찾기
Member member = memberRepository.findById(memberId);
// OrderService 입장에서 할인에 대한 건 잘 모름. 할인에 대한 건 discountPolicy 네가 알아서 해 줘!
// 이것이 바로 단일 체계 원칙! 잘 설계된 것.
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
OrderServiceImpl
은 FixDiscountPolicy
를 의존하지 않는다! 단지 DiscountPolicy
인터페이스만 의존한다.OrderServiceImpl
입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.OrderServiceImpl
의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부(AppConfig)에서 결정한다.OrderServiceImpl
은 이제부터 실행에만 집중하면 된다.OrderServiceImpl
에는 MemoryMemberRepository
, FixDiscountPolicy
객체의 의존관계가 주입된다.✔️hello.core/MemberApp.java
코드 수정
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
public class MemberApp {
public static void main(String[] args) {
// AppConfig를 하나 만들어서 이를 통해 멤버 서비스를 생성
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
// new Member(1L, "memberA", Grade.VIP);
// 이 상태에서 command+option+v 를 누르면 아래의 내용과 같이 자동 생성이 됨
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
// 확인해보기
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find member = " + findMember.getName());
}
}
✔️hello.core/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;
public class OrderApp {
public static void main(String[] args) {
// AppConfig를 사용해 멤버서비스와 오더서비스를 꺼내도록 수정
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order);
// System.out.println("order.calculatePrice = " + order.calculatePrice());
}
}
이제 테스트 코드도 수정해보자❗️
✔️test/../hello.core/member/MemberServiceTest.java
테스트 코드 수정
package hello.core.member;
import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
MemberService memberService;
// @BeforeEach : 각 테스트 시작 전 무조건 실행이 되는 것
@BeforeEach
public void beforeEach() {
// AppConfig를 만들고 멤버서비스 할당해주기
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join() {
// given : 이러이러한 것이 주어졌을 때,
Member member = new Member(1L, "memberA", Grade.VIP);
// when : 이렇게 했을 때,
memberService.join(member);
Member findMember = memberService.findMember(1L);
// then : 이런 결과가 나온다
Assertions.assertThat(member).isEqualTo(findMember);
}
}
✔️test/../hello.core/order/OrderServiceTest.java
테스트 코드 수정
package hello.core.order;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
// @BeforeEach : 각 테스트 시작 전 무조건 실행이 되는 것
@BeforeEach
public void beforeEach() {
// AppConfig를 만들고 멤버서비스, 오더서비스 할당해주기
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder() {
// long을 사용해도 되지만, 이를 쓰면 null을 넣을 수 없음
// (이후에 DB생성시 null을 사용할 수 있기 때문에 미리 고려).
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
// VIP인 경우 할인 금액인 1000원이 맞는지? 검증
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
⬆️ 테스트 실행 결과 👍🏻
✔️ 정리
OrderServiceImpl
은 기능을 실행만 하면 된다.아직.. 안 끝났나요....?
현재 AppConfig를 잘보면 중복도 있고 역할에 따른 구현이 잘 보이지 않는다.
리팩터링을 해 보자 😂...!
우리가 기대하는 그림
역할들이 드러나게 코드를 잘 작성하는 것이 중요하다!
✔️ AppConfig 리팩터링 전
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
✔️ AppConfig 리팩터링 후
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
// 어디서든 AppConfig를 통해 멤버 서비스를 불러다 쓸 수 있음
// 생성자 주입
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
// 생성자 주입
public OrderService orderService() {
// 구체적인 것을 여기서 선택하도록 함
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
private DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
💡 이렇게 리팩토링을 하게 되면,
1) 중복도 사라지고(new MemoryMemberRepository()
),
2) 메서드 명만 봐도 역할이 확실히 드러나고,
3) MemoryMemberRepository
를 다른 구현체로 변경할 때 한 부분만 변경하면 된다 !!
현기증이 나서...요.......😂