현재 OrderServiceImpl의 문제점

// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
  • 구현체가 없어서 NullPointerException이 발생
  • OrderServiceImpl이 구현체를 정해야 하는 상황
    • OrderServiceImpl이 직접 객체를 생성하고 구현체의 선택도 함
  • 관심사의 분리가 필요
    • 별도의 설정 클래스가 구현체를 생성하고 연결

AppConfig 등장

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

AppConfig와 MemberService 관련 코드 변경

  • 먼저 이전에 만든 MemberService와 관련된 코드부터 변경
//    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
  • MemberServiceImpl의 코드는 위와 같이 변경
    • 주석 처리된 부분이 이전 코드로, MemoryMemberRepository라는 구현체를 선택하고 있음
    • 변경된 코드는 구현체에 대한 정보가 존재하지 않음
    • DIP를 준수하는 코드가 됨
package hello.core;

import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
}
  • AppConfig에서 MemberServiceImpl에 대한 구현체를 선택
  • 생성자 주입 기법

AppConfig와 OrderServiceImpl 관련 코드 변경

private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
  • OrderServiceImpl은 구현체에 대한 정보가 두 개 필요함
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
  • 따라서 생성자에 MemberRepository와 DiscountPolicy 두 개의 매개변수를 받음
  • OrderServiceImpl에는 구현체에 대한 정보가 없음
  • 생성자에 의해 구현체가 할당됨
  • 철저하게 DIP를 준수하는 코드(인터페이스에만 의존, 구체적인 클래스에 대해서 모름)
public OrderService orderService() {
    return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
  • AppConfig에서 다음과 같이 두 구현체를 지정

지금까지 정리

  • AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성
    • MemberServiceImpl
    • MemoryMemberRepository
    • OrderServiceImpl
    • FixDiscountPolicy
  • AppConfig는 생성한 객체 인스턴스의 참조(래퍼런스)를 생성자를 통해서 주입(연결)
    • MemberServiceImpl → MemoryMemberRepository
    • OrderServiceImpl → MemoryMemberRepository, FixDiscountPolicy

변경된 MemberServiceImpl 코드

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 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지) 알 수 없음
  • MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정
    • 이전에는 MemberServiceImpl 안에서 결정
  • MemberServiceImpl은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중

변경된 설계의 클래스 다이어그램

  • 객체의 생성과 연결은 AppConfig가 담당
  • DIP 완성: MemberServiceImpl은 MemberRepository인 추상에만 의존
    • 구체 클래스를 몰라도 됨
  • 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확하게 분리

객체 인스턴스 다이어그램

  • appConfig 객체가 memoryMemnerRepository(참조값: x001) 객체를 생성
    • 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달
  • 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같음
    • 이러한 이유로 DI(Dependency Injection), 의존관계 주입 또는 의존성 주입이라고 함

변경된 OrderServiceImpl 코드

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

        // OrderService 입장에서는 할인 정책에 대해서 모르고, discountPolicy에 위임함 -> 단일 책임 원칙 준수
        // 할인이 변경되어도 OrderService는 바뀌지 않음
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
  • import에 구현체에 대한 import가 모두 사라짐
  • 설계 변경으로 OrderServiceImpl은 FixDiscountPolicy를 의존하지 않음
    • 단지 DiscountPolicy 인터페이스에만 의존
  • OrderServiceImpl 입장에서 OrderServiceImpl의 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없음
  • 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부(AppConfig)에서 결정
  • OrderServiceImpl은 이제부터 실행에만 집중

MemberApp 변경

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 = new MemberServiceImpl();
        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);
        System.out.println("findMember = " + findMember);
    }
}
  • memberService에 appConfig를 이용하여 객체 생성
  • 인터페이스에만 의존하고 구체 클래스에 의존하지 않음
  • 실행 결과는 동일

OrderApp 변경

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.toString());
    }
}
  • appConfig를 이용하여 객체 생성
  • 인터페이스에만 의존하고 구체 클래스에 의존하지 않음
  • 실행 결과는 동일

MemberServiceTest 변경

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
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);
    }
}
  • @BeforeEach 어노테이션은 각 테스트의 실행 전에 실행 됨
  • beforeEach 메소드를 만들어 객체 생성

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 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() {
        // primitive type인 long으로 해도 되지만, 그러면 null이 들어갈 수 없음
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        // org.assertj.core.api를 사용해야 .을 이용한 메소드 체인을 사용 가능
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}
  • DIP를 지키게 됨

정리

  • AppConfig를 통해 관심사를 확실하게 분리
  • AppConfig는 구체 클래스를 선택
    • 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임
  • OrderServiceImpl은 기능을 실행하는 책임만 지게 됨

 

※ 본 게시글은 인프런의 스프링 핵심 원리 - 기본편(김영한)을 수강하고 정리한 내용입니다.

+ Recent posts