[Spring] 객체 지향 원리 적용 - 관심사 분리

jy9922·2022년 8월 18일
0

Spring

목록 보기
15/34
post-thumbnail

관심사의 분리

애플리케이션을 하나의 공연이라고 생각해봅시다!

  • 각각의 인터페이스는 배역(배우 역할)이다.
  • 실제 배역을 맞는 배우를 선택하는 것을 누가 할까?
    • 배우가 직접 역할에 맞는 배우를 정하는 것이 아니다.

이전에서 우리 코드는...🙄

public class MemberServiceImp implements MemberService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    ...

구현체가 구현체를 호출했다.
즉, 로미오 역할을 하는 디카프리오가 줄리엣 역할을 하는 배우를 직접 초빙하는 것과 같다.

즉, 로미오 역할의 디카프리오는 공연도 해야하고 배우를 초빙해야하는 다양한 책임을 가지고 있다.

💡 관심사를 분리하자

  • 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다.
  • 디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야 한다.
  • 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자 역할이 있어야 한다.

    공연 기획자를 만들고, 배우와 공연 기획자와 책임을 확실히 분리하자.

AppConfig 등장

  • 애플리케이션의 전체 동작 방식을 구성( config )하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만들어보자.

생성자 주입

  • AppConfig
    • 구현 객체를 생성하고 연결하는 책임을 가지는 설정 클래스
public class AppConfig {
    public MemberService memberService(){
        return new MemberServiceImp(new MemoryMemberRepository());
    }

    public OrderService orderService(){
        return new OrderServiceImp(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}
  • AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
    • MemberServiceImpl
    • MemoryMemberRepository
    • OrderServiceImpl
    • findDiscountPolicy

  • AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해준다.
    • MemberServiceImplMemoryMemberRepository
    • OrderServiceImplMemoryMemberRepository, FixDiscountPolicy

MemberServiceImpl

public class MemberServiceImp implements MemberService{

    private final MemberRepository memberRepository;

    // 생성자를 통해서 MeberRepository 구현체가 무엇이 들어갈지 설정해준다.
    // MemberServiceImp 코드에 MemoryMemberRepository가 없는 것을 확인할 수 있다.
    // 추상화에 의존하면서 DIP를 지킬 수 있게 된다.
    // 이를 생성자 주입이라고 한다.
    public MemberServiceImp(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    ...
    }

설계 변경으로 MemberServiceImplMemoryMemberRepository를 의존하지 않는다.

  • MemberRepository 인터페이스만 의존한다.
    • MemberServiceImpl 입장에서 생성자를 통해 이전 구현 객체가 들어올지 주입될지 알 수 없다.
    • MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 외부 AppConfig에서 결정된다.
    • MemberServiceImpl의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.

  • 객체의 생성과 연결은 AppConfig가 담당한다.
  • DIP 완성 - MemberServiceImplMemberRepository인 추상에만 의존하면 된다.
    구체 클래스를 몰라도 된다.
  • 관리사의 분리 - 객채를 생성하고 연결하는 역할과 실행하는 역할이 정확히 분리된다.

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

public class AppConfig {
    public MemberService memberService(){
        return new MemberServiceImp(new MemoryMemberRepository());
    }

    public OrderService orderService(){
        return new OrderServiceImp(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}
  • AppConfig 객체는 MemoryMemberRepository 객체를 생성하고 그 참조값을 MemberServiceImpl을 생성하면서 생성자로 전달한다.
  • 클라이언트인 MemberServiceImp 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것과 같다고 해서 DI(의존관계 주입, 의존성 주입)이라고 한다.

OrderServiceImpl

  • OrderServiceImp
public class OrderServiceImp implements OrderService{
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImp(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
    ...
  • OrderServiceImplFixDiscountPolicy인 구체 클래스를 의존하지 않는다.
  • DiscountPolicy 추상 클래스에만 의존한다.
  • OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지) 알 수 없다.
  • OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부 (AppConfig )에서 결정한다.
  • OrderServiceImpl 은 이제부터 실행에 대한 책임만 가지게 된다.
  • OrderServiceImpl 에는 MemoryMemberRepositoryFixDiscountPolicy 객체의 의존관계가 주입된다.

AppConfig 실행 및 Test

AppConfig 실행

  • MemberApp
public class MemberApp {

    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
//        MemberService memberService = new MemberServiceImp();
        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("find Member = " + findMember.getName());
    }
}
  • OrderApp
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.calculator = "+ order.calculatorPrice());
    }
}

Test 코드

@BeforeEach를 통해 각 테스트를 실행하기 전에 AppConfig를 호출한다.

  • MemberServiceTest
public class MemberServiceTest {

    MemberService memberService;
    
    // 각 테스트 실행 전에 무조건 실행되는 부분
    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }
    @Test
    void join(){
        //given
        Member member = new Member(1L, "memberA", Grade.VIP);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then
        Assertions.assertThat(member).isEqualTo(findMember);
    }
}
  • OrderTest
public class OrderServiceTest {
    MemberService memberService;
    OrderService orderService;

    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
        orderService = appConfig.orderService();
    }

    @Test
    void createOrder(){
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}
  • AppConfig를 통해서 관심사를 확실히 분리한다.
  • AppConfig는 공연 기획자!
  • AppConfig는 구체 클래스를 선택 → 배역에 맞는 담당 배우를 선택한다.
  • OrderServiceImpl 등과 같은 구체 클래스는 기능을 실행하는 책임만 진다 → 각 배우들은 담당 기능을 실행하는 책임만 지면 된다.

AppConfig 리팩터링

AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안 보인다.

  • 리팩터링 전 AppConfig
public class AppConfig {
    public MemberService memberService(){
        return new MemberServiceImp(new MemoryMemberRepository());
    }

    public OrderService orderService(){
        return new OrderServiceImp(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}
  • 리팩터링 후
public class AppConfig {
    public MemberService memberService(){
        return new MemberServiceImp(memberRepository());
    }

    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService(){
        return new OrderServiceImp(memberRepository(), discountPolicy());
    }

    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }

}
  • new MemoryMemberRepository() 이 부분이 중복 제거 되었다.
    • new MemoryMemberRepository() 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.
  • AppConfig를 보면 역할과 구현 클래스가 한 눈에 들어온다.
    • 애플리케이션 전체 구성이 어떻게 되었는지 빠르게 파악할 수 있다.

0개의 댓글