관리 메뉴

공부 기록장 💻

[Spring] 회원 주문 서비스 예제 - 새로운 할인 정책 추가 및 확장 설계, 관심사의 분리와 제어의 역전, 의존 관계 주입 (AppConfig를 통한 DIP, OCP, SRP 적용, IoC, DI) 본문

# Tech Studies/Java Spring • Boot

[Spring] 회원 주문 서비스 예제 - 새로운 할인 정책 추가 및 확장 설계, 관심사의 분리와 제어의 역전, 의존 관계 주입 (AppConfig를 통한 DIP, OCP, SRP 적용, IoC, DI)

dream_for 2023. 1. 23. 23:38

인프런 - 스프링 핵심 원리 기본편 정리


 

 

 

회원, 주문과 할인 도메인을 설계하고 간단한 구현, 테스트를 해보았다. 

고정 할인 정책에서, 새로운 정률 할인 정책이 등장함과 동시에, 객체 지향 원리를 적용하지 못한 기존 설계의 문제점을 파악하고 설계를 변경해 나가자.

그리고 좋은 객체 지향 설계의 원칙인 SOLID 원칙 중, 다음의 3가지 원칙을 적용해 나가는 과정을 이해해보자.

1. DIP (Dependency Injection Principle, 의존 관계 역전 원칙) - 구현체가 아닌 인터페이스에만 의존하도록, 외부에서 의존 관계를 주입하는 역할을 따로 분리하자.
2. OCP (Open-Closed Principle, 개방-폐쇄 원칙) -  클라이언트의 코드를 변경하지 않고, 구현체를 주입하는 외부의 코드만 변경함으로써, 확장에는 열려 있으면서 변경에는 닫혀 있는 원칙을 준수하자.
3. SRP (Single Responsibility Principle, 단일 책임 원칙) - 어떤 구현체를 선택할지를 담당하는 구성 을 담당하는 역할과 각 구현 을 담당하는 역할을 철저히 분리하여, 자신의 기능만 충실히 수행하도록 책임을 분리하자.  

 

 

 

새로운 정률 할인 정책 추가 및 확장 설계

 

지난번 설계했던 주문 서비스 객체 OrderServiceImpl은 할인 정책 인터페이스인 DiscountPolicy를 의존하고 있는 상태이다. DiscountPolicy의 구현체로 고정 할인 정책인 FixedDiscountPolicy 만 사용했다면, 이번에는 정률 할인 정책인 RatedDiscountPolicy가 새로 등장하는 상황을 가정하자. 

 

주문 서비스의 의존 관계를 나타내는 다이어그램은 다음과 같다.

 

 

RatedDiscountPolicy 구현 클래스

 

RatedDiscountPolicy 구현 클래스를 다음과 같이 작성해보자.

할인율을 나타내는 int 형 변수 discountPercent에 값 10을 적용하여, 10% 할인을 나타내자.

price() 메서드에서는 VIP 등급인 회원에게는 기존 금액 price에 10% 할인된 가격을, VIP 등급이 아닌 회원에게는 0을 반환하게 된다.

 

 

 

Test

등급에 따른 정률 할인 정책 적용 유무에 대한 테스트 케이스를 만들어보자.

테스트 실행 창에서 각 테스트가 나타내는 바를 좀 더 명확히 하기 위해 @DisplayName 어노테이션을 적용하였다. 

 

10,000원의 가격을 구매한 VIP 등급 회원 memberA 에게는 1,000원의 할인가가 적용되며, VIP가 아닌 회원에게는 할인가가 적용되지 않는 vip_o(), vip_x() 테스트 메서드를 다음과 같이 구현하였다.

 

 

테스트 실행 결과는 다음과 같이 성공적이다.

 

 


 

할인 정책의 변경에 따른 주문 서비스 코드 변경

 

새로운 할인 정책의 구현체인 RatedDiscountPolicy 에 대한 테스트를 마쳤다.

이제 전체 주문 서비스에서 기존의 고정 할인 정책 FixedDiscountPolicy가 아닌, 정률 할인 정책 RatedDiscountPolicy를 적용하여 모든 회원의 주문을 처리하도록 해야 한다.

이를 코드로 구현하기 위해서는 다음과 같이 주문 서비스 구현체 OrderServiceImpl의 코드를 변경함으로써 할인 정책을 바꿔 사용할 수 있겠다. 

 

 

그런데, 할인 정책이 변경되었다고 주문 서비스 구현체에서 코드를 변경하는 것이 과연 객체 지향 설계를 준수하는 방법이 맞을까? 

 

 

 

