연극 역할을 누가 할지는 배우가 아니라 기획자 결정한다. 배우가 캐스팅까지 담당한다면 너무 다양한 책임을 가진 것이다.
배우는 본인 배역을 잘 수행하는 것에만 집중한다.
배우는 어떤 상대 배우가 오더라도 똑같이 공연할 수 있다.
공연을 구성하고, 배우를 섭외하고, 역할을 지정하는 책임은 기획자가 한다.
기획자를 만들어 배우와 기획자의 책임을 확실히 분리해야 한다.
AppConfig
애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 연결하는 책임
을 가진 별도의 설정 클래스를 만든다.
AppConfig
는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)한다.
MemberServiceImpl → MemoryMemberRepository
OrderServiceImpl → MemoryMemberRepository, FixDiscountPolicy
AppConfig.java MemberServiceImpl.java OrderServiceImpl.java
Copy public class AppConfig {
public MemberService memberService () {
return new MemberServiceImpl( new MemoryMemberRepository()) ;
}
public OrderService orderService () {
return new OrderServiceImpl( new MemoryMemberRepository() , new FixDiscountPolicy()) ;
}
}
Copy public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl ( MemberRepository memberRepository) {
this . memberRepository = memberRepository;
}
}
Copy 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;
}
}
이제 MemberServiceImpl
은 MemoryMemberRepository
를 의존하지 않는다. 단지 MemberRepository
인터페이스만 의존한다.
MemberServiceImpl
은 생성자를 통해 어떤 구현 객체가 주입될 지 알 수 없다. 오직 AppConfig
라는 외부에 의해서 결정된다. MemberServiceImpl
은 이제 의존 관계에 대한 고민을 외부에 맡기고 실행에만 집중하면 된다.
클래스 다이어그램
MemberServiceImpl
이 MemberService
를 구현하고 MemberRepository
에 의존하는 건 동일하다. 대신 AppConfig
가 MemoryMemberRepository
를 생성하고 연결해준다.
DIP가 지켜진다.
MemberServiceImpl
은 MemberRepository
라는 추상에만 의존하면 된다.
관심사의 분리가 이루어졌다.
객체를 생성하고 연결하는 역할과 실행하는 역할이 명확이 분리되었다.
회원 객체 인스턴스 다이어그램
AppConfig
객체는 MemoryMemberRepository
객체를 생성하고 그 참조값을 MemberServiceImpl
을 생성하는 과정에서 생성자로 전달한다.
클라이언트인 MemberServiceImpl
입장에서 보면 의존 관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 즉, 의존 관계 주입이라고 한다.
OrderServiceImpl.java
Copy 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;
}
}
설계를 변경한 후로는 OrderServiceImpl
이 FixDiscountPolicy
를 의존하지 않는다. DiscountPolicy
인터페이스만 의존할 뿐이다.
OrderServiceImpl
입장에선 생성자를 통해 어떤 구현 객체를 주입받을 지 알 수 없다. 오직 AppConfig
에서 결정한다. OrderServiceImpl
은 실행에만 집중하면 된다.
MemberApp.java OrderApp.java MemberServiceTest.java OrderServiceTest.java
Copy public class MemberApp {
public static void main ( String [] args) {
AppConfig appConfig = new AppConfig() ;
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 . getName ());
System . out . println ( "findMember = " + findMember . getName ());
}
}
Copy 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);
System . out . println ( "order.calculatePrice() = " + order . calculatePrice ());
}
}
Copy public class MemberServiceTest {
MemberService memberService;
@ BeforeEach
void beforeEach () {
AppConfig appConfig = new AppConfig() ;
memberService = appConfig . memberService ();
}
}
Copy public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@ BeforeEach
void beforeEach () {
AppConfig appConfig = new AppConfig() ;
memberService = appConfig . memberService ();
orderService = appConfig . orderService ();
}
}
메인 메서드와 테스트 코드도 AppConfig
에서 받아오도록 수정한다.
정리
AppConfig
를 통해 관심사를 확실하게 분리했다.
배역에 맞는 배우를 선택하는 공연 기획자처럼 애플리케이션이 어떻게 동작할지 전체 구성을 책임진다.
각 클래스들은 기능을 실행하는 책임만 지면 된다.
AppConfig 리팩토링
역할이 어떤 게 있고 그 역할이 어떤 걸 구현하는지 한 눈에 보이는 게 중요한데 현재의 AppConfig
로는 그게 보이질 않는다.
AppConfig.java
Copy public class AppConfig {
// 메서드 명을 보는 순간 역할이 드러난다.
// memberService에서는 memberServiceImpl을 쓸 것이다.
public MemberService memberService () {
return new MemberServiceImpl(memberRepository()) ;
}
// memberRepository는 memory로 쓸 것이다.
// 그래서 나중에 다른 repository를 쓰려면 이 코드만 바꾸면 된다.
private MemberRepository memberRepository () {
return new MemoryMemberRepository() ;
}
public OrderService orderService () {
return new OrderServiceImpl(memberRepository() , discountPolicy()) ;
}
private DiscountPolicy discountPolicy () {
return new FixDiscountPolicy() ;
}
}
이제 각 역할과 구현 클래스가 한 눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되었는지 빠르게 파악할 수 있다.
AppConfig.java
Copy public class AppConfig {
...
public OrderService orderService () {
return new OrderServiceImpl(memberRepository() , discountPolicy()) ;
}
private DiscountPolicy discountPolicy () {
// 이 부분만 변경하면 된다.
// return new FixDiscountPolicy();
return new RateDiscountPolicy() ;
}
}
AppConfig
에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy
에서 RateDiscountPolicy
객체로 변경했다. 할인 정책을 변경해도 구성 역할을 담당하는 AppConfig
만 건드리면 된다. 사용 영역은 하나도 변경되지 않는다.
구성 영역은 당연히 변경된다. 공연 기획자는 공연 참여자를 다 알아야 하듯, AppConfig
는 구현 객체를 모두 알아야 한다.