새로운 할인 정책 개발

  • 새로운 할인 정책으로 확장
    • 고정 금액 -> 10% 할인
//RateDiscountPolicy.java

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

public class RateDiscountPolicy implements DiscountPolicy{
    private int discountPercent =10;
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent /100 ;
        }else{
            return 0;
        }

    }
}
  • 테스트 작성
    • class 선택한 후 command + shift + T : 테스트 클래스 만들어 줌

 

//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% 할인이 적용되어야 한다.")
    void vip_o(){
        //given
        Member member = new Member(1L, "memberVIP", 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(2L, "memberVIP", Grade.BASIC);
        //when
        int discount = discountPolicy.discount(member, 10000);

        //then
        Assertions.assertThat(discount).isEqualTo(1000);
    }
}
  • 실패 테스트도 만들어 봐야함

  • 실패 테스트 결과

//Assertions.assertThat(discount).isEqualTo(1000);
assertThat(discount).isEqualTo(1000);
  • option+ enter : 상수나 enum을 사용할 때 해당 클래스명이나 enum명을 사용하지 않고 바로 변수를 사용할 수 있게 해줌
    • 사용 이유 : 나중에 가면 갈수록 코드가 길어지면, 클래스 이름도 거추장스럽다

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

//OrderServiceImpl.java

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository=new MemoryMemberRepository();
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    @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() -> RateDiscountPolicy()로 코드를 고쳐야한다.
  • 역활과 구현 충실하게 분리 O
  • 다형성 활용, 인터페이스와 구현 객체 분리 O
  • OCP,DIP 객체 지향 설계 원칙 준수 X
    • 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있음. - > DIP 위반
      • 추상 : DiscountPolicy
      • 구체 : FixDiscountPolicy, RateDiscountPolicy
    • 기능을 확장해서 변경하면 , 클라이언트 코드에 영향을 준다. -> OCP 위반

//OrderServiceImpl.java

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository=new MemoryMemberRepository();
    private 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);
    }
}
  • 인터페이스에만 의존하도록 코드 변경
  • 구현체가 없음 -> null pointer exception (NPE) 발생
  • 해결방안
    • 누군가가 OrderServiceImpl에 DiscountPolicy 구현 객체를 대신 생성하고 주입해주어야 함

 

관심사의 분리

로미오와 줄리엣 공연으로 비유해보자.

로미오 역할, 줄리엣 역할 : 인터페이스

레오나르도 디카프리오: 구현체

앞선 상황은 디카프리오가 줄리엣 역할을 할 여자 주인공을 직접 초빙하는 것과 같다.

디카프리오는 공연 + 초빙 이라는 다양한 책임을 갖게 됨

 -> 별도의 공연 기획자가 필요.

 

AppConfig 등장

  • 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스
//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 {

    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
    }
}

AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.

AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스) 생성자를 통해서 주입(연결)해준다.

//MemberServiceImpl.java

package hello.core.member;

public class MemberServiceImpl implements MemberService {


    //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의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정
  • 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중할 수 있게 됨

  • 클라이언트인 MemberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 의존관계 주입, 의존성 주입 이라고 한다.
//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=new MemoryMemberRepository();
    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의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정
  • 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중할 수 있게 됨

AppConfig 실행

//MemberApp.java

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;

public class MemberApp {

    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig(); //추가
        MemberService memberService = appConfig.memberService(); //추가
        //MemberService memberService = new MemberServiceImpl();

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

    }
}
//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) {

        //MemberService memberService = new MemberServiceImpl();
        //OrderService orderService = new OrderServiceImpl();
        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);

    }
}

테스트 코드도 수정

//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 = new MemberServiceImpl();
    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); //join한거랑 찾은 거랑 같은가

    }
}
//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 hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

   // MemberService memberService = new MemberServiceImpl();
    MemberService memberService;
   // OrderService orderService = new OrderServiceImpl(discountPolicy);
   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,"itemA",10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);

    }
}
  • @BeforeEach는 각 테스트를 실행하기 전에 호출된다.

 

AppConfig 리팩토링

현재 AppConfig를 보면 중복이 있고 역할에 따른 구현이 잘 안보인다.

//AppConfig.java

package hello.core;

import hello.core.discount.DiscountPolicy;
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(memberRepository());
    }

    private static MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }
    
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}
  • new MemoryMemberRepository() 중복 제거 ( 단축키 : command + option + m )
  • 역할과 구현 클래스가 한눈에 들어옴

 

 

새로운 구조와 할인 정책 적용

정액 할인 정책 -> 정률 할인 정책

FixDiscountPolicy -> RateDiscountPolicy

 

  • AppConfig의 등장으로 애플리케이션이 사용 영역과 , 객체를 생성하고 구성하는 영역으로 분리됨

  • FixDiscountPolicy -> RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 영향을 받지 않음
//AppConfig.java

package hello.core;

,,,
import hello.core.discount.RateDiscountPolicy;
,,,

public class AppConfig {

,,,

    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

 

 좋은 객체 지향 설계의 5가지 원칙의 적용

 

IoC, DI 그리고 컨테이너

제어의 역전 IoC

  • 프로그램의 제어 흐름을 프로그램에서 관리하는게 아니라 외부에서 관리하는 것
  • AppConfig가 프로그램의 제어 흐름을 가져감. 구현 객체는 자신의 로직을 실행하는 역할만 담당.

프레임워크 vs 라이브러리

  •  내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크 (JUnit)
  •  반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리

의존관계 주입 DI (Dependency Injection)

  • 인터페이스만 의존, 실제 어떤 구현 객체가 사용될지는 모른다.
  • 정적인 클래스 의존관계
    • 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다.
    • 애플리케이션을 실행하지 않아도 분석 가능
    • 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 주입 될 지 알 수 없다.
  • 동적인 객체 인스턴스 의존 관계
    • 애플리케이션 실행 시점에 실제 생성된 객 체 인스턴스의 참조가 연결된 의존 관계
  • 실행시점에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결되는 것을 '의존관계 주입' 이라 한다.
  • 의존 관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

IoC 컨테이너 , DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다.
  • 또는 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다. 

 

 

스프링으로 전환하기 

//AppConfig.java

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(),discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • @Configuration : 설정 정보를 구성
  • 각 메소드에 @Bean : 스프링 빈에 등록

 

//MemberApp.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 org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;

public class MemberApp {

    public static void main(String[] args) {
       // AppConfig appConfig = new AppConfig(); 
      //  MemberService memberService = appConfig.memberService();
        ApplicationContext applicationContext = new AnnotationConfigReactiveWebApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        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());

    }
}
  • ApplicationContext는 스프링 컨테이너 : @Bean 객체들 관리
  • AppConfig.class : AppConfig에 있는 환경설정 정보들을 관리

  • 상위 다섯개는 스프링 내부적으로 필요해서 등록하는 스프링빈
  • 아래 다섯개는 @Bean으로 AppConfig에 작성해둔 것들
//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;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.context.ApplicationContext;

public class OrderApp {

    public static void main(String[] args) {

        //AppConfig appConfig = new AppConfig();
        //MemberService memberService = appConfig.memberService();
        //OrderService orderService = appConfig.orderService();

        ApplicationContext applicationContext =new AnnotationConfigReactiveWebApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService",MemberService.class);
        OrderService orderService =applicationContext.getBean("orderService",OrderService.class);
        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);

    }
}
  • 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
    • @Bean(name ="")으로 변경할 수도 있음 : 특별한 경우가 아니면 사용 X
  • 스프링빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있다.
  • 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경

+ Recent posts