Java

[Java] 객체지향적 설계의 의존 관계(DIP, OCP, DI)

뚜코맨 2024. 1. 2. 18:46

김영한의 스프링 강의를 보면서 클론 코딩을 해보던 중에

 

할인 정책을 변경하기 위해 "OrderServiceImpl"클래스의 코드를 수정하였다.

 

강의를 듣는 중에 이렇게 하면 될것이다? 라는 생각으로 구현을 했는데 강의 도중

 

이 부분은 객체지향적 설계의 DIP 위반을 했다는 점이다.

 

  1. 역할(interface)와 구현체(impl)을 충실하게 분리 했다. -> OK!
  2. 다형성 활용, 인터페이스, 구현 객체를 분리 했다. -> OK!
  3. OCP, DIP 같은 객체 지향적 설계원칙을 준수했다. -> 그렇게 느꼈지만, 사실은 아니다.

 

왜 문제일까?

 

DIP: 주문서비스 클라이언트(OrderServiceImpl)은 'DiscountPolicy' 인터페이스에 의존하여 DIP 원칙을 지킨것 같은데?

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

 

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

 

이게 우리가 설계하면서 기대했던 의존관계이지만

 

실제 의존 관계를 코드를 통해 알아보자

 

코드를 보면 OrderServiceImpl이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy인 구체(구현) 클래스도 함께 의존하고 있다 -> DIP 위반

 

 

우리는 정책이 변경되었기 때문에 OrderServiceImpl의 소스코드를 변경해야 한다.

 

위처럼 우리가 FixDiscountPolicy(기존 정책 구현체)를 RateDiscountPolicy(변경된 정책 구현체)로 변경하는 순간!

이것이 OCP 위반이다.

 

그렇다면 어떻게 문제를 해결 할 수 있는가?

 

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

 

이렇듯 인터페이스에만 의존하도록 코드를 한번 변경해보자.

 

 

이러면 어떻게 될까?

 

그렇다. 당연히 안된다. NPE가 발생한다.

 

"구현체가 없는데 어떻게 코드를 실행할 수 있을까"

 

이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주면 해결이 된다.

 

여기서 AppConfig의 개념이 나온다.

 

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

 

 

생성자 주입 방식을 통해 AppConfig에서 (위 코드를 따르면) 누군가가 MemberService, OrderService를 호출하면 new를 통해 구현체를 주입을 시켜주는 것이다.

 

 

클라이언트 코드인 OrderServiceImpl에서 private final로 선언을 하고 값은 기본 생성자에서 할당을 해주는 것이다.

지금 현재 클라이언트 코드는 어떤 구현체가 들어올지 모르고 그냥 어떤 구현체가 들어오던, 주어진 자기 일에만 충실하게 되는 것이다.

 

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

  • MemberServiceImpl
  • MemoryMemberRepository
  • OrderServiceImpl
  • FixDiscountImpl

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

  • MemberServiceImpl -> MemoryMemberRepository
  • OrderSerivceImpl -> MemoryMemberRepository, FixDiscountPolicy
  1. 설계 변경으로 MemberServiceImpl은 MemoryMemberRepository를 의존하지 않는다.
  2. 단지 MemberRepository 인터페이스만 의존하고 있다.
  3. MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
  4. MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
  5. MemberServiceImpl은 이제부터 '의존관계에 대한 고민은 외부'에 맡기고 오로지 실행에만 집중할 수 있다.

 

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

 

클라이언트 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서

 

DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라고 한다. 매우 중요한 개념이다.

 

이제 AppConfig를 생성했으니 클라이언트 코드들에 반영을 해보자.

 

구현체를 생성하는 부분이 AppConfig에서 가져오는 부분을 볼 수 있을 것이다.

 

AppConfig는 구체 클래스를 선택하고, 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다!

이제 각 구현체(Impl)들은 실행하는 기능만 수행하면 된다!

 

오늘은 객체지향적 설계에 따라서 오로지 스프링의 도움없이 퓨어 자바 코드로 DI(의존성 주입)에 대해서 알아봤다.

 

그냥 "이렇게 하면 구현이 될거야" 라는 생각으로 지금까지 구현을 해왔던 것 같다.

근데 이렇게 객체지향적 설계 관점에서 보면서 하나하나씩 왜 이렇게 구현을 해야하는지,

어떤 부분이 객체지향적 설계를 위반을 하고 있는지 딥하게 알아보았는데,

아무렇지않게 넘어갔던 소스들이 다 이유가 있던 구현이였던 것을 느낀다...