(인프런) 김영한님의 스프링 핵심 원리-기본편을 공부하고 리뷰한 글입니다.
3. 관심사의 분리
"애플리케이션 = 공연, 각 인테페이스 = 배역(역할), 구현 객체 = 배우" 라고 생각해보자.
그런데 실제 배역을 맞는 배우를 선택하는 것은 누가 하는가??
이전 코드는 로미오 역할(인터페이스)를 하는 레오나르도 디카프리오 배우(구현 객체)가 줄리엣 역할(인터페이스)를 하는 올리비아 핫세 배우(구현 객체)를 직접 초빙하는 것과 같다. 디카프리오는 공연도 해야하고 동시에 여자 주인공도 직접 초빙해야하는 다양한 책임을 갖고 있다.
1. 관심사를 분리하자
- 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다.
- 디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야 한다.
- 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 나올시점이다.
- 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리하자.
3-1. AppConfig 등장
1. AppConfig
애플리케이션의 전체 동작 방식을 구성(config)하기 위해 구현 객체를 생성, 연결하는 책임을 가진 별도의 설정 클래스를 만든다.
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());
}
}
1) AppConfig는 애플리케이셔느이 실제 동작에 필요한 구현 객체를 생성한다.
- MemberServiceImpl
- MemoryMemberRepository
- OrderServiceImpl
- FixDiscountPolicy
2) AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
- MemberServiceImpl → MemoryMemberRepository
- OrderServiceImpl → MemoryMemberRepository, FixDiscountPolicy
2. MemberServiceImpl - 생성자 주입
package hello.core.member;
public class MemberServiceImpl implements MemberService{
// MemberServiceImpl 은 MemberRepository 인터페이스에만 의존
private final MemberRepository memberRepository;
// 의존성 주입(DI)
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 를 직접 생성했지만 설계 변경 후,
MemberServiceImpl → MemoryMeberRepository 구현 객체 의존X
MemberServiceImpl → MemberRepository 인터페이스에만 의존O
- MemberServiceImpl 은 생성자를 통해 어떤 구현 객체가 주입될지 알 수 없다.
- MemberServiceImpl 의 생성자를 통해 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다!
3. 그림 - 클래스 다이어그램
객체의 생성과 연결은 AppConfig가 담당한다.
- DIP 완성: MemberServiceImpl 은 MemberRepository 인터페이스만 의존한다. (구현체는 알 필요X)
- 관심사의 분리: 객체를 생성, 연결하는 역할 & 실행하는 역할이 분리됨!
4. 그림 - 회원 객체 인스턴스 다이어그램
- appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl 을 생성하면서 생성자로 전달한다.
- 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다.
5. OrderServiceImpl - 생성자 주입
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
public class OrderServiceImpl implements OrderService{
// 인터페이스에만 의존하도록 변경(DIP 만족)
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 의존성 주입(DI)
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); // 해당 id를 가진 회원 조회
int discountPrice = discountPolicy.discount(member, itemPrice); // 회원의 등급에 따른 할인 금액
return new Order(memberId, itemName, itemPrice, discountPrice); // 최종 생성된 주문 반환
}
}
기존에는 OrderServiceImpl 이 MemoryMemberRepository, FixDiscountPolicy 를 직접 생성했지만 설계 변경 후,
OrderServiceImpl → MemoryMeberRepository, FixDiscountPolicy 구현 객체 의존X
OrderServiceImpl → MemberRepository, DiscountPolicy 인터페이스에만 의존O
- OrderServiceImpl 은 생성자를 통해 어떤 구현 객체가 주입될지 알 수 없다.
- OrderServiceImpl 의 생성자를 통해 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
OrderServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다!
3-2. AppConfig 실행
1. 사용 클래스 - MemberApp
MemberServiceImpl을 직접 생성하지 않고 생성한 AppConfig를 이용하여 MemberServiceImpl을 생성하고 MemberService 에 연결해준다.
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
//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());
}
}
2. 사용 클래스 - OrderApp
- MemberServiceImpl을 직접 생성하지 않고 생성한 AppConfig를 이용하여 MemberServiceImpl을 생성하고 MemberService 에 연결해준다.
- OrderServiceImpl을 직접 생성하지 않고 생성한 AppConfig를 이용하여 OrderServiceImpl을 생성하고 OrderService 에 연결해준다.
package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
//MemberService memberService = new MemberServiceImpl();
//OrderService orderService = new OrderServiceImpl();
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); // order.toString() 출력됨
//System.out.println("order.calculatePrice = " + order.calculatePrice()); // 할인된 최종 가격
}
}
3. 테스트 코드 오류 수정
1) MemberServiceTest 수정
public class MemberServiceTest {
MemberService memberService;
// 각 테스트 실행 전에 실행됨
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
...
}
2) OrderServiceTest 수정
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
// 각 테스트 실행 전에 실행됨
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
...
}
<정리>
- AppConfig를 통해 관심사를 확실히 분리함
- AppConfig 는 구체 클래스를 선택한다. (배역에 맞는 담당 배우를 선택하는 것과 동일)
- OrderServiceImpl 은 기능을 실행하는 책임만 지면 된다.
4. AppConfig 리팩터링
현재 AppConfig는 중복이 있고 역할에 따른 구현이 잘 안보인다.
1. 기대하는 그림
2. 리팩터링 전
public class AppConfig {
// 생성자 주입
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
// 생성자 주입
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
- MemoryMemberRepository 를 2번 new 하는 중복이 있음
- 이 코드는 Memberservice 와 OrderService 역할에 대한 구현만 보이고, DiscountPolicy 와 MemberRepository 역할에 따른 구현은 잘 보이지 않는다.
3. 리팩터링 후
중복을 제거하고 역할에 따른 구현이 잘 보이도록 리팩터링 하였다.
AppConfig 를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
public class AppConfig {
// MemberService 역할(생성자 주입)
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
// MemberRepository 역할
private MemberRepository memberRepository() {
return new MemoryMemberRepository(); // 메모리 회원 저장소로
}
// OrderService 역할 (생성자 주입)
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
// DiscountPolicy 역할
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy(); // 고정 할인 정책으로
}
}
1) new MemoryMemberRepository() 부분 중복 제거, 역할에 따른 구현 잘 보이게 하기
- MemoryMemberRepository 를 다른 구현체로 변경할 때 MemberRepository 역할을 하는 메서드 한 부분만 변경하면 된다.
2) DiscountPolicy 역할에 따른 구현 잘 보이게 하기
- FixDiscountPolicy 를 다른 구현체로 변경할 때 DiscountPolicy 역할을 하는 메서드 한 부분만 변경하면 된다.