스터디/스프링 핵심 원리 - 기본편
스프링 핵심 원리 이해2 - 객체 지향 원리 적용 [관심사의 분리, AppConfig 리팩토링]
멩주
2022. 9. 11. 03:30
관심사의 분리
- 애플리케이션을 공연, 각각의 인터페이스는 배역이라고 생각하자. 실제 배역에 맞는 배우를 선택하는 것은 누구일까?
- 역할을 누가할지는 배우가 정하는 것이 아니다. 이전 코드는 마치 역할(인터페이스)을 하는 남배우가 이 다른 역할을 할 여배우를 직접 초빙하는 것과 같다. -> 다양한 책임을 갖게 됨.
관심사를 분리하자!
- 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다.
- 남배우는 어떤 여배우가 선택되더라도 똑같은 공연을 해야한다.
- 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 "공연 기획자"가 나와야한다.
- 공연 기획자를 만들어, 배우와 공연 기획자의 책임을 확실하게 분리하자!
AppConfig 등장
- 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 갖는 별도의 설정 클래스를 만들자
MemberServiceImpl 수정
public class MemberServiceImpl implements MemberService{
// ctl + shift + Enter하면 세미콜론까지 자동완성
private final MemberRepository memberRepository;
// 직접 new 하는 것이 아닌 생성자를 통해 의존성 주입
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
}
- MemoryMemberRepository에 대한 구현체 설명이 사라졌다.
- 오직 MemberRepository라는 인터페이스만 존재. <- 추상화에 의존. DIP 준수
OrderServiceImpl 수정
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;
}
}
- 오직 인터페이스만 존재한다. 구현체에 대한 설명이 없음 <- 추상화에 의존. DIP 준수
- 이 곳에서는 FIx가 들어올지 Rate가 들어올지 알지 못한다.
AppConfig 추가
// 생성자를 통해 주입한다.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
- AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성(new)한다.
- MemberServiceImple
- MemoryMemberRepository
- OrderServiceImple
- FixDiscountPolicy
- AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해 주입(연결) 한다.
- MemberServiceImple -> MemoryMemberRepository
- OrderServiceImple -> MemoryMemberRepository, FixDiscountPolicy
- 설계 변경으로 MemberSrviceImpl은 MemoryMemberRepository를 의존하지 않는다. <- DIP 성공
- 단지 MemberRepository 인터페이스만 의존한다.
- MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지 알 수 없다.
- MemberServiceImpl 의 생성자를 통해 주입될 구현 객체는 외부(AppConfig)에서 결정한다,
- MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고, 실행에만 집중할 수 있다.
- DIP 완성: MemberServiceImpl은 MemberRepository인 추상에만 의존하며, 구체 클래스를 몰라도 된다.
- 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
🚨 ISP(Interface Substitution Principle, 인터페이스 분리 원칙): 하나의 범용 인터페이스보다 각각의 분리된 인터페이스가 좋다.
- AppConfig 객체는 MemoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다.
- 클라이언트인 MemberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것과 같아 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다. <- 객체는 협력하는 관계
OrderServiceImple
- 설계 변경으로 OrderServiceImpl은 FixDiscountPolicy를 의존하지 않는다
- 단지 DiscountPolicy 인터페이스만 의존한다.
- OrderSreviceImpl 입장에서 생성자를 통해 어떤 구현 객체가 주입될지 알 수 없다.
- OrderServiceImpl의 생성자를 통해 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정한다.
- OrderServiceImpl은 이제 실행에만 집중한다.
- OrderServiceImpl에는 MemoryMemberRepository, FixDiscountPolicy 객체의 의존관계가 주입된다.
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
- 이런식으로 AppConfig에서 주입받아 진행한다.
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
// 테스트 전 실행
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
}
- Test시, 주입받는 방법.
- 미리 선언 후 테스트 전 주입받는다.
정리
- AppConfig를 통해 관심사를 확실하게 분리. (객체 생성과 연결 / 실행)
- 배역, 배우 떠올리기
- AppConfig는 공연 기획자다.
- AppConfig는 구체 클래스를 선택해 배역에 맞는 담당 배우를 선택한다. 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다.
- 각 배우들은 담당 기능을 실행하는 책임만 지면 된다.
- 즉, OrderServiceImpl은 기능을 실행하는 책임만 지면 된다.
- 구체 클래스에 대한 고민은 필요없다!
AppConfig 리팩토링
// 생성자를 통해 주입한다.
public class AppConfig {
public MemberService memberService(){ // 역할
return new MemberServiceImpl(memberRepository());
}
// ctrl + alt + M으로 메소드 뽑기
private MemberRepository memberRepository() { // 구현
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
private DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
- 역할과 구현이 잘 나눠야 한다.
- 이전과 달리 new MemoryMemberRepository() 부분이 중복 제거되었다. 이제 MemoryMemberRepository를 다른 구현체로 변경할 때, 한 부분만 변경하면 된다.
- AppConfig를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.