지난 시간에 예제를 작성하면서, 궁금증이 생긴 부분은 어떻게 객체 지향 원리인 DIP/OCP 그리고 다형성을 지키면서 개발을 할 수 있을까였다.궁금증이 극대화 된 상태라 이번 강의를 아주 흥미롭게 들을 수 있었다.
인터페이스 DiscountPolicy를 구현한 FixDiscountPolicy를 RateDiscountPolicy로 변경해야하는 상황이 주어졌다.
private int discountPolicy = 10;
@Override
public int discount(Member member, int price){
if(member.getGrade() == Grade.VIP){
return price * discountPolicy / 100;
}else{
return 0;
}
}
public class OrderServiceImpl implements OrderService{
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
현재 발생하는 문제의 원인은 클라이언트가 인터페이스 뿐만 아니라 구현 클래스도 함께 의존한다는 점이다. 그래서, 해결방법은 인터페이스에만 의존하도록 의존 관계를 변경하면 된다.
public class OrderServiceImpl implements OrderService{
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
하지만, 다음 코드는 구현체가 없기 때문에 NPE가 발생한다. 이 문제를 해결하기 위해서는 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.
그 누군가에 해당하는 것이 AppConfig이다.
애플리케이션의 전체 동작 방식을 구성하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스이다.
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(),
new RateDiscountPolicy());
}
- AppConfig는 애플리케이션의 실제 동작에 필요한 객체를 생성한다.
- MemberServiceImpl
- MemoryMemberRepository
- OrderServiceImpl
- RateDiscountPolicy
- AppConfig는 생성한 객체 인스턴스의 참조를 생성자를 통해서 주입한다.
- MemberServiceImpl -> MemoryMemberRepository
- OrderServiceImpl -> MemoryMemberRepository , RateDiscountPolicy
클라이언트 입장에서 의존 관계를 마치 외부에서 주입해주는 것처럼 보이는 것
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
AppConfig는 MemoryMemberRepository 객체를 생성하고, 그 참조값을 MemberServiceImpl을 생성하면서 생성자로 전달한다.
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
public static void main(String[] args){
AppConfig appConfig = new AppConfig();
//AppConfig에서 memberService 객체를 만들고, memberRepository를 생성한다.
MemberService memberService = appConfig.memberService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
}
public static void main(String[] args){
AppConfig appConfig = new AppConfig();
MemberSerivce memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(1L, "itemA", 10000);
}
테스트 코드를 실행할 때 마다 @beforeEach를 사용하여, 객체를 생성해줄 수 있다.
현재, AppConfig에는 MemoryMemberRepository가 두번 생성되는 중복이 있고, 역할에 따른 구현이 잘 보이지 않는다. 다음과 같이 코드를 변경할 수 있다.
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 RateDiscountPolicy();
}
}
한 클래스는 하나의 책임만 가져야 한다.
프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다.
소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다.
** 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부(AppConfig)에서 관리하는 것을 제어의 역전(IoC)라고 한다.
기존에 클라이언트 코드에서 객체를 생성하고, 객체를 연결하고, 실행했지만 AppConfig를 통해서 객체를 생성하고, 클라이언트 코드에 주입해준 덕분에 클라이언트 코드는 어떤 구현 객체들이 실행될지 모르는 상태에서 단지 필요한 인터페이스를 호출하는 등 자신의 로직을 실행한다.
정적인 클래스 의존 관계
클래스가 사용하는 import 코드만으로 정적인 의존 관계를 쉽게 판단할 수 있다.
OrderServiceImpl는 DiscountPolicy와 MemberRepository를 의존한다는 것을 알 수 있다.
실행 시점에 결정되는 동적인 객체 의존 관계
애플리케이션 실행 시점에 외부(AppConfig)에서 실제 구현 객체를 생성하고, 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결되는 것을 의존 관계 주입 이라고 한다.
의존 관계 주입을 사용하면, 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존 관계를 쉽게 변경할 수 있다.
다음 글은 인프런 김영한 강사님의 스프링 강의 복습용입니다 :)