관심사의 분리

연극 역할을 누가 할지는 배우가 아니라 기획자 결정한다. 배우가 캐스팅까지 담당한다면 너무 다양한 책임을 가진 것이다.

  • 배우는 본인 배역을 잘 수행하는 것에만 집중한다.

  • 배우는 어떤 상대 배우가 오더라도 똑같이 공연할 수 있다.

  • 공연을 구성하고, 배우를 섭외하고, 역할을 지정하는 책임은 기획자가 한다.

  • 기획자를 만들어 배우와 기획자의 책임을 확실히 분리해야 한다.

AppConfig

애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고 연결하는 책임을 가진 별도의 설정 클래스를 만든다.

AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.

  • MemberServiceImpl

  • MemoryMemberRepository

  • OrderServiceImpl

  • FixDiscountPolicy

생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)한다.

  • MemberServiceImpl → MemoryMemberRepository

  • OrderServiceImpl → MemoryMemberRepository, FixDiscountPolicy

public class AppConfig {

  public MemberService memberService() {
    return new MemberServiceImpl(new MemoryMemberRepository());
  }

  public OrderService orderService() {
    return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
  }

}

이제 MemberServiceImplMemoryMemberRepository를 의존하지 않는다. 단지 MemberRepository 인터페이스만 의존한다.

MemberServiceImpl은 생성자를 통해 어떤 구현 객체가 주입될 지 알 수 없다. 오직 AppConfig라는 외부에 의해서 결정된다. MemberServiceImpl은 이제 의존 관계에 대한 고민을 외부에 맡기고 실행에만 집중하면 된다.

클래스 다이어그램

MemberServiceImplMemberService를 구현하고 MemberRepository에 의존하는 건 동일하다. 대신 AppConfigMemoryMemberRepository를 생성하고 연결해준다.

  • DIP가 지켜진다.

    • MemberServiceImplMemberRepository라는 추상에만 의존하면 된다.

  • 관심사의 분리가 이루어졌다.

    • 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확이 분리되었다.

회원 객체 인스턴스 다이어그램

AppConfig 객체는 MemoryMemberRepository 객체를 생성하고 그 참조값을 MemberServiceImpl을 생성하는 과정에서 생성자로 전달한다.

클라이언트인 MemberServiceImpl 입장에서 보면 의존 관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 즉, 의존 관계 주입이라고 한다.

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;
  }
}

설계를 변경한 후로는 OrderServiceImplFixDiscountPolicy를 의존하지 않는다. DiscountPolicy 인터페이스만 의존할 뿐이다.

OrderServiceImpl 입장에선 생성자를 통해 어떤 구현 객체를 주입받을 지 알 수 없다. 오직 AppConfig에서 결정한다. OrderServiceImpl은 실행에만 집중하면 된다.

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());
  }
}

메인 메서드와 테스트 코드도 AppConfig에서 받아오도록 수정한다.

정리

  • AppConfig를 통해 관심사를 확실하게 분리했다.

    • 배역에 맞는 배우를 선택하는 공연 기획자처럼 애플리케이션이 어떻게 동작할지 전체 구성을 책임진다.

  • 각 클래스들은 기능을 실행하는 책임만 지면 된다.

AppConfig 리팩토링

역할이 어떤 게 있고 그 역할이 어떤 걸 구현하는지 한 눈에 보이는 게 중요한데 현재의 AppConfig로는 그게 보이질 않는다.

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();
  }

}

이제 각 역할과 구현 클래스가 한 눈에 들어온다. 애플리케이션 전체 구성이 어떻게 되었는지 빠르게 파악할 수 있다.

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는 구현 객체를 모두 알아야 한다.

Last updated