본문 바로가기

Spring

[Spring] OCP, DIP 원칙을 지켜주는 AppConfig

새로운 할인 정책 적용과 문제점

할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.

private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
// 할인 정책 변경, OCP 원칙 위반, DIP 원칙 위반(추상화 뿐만 아니라 구체화 의존)
private final DiscountPolicy discountPolicy = new RateDiscountPolicy()

 

문제점 발견

  • 우리는 역할과 구현을 충실하게 분리했나? => OK
  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리했나? => OK
  • OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했나? =>그렇게 보이지만 사실은 아니다.

 

DIP: 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데?

  • 클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
    • 추상(인터페이스) 의존: DiscountPolicy
    • 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
  • OCP: 변경하지 않고 확장할 수 있다고 했는데!
  • 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다! 따라서 OCP를 위반한다.

 

 

왜 클라이언트 코드를 변경해야 할까?

클래스 다이어그램으로 의존관계를 분석해보자.

 

기대했던 의존관계

 

실제 의존관계

 

클라이언트인 OrderServiceImpl 이 DiscountPolicy 인터페이스 뿐만 아니라

FixDiscountPolicy 인 구체 클래스도 함께 의존하고 있다. => DIP 위반

 

 

정책 변경

 

중요!

그래서 FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간

OrderServiceImpl 의 소스코드도 함께 변경해야 한다! => OCP 위반

 

 

어떻게 문제를 해결할 수 있을까?

  • 클라이언트 코드인 OrderServiceImpl 은 DiscountPolicy 의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존한다.
  • 그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
  • DIP 위반 => 추상에만 의존하도록 변경(인터페이스에만 의존)
  • DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.
  • 즉, 구체 클래스를 지워버리면 된다!

 

 

인터페이스에만 의존하도록 설계를 변경

 

 

인터페이스에만 의존하도록 코드 변경

// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private final DiscountPolicy discountPolicy;
  • 인터페이스에만 의존하도록 설계와 코드를 변경했다.
  • 그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까?
  • 실제 실행을 해보면 NPE(null pointer exception)가 발생한다.

해결방안

  • 누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.

 

 

AppConfig 등장

애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 

연결하는 책임을 가지는 별도의 설정 클래스를 만들자.

package hello.core;

import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {


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

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

 

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

  • MemberServiceImpl
  • MemoryMemberRepository
  • OrderServiceImpl
  • FixDiscountPolicy

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

  • MemberServiceImpl => MemoryMemberRepository
  • OrderServiceImpl => MemoryMemberRepository , FixDiscountPolicy

 

각 클래스에 생성자를 만든다.

 

MemberServiceImpl - 생성자 주입

package hello.core.member;

public class MemberServiceImpl implements MemberService{
    
    // MemberRepository 인터페이스만 의존한다.
    private final MemberRepository memberRepository;
	
    // 생성자 주입
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);

    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
  • 설계 변경으로 MemberServiceImpl 은 MemoryMemberRepository 를 의존하지 않는다!
  • 단지 MemberRepository 인터페이스만 의존한다.
  • MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
  • MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정된다.
  • MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.

 

클래스 다이어그램

 

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

 

 

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

  • appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl 을 생성하면서 생성자로 전달한다.
  • 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다

 

OrderServiceImpl - 생성자 주입

public class OrderServiceImpl implements OrderService{

    // 인터페이스만 의존하도록 변경 => DIP 원칙
    private final DiscountPolicy discountPolicy;
    private final MemberRepository memberRepository;
	
    // 생성자 주입
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
  • 설계 변경으로 OrderServiceImpl 은 FixDiscountPolicy 를 의존하지 않는다!
  • 단지 DiscountPolicy 인터페이스만 의존한다.
  • OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
  • OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부( AppConfig )에서 결정한다.
  • OrderServiceImpl 은 이제부터 실행에만 집중하면 된다.
  • OrderServiceImpl 에는 MemoryMemberRepository , FixDiscountPolicy 객체의 의존관계가 주입된다.

 

 

AppConfig 실행

public class MemberServiceTest {

    MemberService memberService;

    // 테스트 코드에서 @BeforeEach 는 각 테스트를 실행하기 전에 호출된다.
    // 테스트가 2개 있다면 2번 실행됨
    @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);
    }
}

 

 

AppConfig 리팩터링

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

 

기대하는 그림

 

 

리팩터링 전

package hello.core;

import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {


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

    public OrderService orderService() {
        return new OrderServiceImpl(
                new MemoryMemberRepository(),
                new FixDiscountPolicy());
    }
}
  • 중복을 제거하고, 역할에 따른 구현이 보이도록 리팩터링 하자.

 

리펙토링 후

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {


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

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

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

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

 

 

새로운 구조와 할인 정책 적용

  • 처음으로 돌아가서 정액 할인 정책을 정률% 할인 정책으로 변경해보자.
  • FixDiscountPolicy => RateDiscountPolicy
  • 어떤 부분만 변경하면 되겠는가?

AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 

객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다.

FixDiscountPolicy RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 

사용 영역은 전혀 영향을 받지 않는다.

 

할인 정책 변경 구성 코드

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {


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

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

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

    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy(); // 한줄 변경
    }
}

 

  • AppConfig 에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy => RateDiscountPolicy 객체로 변경했다.
  • 이제 할인 정책을 변경해도, 애플리케이션의 구성 역할을 담당하는 AppConfig만 변경하면 된다. 
  • 클라이언트 코드인 OrderServiceImpl 를 포함해서 사용 영역의 어떤 코드도 변경할 필요가 없다.
  • 구성 영역은 당연히 변경된다. 
  • 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자. 
  • 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.

 

 

 

'Spring' 카테고리의 다른 글

[Spring] 스프링 컨테이너  (0) 2024.06.12
[Spring] IoC, DI 컨테이너  (0) 2024.06.12
[Spring] 객체 지향 설계 SOLID 원칙  (0) 2024.06.10
[Spring] AOP  (0) 2024.06.10
[Spring] 스프링 DB 접근 기술  (0) 2024.06.10