이번 시간에는 스프링의 객체 지향 설계를 활용해서 할인 정책을 유연하게 바꾸는 구조를 어떻게 만들 수 있는지 배웠다. 강의에서 설명한 내용을 정리한 내용이다.
정액 할인에서 정률 할인으로 변경
기존에는 VIP 회원에게 고정 금액(1000원)을 할인해주는 정책이 적용되어 있었다. 그런데 어느 날 기획자가 정률 할인으로 바꿔달라고 요청 하는 상황이다. 따라서 주문 금액의 10%를 할인해주는 방식으로 정책 클래스를 하나 새로 만들게 된다.
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
}
return 0;
}
}
정책을 변경할 때 문제점
문제는 정률 할인 정책을 실제 애플리케이션에 적용하려고 할 때 발생한다. 기존 코드에서 FixDiscountPolicy를 사용하고 있었는데 이걸 RateDiscountPolicy로 바꾸려면 OrderServiceImpl 코드를 직접 수정해야 했다.
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
이 방식은 얼핏 보면 괜찮아 보이지만 강의에서 말하길 DIP랑 OCP를 위반한다고 한다.
- DIP: 인터페이스(DiscountPolicy)에 의존해야 하는데 구현체(RateDiscountPolicy)에도 직접 의존하고 있다.
- OCP: 정책을 확장하려면 클라이언트 코드를 수정해야 하니까 닫혀 있어야 할 변경이 열려버린 셈이다.
관심사를 분리해야 한다.
강사님은 이 상황을 공연에 비유해서 설명해 주셨다. "배우는 연기만 하면 되고, 어떤 배우를 쓸지는 기획자가 정해야 한다."
현재 코드에서는 배우가 기획까지 하고 있는 꼴이다. OrderServiceImpl이 어떤 할인 정책을 쓸지까지 스스로 결정하고 있으니 책임이 너무 많다. 그래서 이걸 분리해야 한다. 실행은 서비스가 하고, 어떤 객체를 쓸지는 외부에서 결정하도록 구조를 바꿔야 한다.
AppConfig 등장
이런 역할을 담당하는 게 바로 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(); // 여기를 바꾸면 정책 변경 가능
}
}
이제는 OrderServiceImpl은 어떤 할인 정책이 들어올지 몰라도 된다. 그냥 DiscountPolicy라는 인터페이스만 알고 있으면 된다.
생성자 주입 방식 적용
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;
}
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
이렇게 해두면 나중에 할인 정책을 바꾸고 싶을 때 AppConfig에서만 바꾸면 된다.
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy(); // 정책 변경은 여기 한 줄이면 끝
}
클라이언트 코드에는 전혀 영향을 주지 않으니 유지보수도 편하고 구조도 깔끔하다.
지금까지 배운 것 정리
- 서비스 코드에서 직접 구현체를 만들지 않고 외부(AppConfig)에서 생성해서 넣어주는 방식이 더 유연하다.
- DIP를 지키려면 인터페이스만 의존해야 하고, 구체 클래스는 외부에서 주입해야 한다.
- OCP를 지키려면 새로운 정책을 추가하거나 교체할 때 클라이언트 코드를 바꾸지 않아야 한다.
- AppConfig를 사용해서 구성과 실행을 분리하면 이런 구조를 쉽게 만들 수 있다.