OCP와 DIP, 의존관계 주입

Jaca·2021년 8월 3일
0

SOLID

클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 정리

  • SRP: 단일 책임 원칙(single responsibility principle)

  • OCP: 개방-폐쇄 원칙 (Open/closed principle)

  • LSP: 리스코프 치환 원칙 (Liskov substitution principle)

  • ISP: 인터페이스 분리 원칙 (Interface segregation principle)

  • DIP: 의존관계 역전 원칙 (Dependency inversion principle)


SOLID중 OCP와 DIP에 대해 알아보자

OCP란, 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다 의 원칙이다.
하지만 확장을 하려면, 당연히 기존 코드를 변경해야한다.
이 점을 극복하기 위해 다형성을 활용해야한다.
인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현 지금까지 배운 역할과 구현의 분리를 생각해보자

public class MemberService {
	private MemberRepository memberRepository = new MemoryMemberRepository();
public class MemberService {
	//private MemberRepository memberRepository = new MemoryMemberRepository();
    private MemberRepository memberRepository = new JdbcMemberRepository();

MemberService 클라이언트가 구현 클래스를 직접 선택하고 있다.
구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다.
이를 해결하기 위해 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요하다.

그런데 OCP에서 설명한 MemberService는 인터페이스에 의존하지만, 구현 클래스도 동시에 의존한다.
MemberService 클라이언트가 구현 클래스를 직접 선택하며 DIP를 위반하고 있다.

이는 객체 지향의 핵심은 다형성이지만,
다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다.
다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다.
다형성 만으로는 OCP, DIP를 지킬 수 없다.
뭔가 더 필요하다.

라는 결론을 얻을 수 있다.

이는 스프링의 DI 컨테이너 의존관계 주입 을 통해 해결해보자.

예제

요구사항 도메인

위 예제의 할인 정책 역할만 살펴볼 것이다.

할인 정책 역할의 추상 인터페이스 DiscountPolicy와 구체 클래스 FixDiscountPolicy, RateDiscountPolicy가 있을 때,

서비스 클래스를 보자,

 public class OrderServiceImpl implements OrderService {
 
      private final MemberRepository memberRepository = new MemoryMemberRepository();
      private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
      
      @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);
      }
}

문제점

위와 같이 설계 되있다면, 주문을 생성하는 클래스가 아래와 같은 문제를 가지고 있다.

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

클래스 의존관계를 분석해 보면, 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.

  • 추상(인터페이스) 의존: DiscountPolicy
  • 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
    이는 현재 OrderServiceImpl 은 OrderService와 할인 정책을 결정하는 책임 또한 가지고 있다(SRP 위반).

또, 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다. 따라서 OCP를 위반한다.

해결방안

그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.


public class OrderServiceImpl implements OrderService {
      //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
      private DiscountPolicy discountPolicy;
}

인터페이스에만 의존하도록 설계와 코드를 변경했다.
그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까?
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImplDiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.

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

public class AppConfig {
      public MemberService memberService() {
          return new MemberServiceImpl(new MemoryMemberRepository());
}
      public OrderService orderService() {
          return new OrderServiceImpl(
          	new MemoryMemberRepository(),
            	new FixDiscountPolicy());
 } 
 

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

  • MemberServiceImp
  • MemoryMemberRepositor
  • OrderServiceImp
  • FixDiscountPolic

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

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

    @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 은 구체 클래스를 의존하지 않는다.

OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정된다.

OrderServiceImpl는 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.

AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리되었다.

FixDiscountPolicy > RateDiscountPolicy 로 변경해도 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다.

profile
I am me

0개의 댓글