지나공 : 지식을 나누는 공간

스프링 만들어진 이유. 객체지향설계 5원칙부터 DI 까지. 2편 본문

Tech/Spring Boot

스프링 만들어진 이유. 객체지향설계 5원칙부터 DI 까지. 2편

해리리_ 2021. 6. 1. 14:37

지금까지의 흐름, 객체 지향 설계 원칙을 따를 때 의존성 주입이 왜 필요했는지 정리해보자.

 

지금까지의 흐름과 DI의 필요성

1. 새로운 할인 정책 개발

다형성 덕분에 새로운 정률 할인 정책 코드를 추가로 개발하는 것까진 문제가 없다.

 

2. 새로 만들어진 정률 할인 정책을 적용할 때 문제 발생

새로 개발한 정률 할인 정책을 적용하려고 했더니, 클라이언트 코드인 주문 서비스 구현체 (OrderSerivceImpl)의 코드도 같이 변경해야 했다. 왜냐면 여기서 직접적으로 DiscountPolicy를 FixDiscountPolicy로 구현하고 있었기 때문이다. 얘를 RateDiscountPolicy로 변경하는 순간 DIP 위반이라는 문제가 발생한다.

 

3. 관심사의 분리

애플리케이션을 하나의 공연으로 생각하자. 기존에는 클라이언트가 의존하는 서버 구현 객체를 직접 생성하고 실행했고, 이를 비유하면 기존에는 남자 주인공 배우가 공연을 하고, 동시에 여자 주인공도 직접 초빙하는 다양한 책임을 갖고 있는 것이다. 그래서 공연을 구성하고 담당 배우를 섭외하고, 지정하는 책임을 담당할 별도의 공연 기획자가 필요하다. AppConfig가 그 역할을 할 것이다. 이는 애플리케이션의 전체 동작 방식을 구성(config)하기 위해 "구현 객체를 생성"하고 "연결"하는 책임을 지닌다. 이제 클라이언트 객체는 자신의 역할을 실행하는 것만 집중하면서 권한이 줄어들고 책임이 명확해졌다.

 

4. AppConfig 리팩터링

config 정보에서 역할과 구현을 명확하게 분리했다. MemberServiceImpl과 OrderServiceImpl에서 모두 MemberRepository를 생성하는데, MemberRepository를 생성하는 메소드를 만들어서 분리하면 이런 중복을 분리할 수 있다. 

 

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

정책할인정책 -> 정률할인정책으로 변경하는 상황에서 AppConfig가 등장하면서 애플리케이션이 크게용 영역과 객체 생성을 구성(Configuration)하는 영역으로 분리되었다.

할인 정책을 변경하더라도 AppConfig가 있는 구성 영역만 변경하면 되고, 사용 영역은 변경할 필요가 없어졌다. 클라이언트 코드를 변경하지 않아도 된다.

 

현재까지 객체 지향 설계 5원칙 중에서 SRP, DIP, OCP를 적용하게 된 것이다.

현재까지 적용된 좋은 객체 지향 설계 원칙 

SRP : 단일 책임 원칙

"한 클래스는 하나의 책임만 가져야 한다."

클라이언트는 직접 구현 객체를 생성하고 연결하고 실행하는 다양한 책임을 갖고 있었는데 관심사를 분리함으로써 클라이언트 객체는 실행하는 책임만 갖게 됐다.

 

DIP : 의존관게 역전 법칙

"프로그래머는 추상화에 의존해야지, 구현체에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나다."

AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 생성자를 통해 주입했다.

 

OCP : 

"소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다."

다형성을 사용하면서 클라이언트가 DIP를 지켰고 AppConfig가 의존 관계를 FixDiscountPolicy -> RateDiscountPolicy로 변경해서 클라이언트에 주입하게 되니까 클라이언트 코드는 변경하지 않아도 된다.

"소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다."라는 원칙이 지켜졌다.

 

IoC란?

제어의 역전. 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고 연결하고 실행했다. 한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다.

 

하지만 AppConfig가 등장한 이후 구현 객체는 자신의 로직을 실행하는 역할만 담당하게 됐다. 프로그램의 제어 흐름은 이제 AppConfig가 가진다. 예를 들어 OrderSerivceImpl은 필요한 인터페이스들을 호출하긴 하지만 어떤 구체적인 구현 객체가 실행될지는 모르는 것이다. 그래서 프로그램의 제어 흐름에 대한 권한을 모두 AppConfig가 갖고 있는 상태다. 심지어 OrderServiceImpl도 AppConfig가 실행한다. 그리고 AppConfig는 OrderServiceImpl이 아닌 OrderService인터페이스의 다른 구현 객체를 생성하고 실행할 수도 있다. 그런 사실을 전혀 모른채 OrderServiceImpl은 묵묵히 자기 로직을 실행할 뿐이다.

이렇게 프로그램의 제어 흐름을 자기가 직접 제어하는 게 아니라 외부에 제어권을 넘겨서 외부가 관리하는 것을 제어의 역전이라고 한다.

 

프레임워크 vs 라이브러리

프레임워크는 JUnit처럼 정해진 흐름이 있어서 내가 작성한 코드를 제어하여 대신 실행한다. 반면 라이브러리는 내가 작성한 코드가 제어의 흐름을 잡고 필요할 때에 코드를 작성하는 내가 직접 라이브러리를 호출해서 사용한다.

 

DI란?

의존 관계 주입이다. OrderServiceImpl은 DiscountPolicy 인터페이스에 의존하고, 실제 어떤 구현 객체가 사용될지는 모른다. 의존 관계는 '정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체, 즉 인스턴스 의존관계' 이렇게 둘로 분리해서 생각해야 한다.

 

* 정적인 클래스 의존관계

클래스가 사용하는 import 코드만 보고 의존 관계를 쉽게 파악할 수 있다. 정적인 의존 관계는 애플리케이션을 실행하지 않아도 분석할 수 있다. 예를 들어 OrderServiceImpl은 MemberRepository와 DiscountPolicy에 의존한다는 것을 코드만 봐도 우리는 바로 알 수 있다. 하지만 이것 만으로는 실제 어떤 객체가 OrderServiceImpl에 주입될 지는 알 수 없다.

 

* 동적인 객체 인스턴스 의존관계

애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 연결 관계다.

애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결되는 것을 "의존관계 주입"이라고 한다.

객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다. 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다. 또한 이를 사용하면 정적인 클래스 의존 관계를 변경하지 않으면서 동적인 객체 인스턴스 의존 관계를 쉽게 변경할 수 있다.

여기서 말하는 정적인 클래스 의존 관계를 변경하지 않는다는 말은, 사용 영역 코드를 변경하지 않는다는 의미다.

 

구현체를 생성하고 관리하면서 의존성을 주입하여 연결해주는 AppConfig이다.

 

IoC 컨테이너와 DI 컨테이너

AppConfig처럼 객체를 생성하고 관리하면서 의존 관계를 연결해주는 컨테이너를 IoC 컨테이너 또는 DI 컨테이너라고 한다. 의존관계 주입에 초점을 맞춰서 최근에는 주로 DI 컨테이너라고 한다.

728x90
Comments