반응형

 

새로운 할인 정책 개발


 

10프로 할인 정책!

package hello.core.discount;

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

//10퍼센트만 할인 한다고 가정
public class RateDiscountPolicy implements DiscountPolicy{
    //할인율
    private int discountPrice = 10;
    
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price*discountPrice/100;
        }
        else{
            return 0;
        }
    }
}

 

새 할인 정책 적용!

OrderService 구현체에서 직접 선택해줘야된다 => 따라서 구현체에 의존하게된다.(인터페이스가 아니라)

클라이언트(OrderServiceImpl)의 코드를 수정해야한다.

OCP => 변경하지않고 확장가능 => 깨짐

DIP => 인터페이스 뿐만 아니라 구현체에 의존하게됨.

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;

//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy= new RateDiscountPolicy();

    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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)를 공연의 예제에 비교하면

배우(로미오 역)이 직접 여배우(줄리엣)역을 섭외하는 격이다!!

배우는 대본에만 집중을 해야지 섭외의 역할까지 맡게되면서 다양한 책임을 가지게 된다.

 

그래서 관심사를 분리해야한다.

배우는 배우 역할에만 신경쓰게!

 

따라서 공연기획자가 필요하다.

공연을 구성, 배우 섭외, 역할에 맞는 배우를 지정하는 책일을 가지는 공연 기획자가 필요!

 

바로 공연기획자가 AppConfig이다.

 

 

 

AppConfig


 

애플리케이션의 전체 동작 방식을 구성한다.

구현 객체를 생성하고, 연결하고 책임을 가진다.

위 역할을 하는 별도의 설정 클래스이다.

 

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(new MemoryMemberRepository());
    }
    
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }

}

각 클라이언트의 생성자를 이용하여 맞는 구현체를 넣어준다.

=> 생성자 주입기법

 

그러면 이제 각 구현체에서 생성자를 통해서 받아오기만 하면 된다!!!

 

멤버서비스 구현체

package hello.core.member;

//멤버서비스 객체는 멤버리포지토리 객체가 필요함
public class MemberServiceImpl implements MemberService{

    //배역 배우로 생각할때
    //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);
    }
}

 

주문 서비스 구현체

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.MemberService;
import hello.core.member.MemoryMemberRepository;

//할인 정책을 가져와서 저장소에서 vip인지 조회해봐야됨
//주문을 생성해서 저장소에 vip인지 조회
//vip이면 할인 해줌
public class OrderServiceImpl implements OrderService{
    //저장소 조회
    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    //할인을 위해서 할인 정책 필요
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    //private final DiscountPolicy discountPolicy= new RateDiscountPolicy();

    //생성자 주입기법으로 맞는 구현체 가져옴
    private final MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    //id로 조회해서 vip 이면 할인정책 적용
    //주문 엔티티 => 회원id, 상품명, 상품가격, 할인된 금액
    @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);
    }
}

 

AppConfig를 통하여 주입받기 위해 MemberApp 클래스를 수정한다.

AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();//주입!

 

package hello.core;

import hello.core.AppConfig;
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();//주입!
        //멤버서비스 구현체는 생성자로 적절한 구현체들을 받게된다.
        //즉, 멤버 서비스 구현체는 어떤 구현체를 가져올지 고민안해도 됨!
        //밑에선 memberServiceImpl를 가져오고 MemberServiceImpl 내부적으로 어떤걸 쓸지 골라야됬음!
        //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("findMember = " + findMember.getName());


    }
}

 

주문 서비스 또한 appConfig로 인젝션을 해준다.

AppConfig appConfig = new AppConfig();
        OrderService orderService = appConfig.orderService(); //주입!!\
        MemberService memberService = appConfig.memberService();

 

package hello.core;

import hello.core.AppConfig;
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();
        OrderService orderService = appConfig.orderService(); //주입!!\
        MemberService memberService = appConfig.memberService();

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


}

 

각 테스트코드 또한 바꾸어 준다.

@BeforeEach를 사용하여 appconfig로 주입시켜준다.

@BeforeEach
    void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }

 

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.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
    void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }
    @Test
    void join(){

        //given - vip
        Member member = new Member(1L, "memberA" , Grade.VIP);

        //when - 회원가입
        memberService.join(member);
        Member findMember = memberService.findMember(member.getId());

        //then - 가입한 회원이 맞는지
        Assertions.assertThat(member).isEqualTo(findMember);


    }
}

 

 

 

AppConfig 리팩터링


 

기존

public class AppConfig {
    
    //메모리멤버리포지토리가 두번이나 생성되어 중복된다.
    //그리고 각 역할과 구현이 뚜렷하게 눈에 보이지 않는다.
    
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    
    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }

}

 

리팩터링 후

public class AppConfig {

