<목차>
- 새로운 할인 정책 개발
- 새로운 할인 정책 적용과 문제점
- 관심사의 분리
- AppConfig 리팩터링
- 새로운 구조와 할인 정책 적용
- 전체 흐름 정리
- 좋은 객체 지향 설계의 5가지 원칙의 적용
- IoC, DI, 그리고 컨테이너
- 스프링으로 전환하기
① 새로운 할인 정책 개발
DiscountPolicy 인터페이스를 만들어놨기 때문에 RateDiscountPolicy만 새로 만들어주기만 하면 됨!
@DisplayName
성공 테스트뿐만 아니라 '실패' 테스트도 꼭 만들어봐야 함
② 새로운 할인 정책 적용과 문제점
할인 정책을 변경하려면 할인 정책의 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
public class OrderServiceImpl implements OrderService {
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
# 문제점 (2가지)
1) DIP 위반
DIP : 구체(구현)이 아닌 '추상(인터페이스)'에 의존해야 한다
<GOOD 의존관계>
<BAD 의존관계 -- 현재>
클래스 의존관계를 분석해보면, 추상(DiscountPolicy)뿐만 아니라 구체(FixDiscountPolicy, RateDiscountPolicy)에도 의존하고 있다
2) OCP 위반
OCP : 클라이언트 코드를 변경하지 않고도 기능을 확장할 수 있다
FixDiscountPolicy 를 RateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스 코드도 함께 변경하게 된다
# 해결방안
'인터페이스'에만 의존하도록 설계 변경
+
누군가가 클라이언트인 OrderServiceImpl 에 DiscountPolicy 의 '구현 객체'를 대신 생성 & 주입
③ 관심사의 분리 ★
관심사의 분리
=> AppConfig 등장!
AppConfig
=> 구현 객체의 생성 & 연결(생성자 주입(DI))하는 책임 有
=> DIP 완성!
MemberServiceImpl, OrderServiceImpl
- 단지 추상 인터페이스(MemberRepository / DiscountPolicy)에만 의존한다.
- 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
- 생성자를 통해 어떤 구현 객체가 주입될지는 오직 외부(=> AppConfig)에서
결정된다.
- 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
DI(Dependency Injection; 의존관계 주입)
- appConfig 객체 : memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다
- 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다
테스트 코드의 @BeforeEach
# 정리
- AppConfig => 관심사를 확실하게 분리
- AppConfig : 구체 클래스를 선택한다 / 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다
- 구체 클래스 : 담당 기능을 '실행'하는 책임만 지면 된다
④ AppConfig 리팩터링
문제점
: 현재의 AppConfig에는 중복이 있고, 역할에 따른 구현이 잘 안보인다.
- 중복 제거
=> new MemoryMemberRepository() 부분의 중복을 제거하여, 이제 MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다
- AppConfig에서 역할에 따른 구현이 한 눈에 들어오도록 리팩터링
=> 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다
⑤ 새로운 구조와 할인 정책 적용
AppConfig의 등장
=> 애플리케이션이 크게 '사용 영역'과, 객체를 생성하고 구성(Configuration)하는 '구성 영역'으로 분리되었다
FixDiscountPolicy → RateDiscountPolicy
구성 영역(AppConfig)만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다
구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig은 애플리케이션의 '기획자'로서, '참여자'인 구현 객체들을 모두 알아야 한다.
따라서, DIP & OCP 모두 만족!
⑥ 전체 흐름 정리
새로운 할인 정책 개발
다형성 덕분에 새로운 정률 할인 정책 코드를 추가로 개발하는 것 자체는 아무 문제가 없음
새로운 할인 정책 적용과 문제점
- 새로 개발한 정률 할인 정책을 적용하려고 하니 클라이언트 코드인 주문 서비스 구현체도 함께 변경해야함
=> OCP 위반
- 주문 서비스 클라이언트가 인터페이스인 DiscountPolicy 뿐만 아니라, 구체 클래스인 FixDiscountPolicy 도 함께 의존
=> DIP 위반
관심사의 분리
- <기존> 클라이언트가 의존하는 서버 구현 객체를 직접 생성하고, 실행함. 비유를 하면 기존에는 남자 주인공 배우가 공연도 하고, 동시에 여자 주인공도 직접 초빙하는 다양한 책임을 가지고 있었음
- 공연 기획자인 AppConfig의 등장
=> AppConfig : 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임 有
- 이제부터 클라이언트 객체는 자신의 역할을 실행하는 것만 집중, 권한이 줄어듦 (책임이 명확해짐)
=> SRP 만족
AppConfig 리팩터링
- 구성 정보에서도 역할과 구현을 명확하게 분리되어 보이도록 리팩터링함
=> 역할이 잘 드러남 & 중복 제거
새로운 구조와 할인 정책 적용
- 정액 할인 정책 → 정률% 할인 정책
- AppConfig의 등장 => 애플리케이션이 크게 '사용 영역'과, 객체를 생성하고 구성(Configuration)하는 '구성 영역'으로 분리
- 할인 정책을 변경해도 AppConfig가 있는 구성 영역만 변경하면 됨, 사용 영역은 변경할 필요가 없음. 물론 클라이언트 코드인 주문 서비스 코드도 변경하지 않음
- DIP, OCP 모두 만족!
⑦ 좋은 객체 지향 설계의 5가지 원칙의 적용
여기서는 3가지 (SRP, DIP, OCP) 적용됨
SRP ; 단일 책임 원칙
=> 한 클래스는 하나의 책임만 가져야 한다.
- 관심사의 분리 by 'AppConfig'
=> 클라이언트 객체 : 다양한 책임(객체 생성, 연결, 실행) --> only 실행하는 책임만 담당
=> AppConfig : 구현 객체의 생성, 연결하는 책임 담당
DIP ; 의존관계 역전 원칙
=> 프로그래머는 "추상화"에 의존해야지, '구체화'에 의존하면 안 된다.
=> '의존관계 주입'
- 클라이언트 코드 : DiscountPolicy 추상화 인터페이스에만 의존하도록 코드를 변경한다. 하지만 인터페이스만으로는 아무것도 실행할 수 없다.
- AppConfig : FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입해준다.
OCP
=> 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- OCP의 전제 : 다형성 사용 + 클라이언트가 DIP를 지킴 (의존관계 주입)
- 애플리케이션을 '사용 영역'과 '구성 영역'으로 나눔
- AppConfig가 의존관계를 변경해서(FixDiscountPolicy → RateDiscountPolicy) 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨
- 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다
- '변경이 닫혀 있다' == 변경이 필요 없다
⑧ IoC, DI, 컨테이너
1) IoC (Inversion of Control) ; 제어의 역전
=> 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것
스프링에만 국한된 단어는 아님
- <기존> 클라이언트 구현 객체가 프로그램의 제어 흐름(생성/연결/실행)을 스스로 조종했다. (개발자 입장에서 자연스러운 흐름에 해당)
- <AppConfig의 등장 이후>
- 구현 객체 : 자신의 로직을 실행하는 역할만 담당
- AppConfig : 프로그램의 제어 흐름에 대한 모든 권한 有
=> OrderServiceImpl : 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모름
=> AppConfig : OrderServiceImpl 이 아닌 OrderService 인터페이스의 다른 구현 객체를 생성하고 실행할 수도 있다. 그런 사실도 모른 채 OrderServiceImpl은 묵묵히 자신의 로직을 실행할 뿐.
# 프레임워크 VS. 라이브러리
- 프레임워크 : 내가 작성한 코드를 제어 & 대신 실행 ; ex) JUnit (test 프레임워크)
- 라이브러리 : 내가 작성한 코드가 직접 제어의 흐름을 담당
2) DI (Dependency Injection) ; 의존관계 주입 ★
- 의존관계 = '정적인 클래스 의존관계' + '동적인 객체(인스턴스) 의존관계'
i) 정적인 클래스 의존관계
- 애플리케이션을 실행하지 않아도, import 코드만 보고도 의존관계를 쉽게 분석 가능
- 클래스 다이어그램
OrderServiceImpl
=> MemberRepository , DiscountPolicy 에 의존한다는 것을 알 수 있다.
=> BUT 클래스 의존관계만으로는 실제 어떤 객체가 OrderServiceImpl에 주입 될지 알 수 없다.
ii) 동적인 객체 인스턴스 의존관계
- 애플리케이션 실행 시점에 실제 생성된 객체(인스턴스)의 참조가 연결된 의존 관계
- '의존관계 주입'
=> 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 그 참조값을 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것
=> 클라이언트 코드(= 정적 클래스 의존관계)를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스(동적 객체(인스턴스) 의존관계)를 쉽게 변경할 수 있다
- 객체(인스턴스) 다이어그램
3) DI 컨테이너
- 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것
- AppConfig
- == IoC 컨테이너(∵범용적), 어샘블러(∵조립), 오브젝트 팩토리(∵오브젝트 생성)
⑨ 스프링으로 전환하기
① AppConfig 스프링 기반으로 변경
- @Configuration => AppConfig에 설정을 구성한다
- @Bean (각 메서드에 붙이기) => 스프링 컨테이너에 스프링 빈으로 등록한다
② MemberApp에 스프링 컨테이너 적용
③ OrderApp에 스프링 컨테이너 적용
# 스프링 컨테이너
<기존>
- 개발자가 순수한 자바로 만든 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했다
- 개발자가 필요한 객체를 AppConfig 를 사용해서 직접 조회했다
<현재 ; 스프링으로의 전환>
-
스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.
-
ApplicationContext => 스프링 컨테이너 (必)
-
@Configuration => 스프링 컨테이너가 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다
-
@Bean => @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다 => '스프링 빈'
- 스프링 빈의 이름 => == @Bean이 붙은 메서드의 이름 (EX. memberService, orderService)
- @Bean(name = "바꾼 이름") => 스프링 빈의 이름을 별도로 지정도 가능. BUT 웬만하면 default를 따르는 게 좋음.
-
applicationContext.getBean() => 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾는다
근데... 스프링으로 전환함으로써 오히려 코드가 약간 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 도대체 어떤 장점이 있을까?
(to be continued...)
# 유용한 단축키 모음
- shift + ctrl + t ☞ Create New Test (test 코드 자동 생성)
- alt + enter ☞ static하게 import하도록 변경 가능
- ctrl + e ☞ 최근 실행했던 파일 히스토리 볼 수 있음
- ctrl+alt+m ☞ extract method
- shift+f10 ☞ 기존 마지막 실행을 재실행