기존의 섹션 2에서 우리는 순수하게 자바 코드로만 할인 정책을 만들어보았다.
고정 금액할인 정책, 즉 fixedPrice로 만들었는데 우리는 이미 이 전부터 이러한 정책이 변경될 것을 어느정도 알고 있었기에 인터페이스로 분리를 해주었다. 인터페이스로 해놓고 만약 정책이 바뀌면 해당 정책을 인터페이스에 구현 객체로 넘겨주면 되니까.
그러면 이걸 애플리케이션에 적용해보자.
public class OrderServiceImpl implements OrderService{
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final Discount policy = discountPolicy = new RateDiscountPolicy();
}
근데 이걸 개발자 입장에서 생각해보자.
난 그렇다면 클라이언트(Impl)입장에선 추상 인터페이스도 알고 있어야하고 구현 객체 이름도 알고 있어야한다.
만약 지금처럼 기능을 변경해야하려면 구현 객체를 직접 적어줘야하므로 문제가 있다.
(DIP : 현재 Impl 입장에선 추상 인터페이스(DiscountPolicy) 뿐만 아니라 구현 객체(FixDiscountPolicy, RateDiscountPolicy)에도 의존하고 있다.)
(OCP : 변경하지 않고 확장을 할 수 있어야하는데, 지금 클라이언트 코드에도 변경을 해줘야하므로 문제!)
즉, 놀랍게도 지금 OrderServiceImpl은 인터페이스 뿐만 아니라 구현 객체까지도 의존하고 있는 상태이다.
매우 불편티비.. 그렇다면 어떻게 해결할 수 있을지 고민해보자.
현재 문제는 뭐였지?
클라이언트 코드인 Impl이 DiscountPolicy의 인터페이스 뿐만이 아니라 구현 클래스도 함께 의존하는 것이었다.
즉 해결하기 위해선 구현 클래스엔 의존하지 않고 인터페이스에만 의존하게 변경하면 된다.
public class OrderServiceImpl implements OrderService{
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
그런데 이렇게 되면 구현체가 없어서 코드가 실행이 안된다.
즉, 다른 곳에서 클라이언트인 OrderServiceImpl에다가 DiscountPolicy의 구현 객체를 대신 넣어줘야한다.
*비유 ( tmi : 난 해당 강의의 비유가 너무나 와닿았다.)
공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 필요하다.
애플리케이션의 전체 동작 방식을 구성(config, 기획)하기 위해, 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 클래스를 만들어보았다.
public class AppConfig{
public MemberService memberService(){
//생성자를 통해 연결
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
// 중복 제거해서 좀 간단하게
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
//MemberServiceImpl 입장에선 생성자를 통해 어떤 구현 객체가 들어올지 모름.
// 즉, 어떤 구현객체가 들어올지는 외부(AppConfig)에서 결정
// Impl입장에선 의존관계는 신경쓰지 않고 실행에만 집중하면 됨.
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
}
그렇다면 이제 한번 실행해보자.
public class MemberApp{
public static void main(String[] args){
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
}
public class OrderApp{
public static void main(String[] args){
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
}
이제 아까 기존의 할인정책을 Fix에서 Rate로 바꾼다면? 그냥 AppConfig에서 수정하면 된다.
// 중복 제거해서 좀 간단하게
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy(){
//return new FixDiscountPolicy(); // 수정
return new RateDiscountPolicy();
}
}
이젠 클라이언트 코드인 OrderServiceImpl을 포함해서 사용 영역의 코드를 변경할 필요가 없다.
구성 영역은 당연히 변경된다. 구성(기획) 역할을 담당하는 AppConfig를 기획자라 생각하자.
기획자는 당연히 구현 객체들을 다 알아야한다.
<사고정리>
새로 개발한 할인 정책을 적용하려니 클라이언트 코드도 함께 변경해야한다.
클라이언트가 인터페이스 뿐만이 아니라 구현 클래스에도 의존해버려서 문제가 생긴다 -> DIP 위반.
기존에는 클라이언트가 의존하는 서버의 구현객체를 직접 생성하고 실행했었고 그것이 문제가 되었다.
이를 분리하기 위해, 기획하는 별도의 역할을 분리하자.
AppConfig라는 클래스를 만들어보았고, 이 클래스는 구현 객체를 생성하고 연결하는 역할을 한다.
이 클래스 덕분에, 이제 Application은 사용 영역과 구성 (Configuration) 영역으로 나뉘게 된다.
이제 할인 정책을 변경할 때, 우린 구성(Configuration) 영역만 변경하면 된다.
*제어의 역전 IoC(Inversion of Control)
기존 프로그램은 클라이언트 구현 객체가 스스로 서버 구현 객체를 생성하고 연결하고 실행했다.
하지만 이제는 AppConfig를 만들고 나서, 구현 객체는 자신의 로직을 실행하는 역할만 한다.
이제 프로그램들의 제어 흐름은 AppConfig가 담당한다.
이렇게 프로그램의 제어흐름을 직접 제어하는 것이 아니라 외부(AppConfig)가 관리하는 것을 제어의 역전(IoC)라 한다.
*의존관계 주입(Dependency Injection)
의존 관계는 정적인 클래스 의존 관계, 실행 시점에 결정되는 동적 객체(Instance) 의존관계를 분리해서 생각해야 한다.
정적인 건 import로 그냥 쉽게 구분할 수 있다.
동적인 경우, 실행 시작(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서
클라이언트와 서버의 실제 의존관계가 연결되는 것이고, 이를 의존관계 주입이라 한다.
이렇게 되면, 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다!
우린 지금까지 정말 순수하게 자바 코드로만 진행했다.
이 역할을 스프링은 어떻게 구현할까?
@Configuration
public class AppConfig{
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
public class MemberApp{
public static void main(String[] args){
//AppConfig appConfig = new AppConfig();
//MemberService memberService = appConfig.memberService();
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
// 함수이름, return 타입
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
}
}
ApplicationContext를 스프링 컨테이너라 한다
이젠 AppConfig를 만드는게 아니라 스프링 컨테이너를 통해 진행할 거다
스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 한다.
여기서 @Bean이라 적힌 메소드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이 객체를 스프링 빈이라 한다.
스프링 빈은 applicationContext.getBean()으로 찾을 수 있다.
'스프링 강의 필기 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
섹션 7 - (2). 의존관계 자동 주입 (0) | 2022.07.13 |
---|---|
섹션 7 -(1) 의존관계 자동 주입 (0) | 2022.07.13 |
섹션 6. 컴포넌트 스캔 (0) | 2022.07.13 |
섹션 5 - 싱글톤 컨테이너 (0) | 2022.07.09 |
섹션4 - 스프링 컨테이너와 스프링 빈 (0) | 2022.07.09 |