새로운 할인 정책 개발
- 악덕 기획자: 서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 좀 더 합리적인 주문 금액당 할인하는 정률% 할인으로 변경하고 싶어요. 예를 들어서 기존 정책은 VIP가 10000원을 주문하든 20000원을 주문하든 항상 1000원을 할인했는데, 이번에 새로 나온 정책은 10%로 지정해두면 고객이 10000원 주문시 1000원을 할인해주고, 20000원 주문시에 2000원을 할인해주는 거에요!
- 순진 개발자: 제가 처음부터 고정 금액 할인은 아니라고 했잖아요.
- 악덕 기획자: 애자일 소프트웨어 개발 선언 몰라요? “계획을 따르기보다 변화에 대응하기를”
- 순진 개발자: … (하지만 난 유연한 설계가 가능하도록 객체지향 설계 원칙을 준수했지 후후)
새로운 할인 정책 적용과 문제점
// private DiscountPolicy discountPolicy = new FixDiscountPolicy(); private DiscountPolicy discountPolicy = new RateDiscountPolicy();
- 우리는 역할과 구현을 충분하게 분리했다. ⇒ OK
- 다형성도 활용하고, 인터페이스와 구현객체를 분리했다. ⇒ OK
- OCP, DIP와 같은 객체지향 설계 원칙을 충실히 준수했다. ⇒ NO
- DIP: 주문 서비스(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨것 같지만, 사실 구현 클래스에도 의존하고 있다.
- OCP: 변경하지 않고 확장할수 있는 것 같지만, 위 코드에서 주석처리 후 변경해줘야 하는 것을 알 수 있다.
기대했던 의존관계
실제 의존관계
DIP를 위반하고 있다. 마찬가지로, RateDiscountPolicy로 변경하려면 Service를 변경해야하므로 OCP도 위반하고 있다.
해결방안
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야한다.
관심사의 분리
- 구현은 역할을 수행하는 것에만 집중해야한다.
- 구현이 어떻게 되든, 역할만 할 수 있으면 똑같이 동작하여야한다.
- 이러한 구현과 역할을 연결시켜주기 위한 기획자가 나올 시점이다.
- 기획자를 만들고, 역할과 구현을 확실히 분리하자 ⇒ AppConfig
AppConfig 등장
public class AppConfig{ public MemberService memberService() { return new MemberServiceImpl(new MemoryMemberRepository()); } public OrderService orderService() { return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy()); } }
- AppConfig은 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
- MemberServiceImpl
- MemoryMemberRepository
- OrderServiceImpl
- FixDiscountPolicy
- AppConfig은 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입(연결)해준다.
- MemberServiceImpl ⇒ MemoryMemberRepository
- OrderServiceImpl ⇒ MemoryMemberRepository, FixDiscountPolicy
⇒
- 설계 변경으로 MemberServiceImpl은 MemoryMemberRepository에 의존하지 않는다.
- 단지 MemberRepository Interface에만 의존한다.
- MemberServiceImpl은 이제 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
클래스 다이어그램
- DIP완성: MemberServiceImpl은 MemberRepository인 추상에만 의존하면 된다. 구체 클래스는 관심도 없다.
- 관심사의 분리: 객체를 생성하고 연결하느 역할과 실행하는 역할이 명확히 분리되었다.
회원 객체 인스턴스 다이어그램
- appConfig 객체는 memoryMemberRepository 객체를 생성하고, 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달한다.
👉
클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라고 한다.
결론
- AppConfig을 통해서 관심사를 확실하게 분리했다.
- AppConfig은 그냥 기확자다.
- OrderServiceImpl은 기능을 실행하는 책임만 지면 된다. 인터페이스만 보고 개발을 하면 된다.
AppConfig 리팩토링
- 현재 AppConfig을 보면 중복이 있고, 역할에 따른 구현이 잘 안 보인다.
기대하는 그림
public class AppConfig{ public MemberRepository memberRepository(){ return new MemoryMemberRepository(); } public DiscountPolicy discountPolicy() { return new FixDiscountPolicy(); } public MemberService memberService() { return new MemberServiceImpl(memberRepository()); } public OrderService orderService() { return new OrderServiceImpl(memberRepository(), discountPolicy()); } }
- method의 이름으로 역할을 모두 알 수 있다.
- 중복을 제거해서
MemoryMemberRepository
에서 다른 것으로 바꿀 때 하나만 바꾸면 된다.
새로운구조와 할인 정책 적용
- 놀랍게도, AppConfig만 변경하면 모든게 바뀐다.
- AppConfig의 등장으로 애플리케이션이 크게 사용영역과 구성(Configuration)영역으로 분리되었다.
좋은 객체 지향 설계와 5가지 원칙의 적용
SRP 단일 책임 원칙
한 클래스는 하나의 책임만 가져야 한다.
- 클라이언트 객체는 직접 객체 구현을 생성, 연결, 실행 하는 여러 책임을 갖고 있었음.
- 생성과 연결하는 책임을
AppConfig
이 담당하게 변경
- 클라이언트 객체는 실행하는 책임만 담당하도록 변경
DIP 의존관계 역전 원칙
프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
- 새로운 할인 정책을 개발하고, 적용하려고 하니 클라이언트 코드도 함께 변경해야 했다. 기존 클라이언트 코드는 DIP를 지키며 인터페이스에 의존하는 것 같았지만, 사실은 구현 클래스에도 의존하고 있었다.
AppConfig
을 통해FixDiscountPolicy
객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입했다. 이렇게 DIP원칙을 따르면서 문제도 해결했다.
OCP
소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- AppConfig이 의존관계를 FixDiscountPolicy ⇒ RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨.
- 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다!
IOC, DI, 컨테이너
IOC(제어의 역전)
- 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성함. 따라서, 구현 객체가 프로그램의 제어 흐름을 직접 조종함.
- 반면,
AppConfig
을 도입해서 구현 객체는 로직을 실행하는 역할만 담당하도록함. 프로그램 제어 흐름은AppConfig
이 가져간다. 즉,OrderServiceImpl
은 어떤 구현객체가 실행될 지 모르고, 인터페이스만 호출한다.
- 즉, 프로그램의 제어 흐름에 대한 권한을
AppConfig
이 갖고 있다. 심지어OrderServiceImpl
도AppConfig
이 생성한다.
- 이렇게, 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 IOC라고 한다.
프레임워크 vs 라이브러리
- 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크.
- 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 라이브러리.
DI(의존관계 주입)
OrderServiceImpl
은DiscountPolicy
인터페이스에 의존한다. 실제 어떤 구현객체가 사용될지는 모른다.
- 의존관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계를 분리해서 생각해야한다.
정적인 클래스 의존관계
클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있다.
위 클래스 의존관계 만으로는 실제 어떤 객체가 OrderServiceImpl
에 주입 될지 알 수 없다.
동적인 객체 인스턴스 의존관계
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
- DI를 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수있다.
- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
IOC 컨테이너, DI 컨테이너
- AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IOC 컨테이너 또는 DI 컨테이너라고 한다.
- 요즘은 DI 컨테이너로 부른다.
Spring으로 전환하기
Appconfig.java
@Configuration public class AppConfig{ @Bean public MemberRepository memberRepository(){ return new MemoryMemberRepository(); } @Bean public DiscountPolicy discountPolicy() { return new RateDiscountPolicy(); } @Bean public MemberService memberService() { return new MemberServiceImpl(memberRepository()); } @Bean public OrderService orderService() { return new OrderServiceImpl(memberRepository(), discountPolicy()); } }
Annotation을 이렇게 추가 해준다.
MemberApp.java
// AppConfig appConfig = new AppConfig(); // MemberService memberService = appConfig.memberService(); ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderApp.java
// AppConfig appConfig = new AppConfig(); // MemberService memberService = appConfig.memberService(); // OrderService orderService = appConfig.orderService(); ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class); MemberService memberService = applicationContext.getBean("memberService", MemberService.class); OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
스프링 컨테이너
ApplicationContext
를 스프링 컨테이너라 한다.
- 기존에는 개발자가
Appconfig
을 사용해서 직접 객체를 생성하고, DI를 해주었지만 이제부터는 스프링 컨테이너를 통해서 한다.
- 스프링 컨테이너는
@Configuration
이 붙은AppConfig
을 설정 정보로 사용한다. 여기서@Bean
이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에서 관리되는 객체를 스프링 빈 이라고 한다.
- 스프링 빈은
@Bean
이 붙은 메서드의 이름을 스프링 빈의 이름으로 사용한다. (memberService, orderService
)
- 이전에는 필요한 객체를 AppConfig을 통해 직접 조회했지만, 이제는 스프링 컨테이너를 통해서 빈을 찾아야한다. 스프링 빈은
applicationContext.getBean()
메서드를 통해 찾을 수 있다.
- 더 복잡해진 것 같은데 스프링 컨테이너를 사용하면 어떤 장점이 있을까?
Uploaded by N2T
(23.05.31 22:30)에 작성된 글 입니다.