객체 지향 설계 원칙을 위반하는 문제점 발생! 

 

인터페이스와 구현 객체를 분리하여 다형성도 활용하며, 역할 구현을 충실하게 분리했다.

그러나 OCP, DIP 와 같은 객체 지향 설계 원칙을 충실히 준수했다고 볼 수는 없다. 

각각의 관점에서, 객체 지향 설계 원칙을 위반한 이유를 자세히 살펴보자.

 

1. DIP 위반

OrderServiceImpl 구현체는 DiscountPolicy 인터페이스에 의존하고 있으며,

동시에 FixedDiscountPolicy, RatedDiscountPolicy 구현체 클래스에도 의존하고 함께 있었다는 점에서, DIP를 위반하고 있음을 확인할 수 있다.

(SOLID의 원칙 중 하나인 Dependency Inversion Principle, 구현 클래스에 의존하지 말고, 인터페이스에 의존하여 유연하게 구현체를 변경할 수 있는 객체 지향 설계 원칙을 위반)

 

 

2. OCP 위반

FixedDiscountPolicy에서 RatedDiscountPolicy로 할인 정책의 구현체를 바꿔야 하는 순간, OrderServiceImpl 내 코드를 변경해야 한다. 바로 위 코드에서 살펴본 바와 같이, DiscountPolicy 인터페이스의 구현체였던 FixedDiscountPolicy 가 아닌 RatedDiscountPolicy 객체를 생성해야 하는 코드로 변경을 해주어야 한다.

기능을 확장해서 변경하는 상황에서, 클라이언트 코드에 영향을 주고 있다는 점에서 OCP를 위반하고 있다.

(SOLID의 원칙 중 하나인 Open-Closed Principle, 변경에는 닫혀 있으면서 확장에는 열려 있어야 하는 객체 지향 설계의 원칙을 위반)

 

 

 


 

문제 해결 , 관심사의 분리

 

문제 해결 방법은, DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존 관계를 변경하는 것이다.

주문 서비스 구현체인 OrderServiceImpl에서 코드를 변경하여 할인 정책의 구현 객체를 변경하지 않도록 변경해야 한다.

 

 

 

 

코드로 살펴보면, 다음과 같이 주문 서비스 구현체에서 할인 정책 구현 클래스가 아닌, DiscountPolicy 인터페이스에만 의존하도록 코드를 변경해야 한다.

 

 

이제 인터페이스만 의존하도록 설계와 코드를 잘 변경했다. 그러나 구현체가 없기 때문에, 분명 실행을 하는 경우 NPE(Null Point Exception) 이 발생한다. 

이를 해결하기 위해서는, OrderServiceImpl 클라이언트에서 DiscountPolicy 구현체를 생성하는 대신, 외부의 누군가가 OrderServiceImpl에 DiscountPolicy 구현 객체를 대신 생성하고 주입해주어야 한다.

 

구현 객체를 대신 생성하여 주입한다는 의미가 무엇일까?

 

 

공연에 비유를 들어보자

로미오와 줄리엣 이라는 연극에 대해, 로미오 그리고 줄리엣 각 배역(역할, 인터페이스)에 맞는 각 배우(구현체)들을 교체하는 역할을 맡아 전담하는 공연 기획자 를 초빙해야 하는 것과 비슷하다.

즉 배우는 연기하는 것에만 집중하고, 배역의 배우를 섭외하고, 각 날짜에 배정하는 또다른 책임은 공연 기획자가 집중할 수 있도록, 책임을 분리해야 한다.

 

즉, 각자 자신의 역할을 수행하는 것에만 집중할 수 있도록, 관심사를 분리하는 것이 핵심이다.

 

애플리케이션도 마찬가지로, 실행되는 객체들은 본인의 역할만 수행하고, 어떠한 구현체들이 각 인터페이스에 할당될지는 해당 객체 내에서 수행해야 해서는 안 된다. 

위의 주문-할인 정책의 예로 다시 정리해보자면, 주문 서비스 구현체인 OrderServiceImpl은 할인 정책 인터페이스가 어떤 구현체를 선택하게 되는지에 대해 관심을 가지지 않게끔 관심사를 분리하여, 주문에 관한 로직에만 집중할 수 있도록 해야 한다는 것이다. 

 

 

 

AppConfig 설정으로 관심사를 관리하기

 

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

 

 

 

