mojo's Blog

DI(Dependency Injection) 본문

Spring

DI(Dependency Injection)

_mojo_ 2022. 1. 23. 23:52

새로운 할인 정책 개발

 

이번엔 10% 할인이 적용되도록 해야 한다.

우선 [discount] 폴더 아래에 RateDiscountPolicy 클래스를 생성하고 다음과 같이 코드를 작성한다.

 

package hello.core.discount;

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

public class RateDiscountPolicy implements DiscountPolicy{

    private int discountPercent = 10;

    public int discount(Member member, int price){
        if(member.getGrade() == Grade.VIP){
            return price * discountPercent / 100;
        }
        else{
            return 0;
        }
    }
}

 

※ ctrl + shift + t : 현재 만든 RateDiscountPolicy 를 테스트 할 수 있도록 [test] 폴더 아래에 RateDiscountPolicyTest 클래스가 생성된다.

이때 유용한 점은 RateDiscountPolicy 의 경로가 [test] 폴더에도 동일하게 적용된다는 점이다.

 

ctrl + shift + t를 이용하여 RateDiscountPolicyTest 클래스를 생성하고 다음과 같이 코드를 작성하여 테스트를 진행한다.

 

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;

import static org.junit.jupiter.api.Assertions.*;

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, "memberBASIC", Grade.BASIC);
        //when
        int discount = discountPolicy.discount(member, 10000);
        //then
        Assertions.assertThat(discount).isEqualTo(0);
    }
}

 

 

이번엔 실제로 10% 할인이 적용되도록 하는 할인 정책을 애플리케이션에 적용해보도록 한다.

할인 정책을 변경하기 위해서 OrderServiceImpl 코드를 다음과 같이 수정해야 한다.

 

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); <= new!

	...
}

 

※ 문제점

  • 역할과 구현을 충실하게 분리하였고 다형성도 활용하며 인터페이스와 구현 객체를 분리하였다.
  • 하지만 OCP, DIP 같은 객체지향 설계 원칙을 준수하지 못하였다.
  • DIP : 주문서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같지만 구현 클래스에도 의존하고 있다. (추상(인터페이스) 의존 : DiscountPolicy, 구현 클래스 : FixDiscountPolicy, RateDiscountPolicy)
  • OCP : 변경하지 않고 확장할 수 있지만 현재 코드로는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 주므로 OCP를 위반한다.

 

 

왼쪽 사진은 클라이언트인 OrderServiceImpl이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 인 구체(구현) 클래스도 함께 의존하고 있으므로 DIP를 위반하고 있다.

그리고 오른쪽 사진과 같이 정책을 변경하여 FixDiscountPolicy를 RateDiscountPolicy로 변경하는 순간 OrderServiceImpl의 소스코드도 함께 변경해야 한다는 점에서 OCP 또한 위반하고 있음을 알 수 있다.

 

이러한 문제점을 어떻게 해결할 수 있을까?

 

  • 클라이언트 코드인 OrderServiceImpl은 DIscountPolicy의 인터페이스 뿐만 아니라 구체(구현) 클래스도 함께 의존한다.
  • 따라서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다. 
  • DIP를 위반하므로 추상(인터페이스)에만 의존하도록 변경해야 한다. (즉, 인터페이스에만 의존)
  • DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.

 

다음과 같이 인터페이스에만 의존하도록 설계를 변경하면 된다.

 

 

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private DiscountPolicy discountPolicy;
    
    ...
}

 

위 코드로 DiscountPolicy 인터페이스에만 의존하도록 코드를 변경해주면 DIP를 위반하지 않게 된다.

그런데 구현체가 없는에 어떻게 코드를 실행할 수 있는지에 대한 고민을 해봐야 한다.

실제로 실행해보면 NPE(Null Pointer Exception)가 발생한다.

 

※ 해결방안

이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해줘야 한다. 

 

 

관심사의 분리

 

※ AppConfig

애플리케이션의 전체 동작 방식을 구성(config)하기 위해 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만든다.

 

우선 [main] -> [java] -> [hello] -> [core] 아래에 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는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.

 

  • MemberServiceImpl
  • MemoryMemberRepository
  • OrderServiceImpl
  • FixDiscountPolicy

 

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

 

  • MemberServiceImpl -> MemoryMemberRepository
  • OrderServiceImpl -> MemoryMemberRepository, FixDiscountPolicy(or RateDiscountPolicy)

 

MemberServiceImpl 에서 생성자를 통해 주입하는 코드는 다음과 같다.

 

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    
    ...
 }

 

  • 실제 변경으로 MemberServiceImpl은 MemoryMemberRepository를 의존하지 않는다.
  • 단지 MemberRepository 인터페이스만 의존한다.
  • MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어오는지(주입될지) 알 수 없다.
  • MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
  • MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.

 

클래스 다이어그램는 아래와 같다.

 

 

객체의 생성과 연결은 AppConfig가 담당한다.

MemberServiceImpl은 MemberRepository 의 추상(인터페이스)에만 의존하면 된다. (구현 클래스를 몰라도 됨 -> DIP 완성)

객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다. (관심사의 분리)

 

 

 

appConfig 객체는 memoryMemberRepository 객체를 생성하고 memberServiceImpl 을 생성하면서 생성자로 전달한다.

클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection)이라 한다.

 

이번엔 OrderServiceImpl 에서 생성자를 통해 주입하는 코드는 다음과 같다.

 

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

    ...
}

 

실제 변경으로 OrderServiceImpl은 FixDiscountPolicy를 의존하지 않는다. (단지 DiscountPolicy를 의존)

OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지는 알 수 없다.

OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.

OrderServiceImpl 은 이제부터 실행에만 집중하면 된다.

OrderServiceImpl 에는 MemoryMemberRepository, FixDiscountPolicy(or RateDiscountPolicy) 객체의 의존관계가 주입된다.

 

이제 AppConfig을 통해서 제대로 동작하는지 확인해보도록 한다.

① MemberApp 클래스

 

package hello.core.member;

import hello.core.AppConfig;

public class MemberApp {

    // psvm : 자동으로 public static void main 생성
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        // ctrl + alt + v : 자동으로 Member member 생성
        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

 

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;

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);
        System.out.println("order.calculatePrice = " + order.calculatePrice());
    }
}

 

 

③ MemberServiceTest

 

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

 

 

④ OrderServiceTest

 

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

    }
}

 

 

모두 성공적으로 실행되는것을 확인할 수 있다.

'Spring' 카테고리의 다른 글

스프링 컨테이너와 스프링 빈  (0) 2022.01.25
AppConfig 리팩터링 및 IoC, DI, 그리고 컨테이너  (0) 2022.01.24
스프링 핵심 원리 이해 - 예제 만들기  (0) 2022.01.23
객체 지향 설계와 스프링  (0) 2022.01.20
AOP  (0) 2022.01.15
Comments