꼬물꼬물

스프링 핵심 원리 이해2 - 객체 지향 원리 적용 [관심사의 분리, AppConfig 리팩토링] 본문

스터디/스프링 핵심 원리 - 기본편

스프링 핵심 원리 이해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 은 이제부터 의존관계에 대한 고민은 외부에 맡기고, 실행에만 집중할 수 있다.

 

객체의 생성과 연결은 AppConfig가 담당한다.

  • 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 리팩토링

현재 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를 보면 역할과 구현 클래스가 한눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.