AppConfig의 역할을 정리해보자면 다음과 같다.

 

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

  • MemberServiceImpl (구체적인 회원 서비스 객체)
  • MemoryMemberRepository (구처젝인 회원 리포지토리 객체)
  • OrderServiceImpl (구체적인 주문 서비스 객체)
  • FixedDiscountPolicy (구체적인 할인 정책 객체)

 

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

  • MemberServiceImpl에 MemoryMemberRepository를 주입 (회원 서비스 객체가 참조할 회원 리포지토리 객체)
  • OrderServiceImpl에 MemoryMemberRepository와 RatedDiscountPolicy를 주입 (주문 서비스 객체가 참조할 회원 리포지토리 객체와 할인 정책 객체)

 

IoC, 제어의 역전 그리고 DI 컨테이너 

이렇게 AppConfig가 등장함으로써, 제어의 역전 IoC(Inversion of Control) 이 발생하였다. 즉 모든 제어의 흐름을 ApppConfig가 가지게 된 것이다.

이전에는 클라이언트 구현 객체인 OrderServiceImpl이 스스로 필요한 DiscountPolicy의 서버 구현 객체를 생성하여 스스로 프로그램의 제어 흐름을 조종했다면, 이제는 AppConfig가 이 역할을 하게 되었다. 구현 객체는 이제 필요한 인터페이스들을 호출하지만, 어떤 구현 객체들이 실행될지는 모르는 상황에서 자신의 로직을 묵묵히 실행하는 역할에만 충실하면 된다.

그리고 AppConfig와 같이 객체를 생성하고 관리하며, 의존 관계를 연결해주는 컨테이너를 IoC 컨테이너 또는 DI 컨테이너라 한다. 

 

 


 

 

다음으로 생성자를 주입하는 코드를 추가해보도록 하자.

 

MemerServiceImpl

 

MemberRepository 인터페이스에만 의존하게 된 MemberServiceImpl 구현체의 생성자를 추가하면 다음과 같다.

 

 

이제 더이상 이전처럼 MemoryMemberRepository 구현체를 생성하는 코드는 존재하지 않으며, MemberServiceImpl은 구 클래스에 의존하지 않으며, MemberRepository 인터페이스에만 의존한다.

MemberServiceImpl 은 MemberRepository 역할의 어떤 구현 객체가 주입될지 알 수 없으며, 생성자를 통해서 외부에서 외부에서 결정되어 주입된다. 즉, MemberServiceImpl은 이제 의존 관계에 대한 고민은 외부에 맡기고, 실행에만 집중하게 되는 것이다.

 

 

MemberSerivce - AppConfig와의 연관 관계를 나타낸 클래스 다이어그램

 

객체들을 생성하고, 생성자를 통한 주입으로 객체들을 연결하는 역할을 담당하는 AppConfig와 MemberServiceImpl과의 연관 관게를 클래스 다이어그램으로 나타내면 다음과 같다.

이제 MemberServiceImpl은 MemberRepository인 추상에만 의존하면 되므로, DIP가 완성되었다.

객체를 생성하고 연결하는 역할(AppConfig)과 실행하는 역할(MemberServiceImpl)이 명확히 분리되어 었다.

 

 

 

회원 객체의 인스턴스 다이어그램, 의존 관계 주입

 

회원 객체 memoryMemberRepository는 appConfig 객체에 의해 생성된다. appconfig는 다시 memberServiceImpl 객체를 생성하며, 생성자로 memoryMemberRepository 객체의 참조값을 전달한다

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

이러한 의존 관계 주입을 통해, 클라이언트의 코드를 변경하지 않고, 클라이언트가 호충하는 대상의 타입 인스터를 변경할 수 있다. (정적인 클래스의 의존 관계를 변경하지 않고, 동적인 객체 인스턴스 의존 관계를 쉽게 변경할 수 있다.)

 

 

 

 


 

OrderServiceImpl 

 

주문 서비스의 객체인 OrderServiceImpl의 생성자를 다음과 같이 변경해보자.

MemberRepository, DiscountPolicy 인터페이스에 각 구현체를 할당하지 않고, 멤버 변수로만 남겨 둔 후에 생성자를 통해 외부로부터 주입을 받도록 구현하면 다음과 같다.

 

 

OrderServiceImpl 도 더이상 구현 클래스 FixedDiscountPolicy 또는 RatedDiscountPolicy 구현체에 의존하지 않고, DiscountPolicy 인터페이스에만 의존한다.

마찬가지로, OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 주입될지는 모르며, 외부(AppConfig)에서 결정할 뿐이다. 이제 실행에만 집중하면 된다.

 


