현재 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은 기능을 실행하는 책임만 지게 됨
※ 본 게시글은 인프런의 스프링 핵심 원리 - 기본편(김영한)을 수강하고 정리한 내용입니다.
'Study > Spring' 카테고리의 다른 글
[스프링 핵심 원리 - 기본편] 스프링 핵심 원리 이해2 - 객체 지향 원리 적용(전체 흐름 정리) (0) | 2022.12.11 |
---|---|
[스프링 핵심 원리 - 기본편] 스프링 핵심 원리 이해2 - 객체 지향 원리 적용(AppConfig 리팩터링) (0) | 2022.12.10 |
[스프링 핵심 원리 - 기본편] 스프링 핵심 원리 이해2 - 객체 지향 원리 적용(새로운 할인 정책 적용과 문제점) (0) | 2022.12.09 |
[스프링 핵심 원리 - 기본편] 스프링 핵심 원리 이해2 - 객체 지향 원리 적용(새로운 할인 정책 개발) (0) | 2022.12.08 |
[스프링 핵심 원리 - 기본편] 스프링 핵심 원리 이해1 - 예제 만들기(주문과 할인 도메인 실행과 테스트) (0) | 2022.12.06 |