public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private DiscountPolicy discountPolicy;
public OrderServiceImpl(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
...
생성자 주입을 통해서 discountPolicy까지 책임을 후로 넘겼다. 그에따라 남은 MemberRepository도 수정해주면 다음과 같다.
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;
}
...
이렇게 DIP를 지켰다고 볼 수 있지만 더욱 SOLID를 추구하는 개발 방향이 존재한다.
그 전에 OrderServiceImpl은 리펙터링 전과 후 어떻게 달라졌는지 구체적으로 정리해보자.
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
이전에는 위의 코드와 같이 OrderServiceImpl이 DiscountPolicy에도, FixDiscountPolicy에도 의존했다고 볼수 있다.
SRP에 대해 생각해보자. 모듈이 변경되는 이유가 한가지여야 함으로 받아들여야 한다. 만약 Discount정책이 변경되어 FixDiscountPolicy대신 RateDiscountPolicy를 적용해야한다면 우리는 무엇을 수정해야하는가?
DiscountPolicy를 사용하는 OrderServiceImpl의 내부를 수정해야한다. OCP원칙으로 볼 때 확장에는 열려있으며 변경에는 닫혀있다. 의 관점에서 확장을 시도했는데 확장의 대상이 아닌 다른 역할을 하는 클래스에서 변경이 일어났다.(SRP, OCP의 관점에서 개선할 여지가 존재한다는 것이다.)
DIP는 애초부터 추상화에만 의지하라고 단언한다. 위의 코드는 추상화와 구체화에 모두 의지중인 코드이다. 의지중이라는 말이 어색하게 들린다면 해당 클래스에 작성된 모든 것은 의지한다고 표현할 수 있다.
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은 자신 혼자서 memberRepository와 discountPolicy의 구현체를 결정할 수 없다. 누군가가(외부의 호출자가) 나를 호출해서 나에게 내가 활용할 구현체를 넘겨주어야 OrderServiceImpl이란 존재는 활동이 가능한 것이다.
이렇게 클래스 자신은 외부로부터의 구현체 주입에 의해 작동하므로 구현체가 변경되어도 어차피 자신은 주는 것만 받아쓰는 존재이기에 밖에서의 변경을 신경쓸 필요가 없다. 즉 코드 수정이 일어날 이유가 사라지는 것이다.
이렇게 외부로부터 받아서 쓰도록, 외부에서 주입받는 방식을 Dependency Injection(DI, 의존관계주입)이라고 한다.
이제 OrderServiceImpl은 많은 곳에서 사용될 수 있을 것이다.
사용되는 곳 마다 리포지토리와 할인정책 구현체를 생성자로 넘겨주어 활용하면 된다.
여기서 다시 의문은, OrderServiceImpl이 많은 곳에서 사용된다면 문제가 된다.
구현체가 바뀔 경우(OrderServiceImpl -> OrderServiceImplV2)
OrderServiceImpl를 생성하기 위한 코드
new OrderServiceImpl(memberRepository, ...) -> new OrderServiceImplV2(memberRepository, ...)
그 많은 곳을 모두 수정해주어야 한다는 것이다.
결국 끝없는 문제가 된다.
그렇기에 우리는 AppConfig라는 클래스를 만들어 이리저리 사용되는 클래스들을 등록시켜놓을 수 있다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
이렇게 한 곳에 모아서 관리할 경우 memberService를 쓰고 싶은 곳에서 AppConfig로부터 가져와 사용할 수 있다. 만약 구현체가 변경되어야 하는 상황이라면 AppConfig의 위 코드에 보이는 return 부분만 수정해준다면, 수많은 사용처에서 동시에 동일한 변화를 줄 수 있다.
기존 프로그램은 클라이언트 구현 객체가 서버 구현 객체를 생성, 연결, 실행했다. AppConfig로 중앙관리 클래스를 만들자 구현 객체는 자신의 로직을 실행하는 역할만 담당하게 된다.
프로그램 제어의 흐름은 이제 AppConfig로 위임된 것이다. 생성자로 하여금 DI가 잘 이루어진 클래스여도 해당 클래스를 여러 곳에서 쓰고있다면, 제어흐름은 각각의 사용처이다. 하지만 AppConfig에서 제어흐름을 고정하고 각각의 사용처또한 필요시 AppConfig에게 요청해서 사용한다면 그들에게 생성자에 들어갈 부분을 제어할 권리는 사라진다.
커스텀으로 만든 AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 Ioc Container, DI Container라고 한다. (DI에 포커스하여 최근에는 주로 DI 컨테이너라고 한다.)
이쯤되면 이전에 들었던 가장 첫 강의에서 활용했던 @Bean이나 컴포넌트 스캔이 그런거였나? 혹은 그런거 였구나라는 생각을 가지게 된다.(난 그랬다.)
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(MemberRepository());
}
@Bean
private static MemberRepository MemberRepository() {
return new MemoryMemberRepository();
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(MemberRepository(), discountPolicy());
}
@Bean
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
그래서 우리는 스프링 데코레이터로 하여금 @Configuration으로 AppConfig를 스프링이 관리하는 DI컨테이너로 선언할 수 있고, @Bean을 통해 각각의 객체들을 컨테이너에 보관할 수 있다.
이제야 위 두 데코레이터의 역할이 정확히 이해가 되는 것 같다.
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);
빈에 존재하는 객체를 사용하기 위해서, 위와 같이 코딩해서 불러올 수 있다.
ApplicationContext는 스프링의 IoC(Inversion of Control) 컨테이너를 의미한다.
AnnotationConfigApplicationContext는 자바 기반의 설정 클래스를 사용하여 스프링 컨테이너를 생성하고 AppConfig라는 클래스를 설정 파일로 사용하고 있다.
applicationContext.getBean() 메서드는 스프링 컨테이너에서 지정된 이름의 빈을 찾아 반환한다.
"memberService"와 "orderService"는 빈의 이름이며, 각각 MemberService와 OrderService 클래스 타입의 빈을 가져오도록 지정한다.
현재까지의 코드로만 봐서는 데코레이터나 호출시 더 길어진 코드를 보며 활용한 기능이 동일하기에 개선점은 잘 느껴지지 않는다.
AppConfig에 스프링 옷을 입혀 스프링 컨테이너로 만들고 객체들을 "Bean"화 시킨 것은 더 많은 이점들을 우리에게 준다는 것을 계속 알아보도록 하자!