<Spring> 스프링 핵심원리 이해 2 - AppConfig를 통한 DI 구현

새로운 할인 정책 개발

  • 현재는 정액 할인 정책을 채택하여 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();
  }

해당 코드의 문제점 찾기

  1. 역할(interface)과 구현(class)는 충실하게 분리하여 구현하였다.
  2. 그러면 OCP,DIP를 지키고 있는가?
  3. DIP : OrderServiceImplDiscountPolicy 인터페이스에 의존하는 동시에 RateDiscountPolicy 구현체에도 의존하고 있기에 DIP를 위반한다.
  4. 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의 생성자 설정에서도 구현체 두개를 리턴하도록 하면된다.
  • MemberRepositoryDiscountPolicy에 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();
} }
해당 글은 김영한님의 <스프링 핵심> 강좌를 기반으로 작성되었습니다.