실행 및 테스트 코드 변경

 

MemberApp에서 실행

기존에 MemberService 구현체를 직접 MemberApp 메인 메서드에서 생성했던 것과 다르게, 이제 모든 객체의 생성과 객체 간의 주입, 연결을 담당하는 AppConfig 객체를 생성하자.

appConfig에서 생성한 MemberService 객체만 가져와 사용할 뿐이다.

 

 

OrderApp에서 실행

OrderApp도 마찬가지로 AppConfig 객체에서 생성한 MemberService, OrderService를 꺼내와 해당 서비스들을 생성하면 된다.

 

 

 

MemberServiceTest 코드 수정

기존 테스트 코드도 수정하면 다음과 같다.

MemberServiceTest에서는 MemberService 멤버 변수를 만든 후, @BeforeEach 어노테이션을 사용하여 AppConfig 객체에서 생성한 MemberService를 할당하도록 바꾸자.

 

 

OrderServiceTest 코드 수정

OrderServiceTest에서도 마찬가지로, MemberService와 OrderService모두 AppConfig에서 생성한 객체를 할당받아 사용하도록 변경하자.

 

 


 

정리

정리를 해보자면, AppConfig를 통해 각자의 역할과 구현에 대한 관심사를 확실하게 분리했다.

배역과 배우를 생각해봤을 때, 이제 AppConfig는 각 배역에 맞는 담당 배우를 선택하는 공연 기획자의 역할을 한다. 즉 구체 클래스를 선택하며, 애플리케이션 전체가 어떻게 동작해야 할지 구성을 책임진다.

그리고 각 배우들(각 구현 객체)은 각자가 담당한 기능을 충실히 실행하는 책임만 지면 된다. 

 

 

이제 OCP, DIP와 더불어 단일 책임 원칙(SRP, Single Responsibility Principle) 에 기반한 설계를 끝마쳤다고 볼 수 있다.

 


 

 

최종 설계, 각 역할이 잘 드러나도록 AppConfig 리팩터링

 

위에서 구현한 AppConfig를 보면, 중복이 존재하며 역할에 따른 구현이 명확히 드러나지 않는다.

역할과 구현을 분리한 최종적인 설계는 다음과 같으며, AppConfig 에 대해 각자의 역할이 잘 드러나도록 리팩터링이 필요하다.

 

기존의 AppConfig

 

기존 AppConfig 클래스에서는 아래의 역할이 명확히 드러나지 않으며, MemberRepository 구현 객체를 회원, 주문 서비스가 각각 중복되어 생성하고 있다.

  1. 회원 서비스 구현 객체 생성
  2. 주문 서비스 구현 객체 생성
  3. 회원 저장소 구현 객체 생성
  4. 할인 정책 구현 객체 생성

 

 

 

리팩터링 이후 AppConfig

 

이제 위의 4개의 역할이 명확히 드러났고, 중복도 제거 되었다.

위의 최종 설계본이 구성 정보에 그대로 드러나도록 구현이 완료되어, 각 메서드의 이름, 반환 타입을 통해 각각의 역할이 한 눈에 들어오게 됨을 알 수 있다. 이렇게 구현된 AppConfig를 통해 애플리케이션 전체 구성이 어떻게 되어 있는지 빠르게 파악할 수 있다.

 

 

 


 

할인 정책 변경 적용

 

이제 마지막으로 할인 정책을 기존의 FixedDiscountPolicy에서 RatedDiscountPolicy로 변경하는 경우에는 아래와 같이 AppConfig에서 할인 정책 구현 객체를 생성하는 discountPolicy() 메서드에서 FixedDiscountPolicy 객체가 아닌, RatedDiscountPolicy 객체를 생성하도록 구성 정보만 변경해주면 된다.

 

 

AppConfig의 등장으로 애플리케이션이 사용 영역과 객체를 생성하고 구성하는 Configuration 영역으로 명확하게 분리되었기 때문에, 사용 영역의 코드는 전혀 변경하지 않아도 된다. 즉 이제 클라이언트의 어떠한 코드도 변경하지 않고도 기능을 확장 가능하게 되었으며, DIP와 OCP 모두 지키는 설계를 완료하였다.

 

아래 그림과 같이 기존의 고정 할인 정책에서 정률 할인 정책으로 할인 정책이 변경되는 경우, AppConfig 구성 영역에서 생성하는 구현 객체만 FixedDiscountPolicy로 변경하면 사용 영역의 그 어떠한 코드도 변경 없이 기능이 추가된다.

 

728x90
반응형
Comments