    //멤버 서비스는 멤버리포지토리가 필요하다.
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    //리포지토리는 멤버 리포지토리
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    //주문 서비스는 저장소와 할인정책이 필요하다.
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    //할인정책은 FixDiscountPolicy
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
        //return new FixDiscountPolicy();
    }

}

중복이 제거되었고, 각 역할과 구현이 명확하게 보인다.

 

또한 AppConfig 설정을 바꿔줌으로써 할인정책을 바꿀수 잇게 되었다.

따라서 클라이언트의 코드를 수정하지 않고 변경가능!!!

OCP = 확장에 유연, 변경에는 닫힘

을 지킬 수 있고

DIP = 인터페이스에 의존

도 가능해졌다~~!

 

AppConfig를 사용함으로써

사용영역과 구성영역으로 구분이 되었고, 변경사항시 구성영역만 변경하면 되게 되었따.

또한 구성영역을 사용함으로써 역할과 구현이 명확하게 분리 되고, 역할이 잘 보이게 되었고, 중복이 제거되었다.

 

정리하면, SRP, DIP, OCP를 적용하게 되었다.

 

SRP

한 클래스는 하나의 책임만 가져야한다.

기존 - 클라이언트가 직접 구현객체생성, 연결, 실행

AppConfig 사용 - 구현객체를 AppConfig가 생성하고 연결함, 따라서 클라이언트는 실행하는 책임만 가지게됨.

 

DIP 의존관계 역전 원칙

프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다."

=> 의존관계 주입은 이 원칙을 따르는 방법 중 하나이다.

 

기존 - 새로운 할인정책을 만들고 적용시킬때, 클라이언트 코드 수정해야했음

AppConfig 사용 - AppConfig가 할인정책 객체 인스턴스를 클라이언트 코드 대신에 생성해서 클라이언트 코드에 의존관계를 주입했다.

즉, 외부에서 객체 인스턴스를 넣어주어 DIP원칙을 지켰다.

 

 

OCP

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다.

다형성을 사용하고 클라이언트가 DIP를 지키면서 OCP가능성이 높아졌다.

애플리케이션 => 사용영역 / 구성영역으로 분리되었다.

AppConfig가 의존관계를 클라이언트에 주입해주어 클라이언트 코드변경X

따라서, 소프트웨어 요소를 새로 확장해도 사용 영역의 변경은 없다 => 즉 닫혀있다.!

 

 

IoC, DI, 컨테이너

IoC? => Inversion of Control(제어의 역전) , 제어를 AppConfig가 가지게 됨(외부에서 제어흐름을 가지게 됨)

 

기존 - 클라이언트 구현객체가 스스로 서버구현객체 생성, 연결, 실행 => 구현 객체가 프로그램 제어흐름 조종!

AppConfig - 구현객체는 자신의 로직만 실행, 제어흐름은 AppConfig가 해줌

(예를 들면 OrderServiceImpl은 필요한 인터페이스를 호출 하지만 어떤 구현객체가 올지 모른다. => AppConfig가 주입해줌)

 

 

프레임워크 vs 라이브러리

프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다 => ex) Junit

내가 작성한 코드가 직접 제어의 흐름을 담당하면 라이브러리이다. => 직접 메서드 호출 등..

 

DI(Dependecy Injection)= 의존관계 주입

OrderServiceImpl(구현체)는 DiscountPolicy(인터페이스)에 의존한다. => 어떤 DiscountPolicy가 올지 모른다.

의존관계는 정적인 클래스 의존 관계, 실행시점에 결정되는 동적인 객체 의존 관계를 분리해서 생각해야한다.

 

  • 정적인 클래스 의존 관계

    클래스가 사용하는 import 코드만 보고 의존관계 파악이 가능하다.

    즉, 애플리케이션을 실행하지 않아도 분석가능!

    하지만 어떤 구현객체가 주입되는지는 모름!

     

  • 동적인 객체 의존 관계

    애플리케이션 실행 시점에 실제 생성된 객체 인스턴스 참조가 연결된 의존 관계

    애플리케이션 실행 시점(런타임)에 외부에서 실제 구현객체를 생성해서 클라이언트에 전달!

    클라이언트와 서버 의존관계가 연결된다. => 의존관계 주입

    의존관계 주입을 통해서 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 변경할 수 있다.

 

IoC컨테이너 = DI컨테이너 (어셉블러(조립), 오브젝트 팩토리(오브젝트를 만드니까) 등으로 불리기도함)

AppConfig와 같은 역할

객체를 생성, 관리하면서 의존관계를 연결해주는 것.

 

 

살짝 정리

이때까지 순수 자바코드로 DI, 의존관계 주입을 해보았다.

다음으론 스프링을 사용하여 의존관계 주입을 해본다.

728x90
반응형
블로그 이미지

아상관없어

,