기존에 사용하고 있던 FixDiscountPolicy
인터페이스가 아닌 RateDiscountPolicy
로 바꾸려고 함
고정 금액 할인이 아니라 좀 더 합리적인 주문 금액당 할인하는 정률 % 할인으로 변경해보자. 예를 들어서 기존 정책은 VIP가 10000원을 주문하든 20000원을 주문하든 항상 1000원을 할인했는데, 이번에 새로 나온 정책은 10%로 지정해두면 고객이 10000원 주문시 1000원을 할인해주고, 20000원 주문시에 2000원을 할인해주는 식이다.
OrderServiceImpl
코드를 고쳐야함 -> OCP 위반
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
클래스 의존 관계를 보면, 추상(인터페이스
) 뿐만 아니라 구체(구현
) 클래스에도 의존하고 있다
OrderServiceImpl -> FixDiscountPolicy
클래스와 RateDiscountPolicy
클래스를 의존 -> DIP 위반
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 인터페이스 + 구현 클래스에 까지 의존하는 상황
private DiscountPolicy discountPolicy; //
}
위 코드는 인터페이스에만 의존하도록 설계와 코드를 변경함
하지만 구현체가 없어서 실행하면 Null Pointer Exception
발생
문제 해결 방법은 누군가가 클라이언트인 OrderServiceImpl
에 DiscountPolicy
의 구현객체를
대신 생성하고 주입해야함
애플리케이션을 하나의 공연으로 생각했을때, 각각의 인터페이스를 배역 이라 하자
로미오 역할(인터페이스) -> 원빈 (구현 클래스) 이라 할때
이전 코드는 마치 원빈(구현 클래스)가 여자 주인공(구현 클래스)를 직접 초빙하는 것과 같다
원빈은 공연도 하고, 여자 주인공도 직접 초빙해야하는 다양한 책임을 가지고 있다
그래서 관심사를 분리
해야 한다
-> 공연기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리해야함
애플리케이션의 전체 동작 방식을 구성하기 위해
구현 객체를 생성하고, 연결하는 책임을 가지는 별도 설정 클래스 만들기
AppConfig는 실제 동작에 필요한 필요한 구현 객체를 생성
AppConfig는 생성한 객체를 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결)해줌
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) { // 생성자 주입
this.memberRepository = memberRepository;
}
}
위 코드의 변경으로 MemberServiceImpl
은 MemoryMemberRepository
를 의존하지 않고
MemberRepository
인터페이스만 의존한다
MemberServiceImpl
의 입장에서 생성자를 통해 어떤 구현 객체가 들어오질 알 수 없다
MemberServiceImpl
의 생성자를 통해서 어떤 구현 객체가 주입될지는 외부(AppConfig
)에서 결정됨
MemberServiceImpl
은 이제 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 됨
객체의 생성과 연결은 AppConfig
가 담당
MemberServiceImpl
은 MemberRepository
에만 의존하면되며, 구체클래스를 몰라도 됨OrderServiceImpl
- 생성자 주입
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository; // 인터페이스에만 의존
private final DiscountPolicy discountPolicy; // 인터페이스에만 의존
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
OrderServiceImpl
은 FixDiscountPolicy
를 의존하지 않고 DiscountPolicy
인터페이스만 의존한다.
OrderServiceImpl
입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
OrderServiceImpl
의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부에서 결정한다.
OrderServiceImpl
은 이제부터 실행에만 집중하면 된다.
//전
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 MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy(){
return new FixDiscountPolicy();
}
}
즉 리팩토링 후 코드에서
할인율을 고정이 아닌, 정률로 바꾸려면
클라이언트인 ServiceImpl 에서 바꿀 필요없이
AppConfig에서 return new RateDiscountPolicy();
만 바꿔주면 된다
위에 코드에서 AppConfig 가 프로그램 제어 흐름을 스스로 조종함
OrderServiceImpl 은 필요한 인터페이스들을 호출하지만, 어떤 구현 객체들이 실행될 지 모른다
이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라
외부에서 관리하는 것을 제어의 역전 이라 한다
AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것
Ioc컨테이너
또는 DI 컨테이너
라고 한다
의존관계에 주입에 초점을 맞추어 최근에는 DI 컨테이너라 한다
ApplicationContext
를 스프링 컨테이너라 함public class OrderApp {
public static void main(String[] args){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 20000);
System.out.println("order = " + order.toString());
}
}
AppConfig
를 사용해서 DI
를 했지만, 이제부터 스프링 컨테이너
를 사용한다@Configuration
이 붙은 AppConfig
를 설정 정보로 사용한다@Bean
이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다applicationContext.getBean()
메서드를 사용해서 찾을 수 있다정리!
1. 스프링 컨테이너에 객체를 스프링 빈으로 등록
2. 스프링 컨테이너에서 스프링 빈을 찾아서 사용
@Configuration
public class AppConfig {
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}