현재는 정액 할인 정책을 채택하여 DiscountPolicy를 구현하고 있는데, 정률 할인 정책을 채택하여 DiscountPolicy를 변경하려한다.
다행히 객체지향적으로 설계해서 유연하게 변화에 대응가능하다.
RateDiscountPolicy 클래스 생성
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
public class RateDiscountPolicy implements DiscountPolicy{
private final int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price * discountPercent / 100;
}
else{
return 0;
}
}
}
테스트 코드 작성
테스트 코드 작성 팁 : command + shift + T 단축키 사용
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 rateDiscountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10프로 할인이 적용되어야 한다.")
void vip_o() {
//given
Member member = new Member(1L, "test", Grade.VIP);
//when
int discount = rateDiscountPolicy.discount(member, 10000);
//then
Assertions.assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("일반 회원은 비율할인이 적용되어야지 않아야 한다.")
void vip_x() {
//given
Member member = new Member(2L, "test2", Grade.BASIC);
//when
int discount = rateDiscountPolicy.discount(member, 10000);
//then
Assertions.assertThat(discount).isEqualTo(0);
}
}
새로운 할인 정책 적용과 문제점
RateDiscountPolicy를 적용하기위해 OrderServiceImpl의 코드를 아래와 같이 변경해야 한다.
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
해당 코드의 문제점 찾기
역할(interface)과 구현(class)는 충실하게 분리하여 구현하였다.
그러면 OCP,DIP를 지키고 있는가?
DIP : OrderServiceImpl는 DiscountPolicy 인터페이스에 의존하는 동시에 RateDiscountPolicy 구현체에도 의존하고 있기에 DIP를 위반한다.
OCP : 코드의 변경없이 기능의 확장이 가능해야하는데 RateDiscountPolicy를 확장하기 위해서 코드의 변경이 불가피하다.따라서 OCP도 위반한다.
예상했던 의존관계실제 의존관계
의존성 문제해결
아래처럼 인터페이스만을 의존하도록 코드를 바꾸면, NPE(null point exception)이 발생한다. 그래서 옳은 방법은 아니다.
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
NPE 해결방안
관심사의 분리를 해야한다.
예를 들어, 해당 애플리케이션을 하나의 공연이라 하자. 그러면 크게 공연기획자, 배우, 배역이 존재 할 것이다.
공연기획자의 경우 어떠한 배역에 누구를 쓸지 결정한다. 즉 해당 배역을 맡을 배우를 결정할 수 있다.
하지만 위의 DIP와 OCP를 위반하는 코드의 경우 배우가 상대역 배우를 결정하는 구조이다.
코드로 설명하자면 OrderServiceImpl구현체가 RateDiscountPolicy 구현체를 선택하고 있다. 배우가 배우를 고르는 셈이다.
그래서 우리는 공연기획자, 즉 이러한 의존관계를 위한 새로운 클래스가 필요하다.
해당 역할을 바로 AppConfig 클래스가 수행한다.
애플리케이션과 공연을 빗대어서 매칭하면 공연기획자-AppConfig, 배우-구현체, 배역-인터페이스로 볼 수있다.
배우는 배우의 역할, 즉 배역만 수행하도록 하고 공연기획자는 배역에 누구를 쓸지를 정하면 서로의 관심사를 가장 효율적으로 분리한 구조라 볼 수 있다.
AppConfig 구현
hello.core 패키지에 AppConfig 클래스를 생성한다.
생성자를 통해 해당 인터페이스가 어떠한 구현체를 리턴 할지를 결정해준다.
즉, 배역에 들어갈 배우를 결정하는 과정이다.
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
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());
}
}
MemberServiceImpl 생성자 주입
오직 인터페이스 memberRepository만 의존하도록 코드 수정이 가능하다.
MemberServiceImpl의 입장에서 본인이 어떠한 클래스를 구현체로 받아들이지는(주입 될지)전혀 모른다.
이는 오직 외부에서 결정되고 주입된다(AppConfig로부터!)
package hello.core.member;
public class MemberServiceImpl implements MemberService {
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의 클래스 다이어그램회원 객체 인스턴스 다이어그램
OrderServiceImpl 생성자 주입
인터페이스를 두개를 의존하니, AppConfig의 생성자 설정에서도 구현체 두개를 리턴하도록 하면된다.
MemberRepository와 DiscountPolicy에 DI가 진행된다.
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);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
테스트 코드 수정
@BeforeEach는 테스트 실행하기 전, 해당 메서드가 실행되도록 하는 어노테이션이다.
class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
}
class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
} }
해당 글은 김영한님의 <스프링 핵심> 강좌를 기반으로 작성되었습니다.
You might also like...
5월
26
<Spring> 우리는 왜 MVC를 사용할까?
MVC의 등장 배경자바의 JSP와 서블릿만을 이용해서 웹서비스를 개발해보신 경험이 있으신가요?JSP의 경우 아래와 같은 코드 구조를 가지게 됩니다.굉장히 간단한
8 min read
5월
20
<Spring> 서블릿 사용해보기
해당 포스팅은 스프링 배우기 전 알아두면 좋은 개념과 연속적으로 진행되는 포스팅입니다. 프로젝트 설정하기우선 https://start.spring.io/으로 접속해서 아래와
15 min read
5월
19
<Spring> 스프링 배우기 전 알아두면 좋은 개념
Web Application Server vs Web ServerWeb Server : 단순한 정적인 리소스들 화면에 뿌려줄 수 있습니다.대표적인 예로 Apache,Nginx 등이 존재합니다.
6 min read
2월
22
<Spring> 스프링에서 왜 DI가 중요할까?
우선 DI를 파보기 전에 스프링이 뭔지에 대해서 알아봅시다. 스프링이라 함은 오픈 소스의 경량 웹 프레임워크라고 정의되어있습니다.즉,개발자들이 웹 개발을
5 min read
1월
26
Springboot 프로젝트에 MS Azure MySQL 연결하기
최근 AWS 과금으로 인해서 어느 클라우드 디비를 사용할까 하다가 Azure가 학생용 요금이 따로 존재한다는 것을 보고 선택했습니다.(사실 AWS도 학생용이