인프런 김영한님의 강의 '스프링 핵심 원리 - 기본편'의 강의 내용을 참고하여 작성한 글입니다.
서비스 기획 과정에서 이전에 적용하던 정액할인정책(fixDiscountPolicy)보단 정률할인정책이 적합하다는 판단하에 서비스에 정률할인정책 적용하고자 한다. 객체 지향 원리를 적용하여 서비스의 할인정책을 수정해보자.
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
public class RateDiscountPolicy implements DiscountPolicy{
private int discountRate = 10; // 10% 할인
@Override
public int discount(Member member, int price) {
if(member.getGrade() == Grade.VIP){
return price * discountRate / 100;
}
return 0;
}
}
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;
public class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 가격의 10% 할인이 적용되어야한다.")
void vip_o(){
// given
Member member = new Member(1L, "memberA", Grade.VIP);
// when
int discount = discountPolicy.discount(member, 10000);
// then
Assertions.assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않는다.")
void vip_x(){
// given
Member member = new Member(1L, "memberA", Grade.BASIC);
// when
int discount = discountPolicy.discount(member, 10000);
// then
Assertions.assertThat(discount).isEqualTo(0);
}
}
할인 정책을 변경하려면 클라이언트인 OrderServiceImpl
코드를 고쳐야 한다.
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
OrderServiceImpl)
는 DiscountPolicy
인터페이스에 의존하면서 DIP를 지킨 것 같지만 추상(인터페이스)뿐만 아니라 구체(구현)클래스에도 의존하고 있음을 알 수 있다!DiscountPolicy
FixDiscountPolicy
, RateDiscountPolicy
DiscountPolicy
에만 의존한다고 생각하였지만 아니었다.OrderServiceImpl
을 보면 인터페이스DiscountPolicy
뿐만 아니라 구현 클래스FixDiscountPolicy
와 RateDiscountPolicy
에도 의존함을 알 수 있다. -> DIP 위반FixDiscountPolicy
를 RateDiscountPolicy
로 변경하는 순간 OrderServiceImpl
의 코드도 변경해야한다. -> OCP 위반public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
OrderServiceImpl
에 DiscountPolicy
의 구현 객체를 대신 생성하고 주입해주어야 한다.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());
}
}
MemberServiceImpl
MemoryMemberRepository
OrderServiceImple
FixDiscountPolicy
MemberServiceImpl
-> MemoryMemberRepository
OrderServiceImpl
-> MemoryMemberRespository
, FixDiscountPolicy
참고: 지금은 각 클래스에 생성자가 없어서 컴파일 오류가 발생하므로 바로 다음 코드에서 생성자를 만든다.
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
은 MemoryMemberRepository
를 의존하지 않는다!MemberRepository
인터페이스에만 의존한다.MemberServiceImpl
입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.MemberServiceImpl
의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig
)에서 결정된다.MemberServiceImpl
은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.AppConfig
가 담당한다.MemberServiceImpl
은 MemberRepository
인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다!AppConfig
객체는 memoryMemberRepository
객체를 생성하고 그 참조값을 memberServiceImpl
을 생성하면서 생성자로 전달한다.memberServiceImpl
입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(dependency injection), 의존관계주입 또는 의존성 주입이라고 한다.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);
}
}
OrderServiceImpl
은 FixDiscountPolicy
를 의존하지 않는다!DiscountPolicy
인터페이스에만 의존한다.OrderServiceImpl
입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.OrderServiceImpl
의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig
)에서 결정한다.OrderSerivceImpl
은 이제부터 실행에만 집중하면 된다.OrderServiceImpl
에는 MemoryMemberRepository
, FixDiscountPolicy
객체의 의존관계가 주입된다.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 = new AppConfig();
MemberService memberService = appConfig.memberService();
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("findMember = " + findMember.getName());
}
}
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
public class OrderApp {
public static void main(String[] args) {
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);
}
}
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;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
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);
}
}
package hello.core.order;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder() {
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itmeA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
@BeforeEach
는 각 테스트를 실행하기 전에 호출된다.OrderServiceImpl
은 기능을 실행하는 책임만 지면 된다.현재 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 {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
new MemoryMeberRepository()
이 부분이 중복 제거되었다. 이제 MemoryMemberRepository
를 다른 구현체로 변경할 때 한 부부만 변경하면 된다.AppConfig
를 보면 역할과 구현 클래스가 한 눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.FixDiscountPolicy
-> RateDiscountPolicy
로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다.package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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 {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
AppConfig
에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy
-> RateDiscountPolicy
객체로 변경했다.OrderServiceImpl
를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.다형성 덕분에 새로운 정률 할인 정책 코드를 추가로 개발하는 것 자체는 아무 문제가 없음
새로 개발한 정률 할인 정책을 적용하려고 하니 클라이언트 코드인 주문 서비스 구현체도 함께 변경해야함
주문 서비스 클라이언트가 인터페이스인 DiscountPolicy
뿐만 아니라, 구체 클래스인 FixDiscountPolicy
도 함께 의존 -> DIP 위반
여기서 3가지 SRP, DIP, OCP적용
한 클래스는 하나의 책임만 가져야 한다.
프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
OrderServiceImpl
)는 DIP를 지키며 DiscountPolicy
추상화 인터페이스에 의존하는 것 같았지만, FixDiscountPolicy
구체화 구현 클래스에도 함께 의존했다.DiscountPolicy
추상화 인터페이스에만 의존하도록 코드를 변경했다.FixDiscountPolicy
객체 인스턴스를 클라이언트 코드 대신 생성하여 클라이언트 코드에 의존관계를 주입했다. 이렇게 해서 DIP 원칙을 따르면서 문제도 해결할 수 있었다.소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다.
FixDiscountPolicy
-> RateDiscountPolicy
로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨OrderServiceImpl
은 필요한 인터페이스만을 호출할 뿐 어떤 구현 객체들이 실행될 지 알지 못한다.OrderServiceImpl
도 AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl
이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수도 있다. 여기서 OrderSerivceImpl
은 어떤 구현 객체가 생성될지 모른 상태로 자신의 로직만을 수행한다.프레임워크 vs 라이브러리
OrderServiceImpl
은 DiscountPolicy
인터페이스에만 의존하고 실제로 어떤 구현 객체가 사용될지는 모른다.OrderServiceImpl
은 MemberRepository
, DiscountPolicy
에 의존하는 것을 알 수 있다.OrderServiceImpl
에 주입될지 알 수 없다.지금까지는 순수한 java만을 이용해서 서비스를 구현하였다. 다음 포스트에서는 본격적으로 spring을 활용하여 똑같은 서비스를 어떻게 구현하는지 알아보고 그 과정에서 spring의 강력한 역할과 개발자를 어떻게 도와주고 있는지에 대해서 알아보겠다.