관심사의 분리
관심사의 분리란 하나의 역할(인터페이스)은 하나(자신)의 역할 수행에만 집중해야함을 의미한다.
비유를 먼저 해보면,
- 애플리케이션 : 공연
- 인터페이스 : 배역(배우 역할)
- 구현체 : 배역에 캐스팅된 배우
라고 하자.
실제 배역에 맞는 배우를 캐스팅하는 것은 누구인가? "캐스팅 담당자"가 따로 존재한다.
이전 까지의 코드는 마치, 로미오 역할에 디카프리오(구현체)가 줄리엣 역할의 배우를 직접 섭외하는 것과 같은 상태였다.
이렇게 되면, 배우라는 구현체는 "연기"라는 관심사 주요 역할에서 벗어나, 누군가를 캐스팅하는 다양한 책임이 부여된다.
이는 올바른 상태가 아니다.
배우 디카프리오(구현체)는 "연기(역할)"에만 집중해야하며,
상대 배우로 누가 캐스팅되던지 (의존하는 인터페이스의 구현체가 무엇이 되든지) 관계없이 똑같이 "연기"할 수 있어야 한다.
캐스팅 담당자(공연 기획자)를 따로 두어, 관심사를 분리해야한다. (SRP 단일책임 원칙 : 하나의 클래스는 하나의 책임만)
지금 상태는 다음과 같다
2가지 서비스가 존재하는데
- 회원 시스템(MemberService)
- 주문 시스템(OrderService)
MemberServiceImpl
public class MemberServiceImpl implements MemberService {
// " 인터페이스 = 구현체 " 형태
// 의존관계에는 구현체가 아닌 인터페이스를 의존해야하며,
// 구현객체가 없으면 nullpointException이므로 구현체를 선택해줘야한다.
// 인터페이스뿐 아니라 구현체까지 의존한 상태(DIP위반)
private final MemberRepository memberRepository = new MemoryMemberRepository();
OrderServiceImpl
public class OrderServiceImpl implements OrderService{
//인터페이스 = 구현체 -> 인터페이스 뿐 아니라 구현체에 의존하고 있음
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DisountPolicy disountPolicy = new FixDiscountPolicy();
따라서, 위의 비유처럼
구현체를 생성하고, 이들을 의존관계의 구현체와 연결해주는 일은 OrderServiceImpl / MemberServiceImpl (의존하고 있는 구현체)가 아닌 외부에서 해주어야 한다. ( = 줄리엣(상대역)을 캐스팅 해주는 일은 디카프리오가 아닌 캐스팅담당자의 일)
이 두 구현체는 자신의 로직실행에만 충실해야한다 (= 디카프리오가 로미오 연기에만 충실)
AppConfig
애플리케이션의 전체 동작방식을 구성(config), 구현 객체를 생성(생성자 / new)하고, 연결하는 책임(생성자의 매개변수로)을 가지는 별도의 설정 클래스를 만들어보자.
각각의 서비스가 config를 통해 조회될 때, 아래와 같은 역할을 하여 구현체를 리턴해준다.
회원 시스템(memberService)
memberService(인터페이스)는 MemberRepository(인터페이스)를 의존한다. (필요하다는 뜻)
AppConfig에서는 MemberServiceImpl이라는 구현체를 생성자를 이용해 "생성"하면서, 매개변수로 MemoryMemberRepository라는 구현체를 "생성"하여 "연결"(주입)시켜준다.
주문 시스템(orderService)
orderService(인터페이스)는 MemberRepository(인터페이스)와 DiscountPolicy(인터페이스)를 의존한다. (필요하다는 뜻)
AppConfig에서는 OrderServiceImpl이라는 구현체를 생성자를 이용해 "생성"하면서, 매개변수로 MemoryMemberRepository라는 구현체를 "생성"하여 "연결"(주입)시켜준다.
이렇기 때문에 이를 "생성자 주입" 이라고 한다.
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 { //관심사의 분리
//config를 통해서 memberService / OrderService(인터페이스) 를 조회시 -> 조회하는 애들은 인터페이스와만 의존관계를 가진다! (구현체를 조회한게 아니라 인터페이스를 조회함)
//config에서 등록한 각각 구현체 MemoryMemberRepo / FixDiscountPolicy 구현체로 생성한
// memberSerivceImpl / OrderServiceImpl(구현체)를 반환한다.
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
// MemberServiceImpl을 생성자를 이용해서 생성함 -> 생성시 매개변수로 구현체를 넣어줌
// MemberServiceImpl자체에서 MemberRepository구현체를 선택하지 않고, 여기서 넣어주기 때문에 DIP/OCP를 지켰다
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
<정리>
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
- MemberServiceImpl
- MemoryMemberRepository
- OrderServiceImpl
- FixDiscountPolicy
AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
- MemberServiceImpl → MemoryMemberRepository
- OrderServiceImpl → MemoryMemberRepository , FixDiscountPolicy
생성자 주입
그렇다면 당연하게도 각 서비스 구현체는 1. 생성자가 필요할 것이며,
위에서 각각 코드상에서 지적했던 2. 구현체를 직접 작성해주는 부분을 제거해야할 것이다.
MemberServiceImpl
public class MemberServiceImpl implements MemberService {
// " 인터페이스 = 구현체 " 형태
// 의존관계에는 구현체가 아닌 인터페이스를 의존해야하며,
// 구현객체가 없으면 nullpointException이므로 구현체를 선택해줘야한다.
private final MemberRepository memberRepository; // 구현체를 직접 선택하지 않도록 함 -> 오직 인터페이스만 존재 (추상화에만 의존함 DIP)
//생성자
public MemberServiceImpl(MemberRepository memberRepository){ //생성될 때 매개변수로 MemberRepsository구현체를 받아서 생성됨(생성자주입) -> 구현체를 매개로 넣어주는일은 AppConfig에서
this.memberRepository = memberRepository;
}
...
OrderServiceImpl
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;// -> 오직 인터페이스만 존재 (추상화에만 의존함 DIP)
private final DisountPolicy disountPolicy; //인터페이스 (추상)에만 의존함 (DIP)
// 어떤 구현체가 들어올지 전혀 모르는 상태다
public OrderServiceImpl(MemberRepository memberRepository, DisountPolicy disountPolicy){
this.disountPolicy =disountPolicy;
this.memberRepository=memberRepository;
}
...
<정리 MemberServiceImpl 기준으로>
- 설계 변경으로 MemberServiceImpl 은 MemoryMemberRepository(구현체) 를 의존하지 않는다!
- 단지 MemberRepository 인터페이스만 의존한다.
- MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
- MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정된다.
- MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
클래스 다이어그램
회원 객체 인스턴스 다이어그램
1. 생성
new MemoryMemberRepository()
2. 생성 + 주입(memoryMemberRepository x001)
// 생성
new MemberServiceImpl();
// 생성 + 주입
new MemberServiceImpl(new MemoryMemberRepository());
- appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl 을 생성하면서 생성자로 전달한다.
- 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다
의존성 주입으로 DIP문제를 해결하였다.
실행(테스트)
순수 자바 코드 버전
순수 자바 코드 버전으로 MemberApp / OrderApp 자바코드를 만들어 main으로 직접 실행해 보았었다.
전부 AppConfig를 이용하는 방식으로 변경해주어야한다.
MemberApp
public class MemberApp {
//psvm (단축)
public static void main(String[] args) {
// 수정된 코드
// 구현체의 생성과 의존관계 연결을 담당하는 "appConfig"객체를 생성함
// appConfig내 memberService(인터페이스)의 "생성+의존연결"을 담당하는 메소드를 호출해 memberServiceImpl(구현체)리턴받아 사용
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
//이전 코드
//main는(클라이언트)에서 memberService 내 메소드를 실행하기 위해 구현체를 만듬
// memberService는 memberRepository를 의존하기 때문에, MemberServiceImpl코드 내에서 직접 MemoryMemberRepository를 생성해 사용했음.
// MemberService memberService = new MemberServiceImpl(); // 인터페이스 = 구현체
이전 코드에서는
main(클라이언트)가 memberService를 의존하고, memberService는 memberRepository를 의존했으며,
실제로 인터페이스만 의존하지 않았고, 구현체를 의존했기 때문에
각각의 코드에서 직접 new로 구현체를 생성하는 방식(main은 memberServiceImpl을 생성 -> memberServiceImpl은 MemoryMemberReopository를 생성)으로 진행되었다.
하지만, 수정된 코드에서는 memberService라는 메소드를 통해 내부 의존관계를 (생성+연결) 완성하고 있기 때문에, 다음과 같이 작동한다.
실행결과는 이전코드와 이후 코드가 동일하게 문제 없이 실행된다.
OrderApp
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();
스프링 테스트 버전
@BeforeEach를 이용한다.
이는 테스트코드가 돌아가기전에 무조건 처음에 실행되는 메소드이다.
MemberServiceTest
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach(){ // 코드 실행전에 무조건 실행되는 코드
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
// MemberService memberService = new MemberServiceImpl(); // 인터페이스 = 구현체
OrderServiceTest
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach(){ // 코드 실행전에 무조건 실행되는 코드
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
// MemberService memberService = new MemberServiceImpl();
// OrderService orderService = new OrderServiceImpl();
둘다 성공적으로 테스트가 완료되었다.
정리
- AppConfig를 통해서 관심사를 확실하게 분리했다.
- 배역, 배우를 생각해보자.
- AppConfig는 공연 기획자다.
- AppConfig는 구체 클래스를 선택한다. 배역에 맞는 담당 배우를 선택한다.
- 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다.
- 이제 각 배우들은 담당 기능을 실행하는 책임만 지면 된다.
- OrderServiceImpl 은 기능을 실행하는 책임만 지면 된다.
AppConfig 리팩토링
현재 AppConfig에는 중복이 있고, 역할에 따른 구현이 한눈에 보이지 않음
현재 AppConfig
public class AppConfig { //관심사의 분리
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
실제 인터페이스는 4개이고, 구현체도 4개(종류)인데, 한눈에 보기어려움
지금 2개의 인터페이스에 대해서만 구현체를 만드는 것이 직관적임
cmd + option + M 를 이용하면 메소드를 추출해줌 Return type은 구현체가 아닌 인터페이스로 해야,
구현체를 다른것으로 변경했을 때 용이함.
다음과같이 MemberRepository인터페이스에 대해서도 구현체를 리턴해주는 함수가 생겨 직관적이며,
공통되는 부분도 함께 변경됨. (중복제거)
최종적으로 다음과 같이 리팩토링
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();//나중에 구현체를 변경할 때에도 쉽게 변경하기 쉬움
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), disountPolicy());
}
public DisountPolicy disountPolicy(){
return new FixDiscountPolicy();//나중에 구현체를 변경할 때에도 쉽게 변경하기 쉬움
}
}
설계 그림과 같이 4개의 인터페이스가 어떤 구현체로 리턴되는지 직관적으로 확인이 가능함 (중요)
AppConfig를 통해 역할과 구현 클래스가 한눈에 들어오며, 전체 애플리케이션 구성이 어떻게 되어있는지 빠르게 파악가능!
'Backend > Spring' 카테고리의 다른 글
[Spring] 스프링으로 변경 1) IoC, DI, 컨테이너 (0) | 2021.09.24 |
---|---|
[Spring] 순수 자바의 문제와 해결 3) 요구사항 변경 반영(OCP) (0) | 2021.09.24 |
[Spring] 순수 자바의 문제와 해결 1) OCP/DIP위반 (0) | 2021.09.22 |
[Spring] 순수 자바 예제 3) - 주문/할인 도메인 개발&테스트 (0) | 2021.09.22 |
[Spring] 순수 자바 예제 2) - 회원 도메인 개발&테스트 (0) | 2021.09.22 |