[작성일: 2023. 08. 01]
새로운 할인 정책 개발
이번에는 주문한 금액의 %를 할인해 주는 새로운 정률 할인 정책을 추가하자.
RateDiscountPolicy 추가
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;
} else {
return 0;
}
}
}
테스트
- 테스트는 성공 테스트도 중요하지만 실패할 경우의 테스트도 꼭 진행해봐야 한다.
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다.")
void vip_o() {
// given
Member member = new Member(1L, "memberVIP", Grade.VIP);
// when
int discount = discountPolicy.discount(member, 10000);
// then
assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x() {
// given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
// when
int discount = discountPolicy.discount(member, 10000);
// then
assertThat(discount).isEqualTo(1000);
}
}
새로운 할인 정책 적용(1)
새로운 할인 정책을 적용하려면 아래와 같이 OrderServiceImpl를 수정해야 한다.
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
여기서 문제점이 발견된다.
- 우리는 역할과 구현을 충실하게 분리했다. ✅
- 다형성을 활용하여 인터페이스와 구현 객체를 분리했다. ✅
- 객체지향 설계 원칙(SOLID)를 충실히 준수했다. ❎
- 클래스의 의존관계를 분석해보면 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
- 인터페이스 의존: DiscountPolicy
- 구현 클래스 의존: FixDiscountPoilcy, RateDiscountPolicy
- 지금 코드는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 주므로 OCP를 위반하게 된다.
- OrderServiceImpl 클래스는 DiscountPolicy에만 의존을 하고 있다고 생각했지만 코드를 자세히 보면 구현 클래스에도 의존을 하고 있으므로 DIP도 위반하게 된다.
그럼 문제를 어떻게 해결할 수 있을까?
DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private DiscountPolicy discountPolicy = new FixDiscountPolicy();
private DiscountPolicy discountPolicy;
// ... 생략
}
이렇게만 수정하고 테스트를 실행해보면 Null pointer Exception이 발생한다.
이 문제를 해결하려면 누군가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.
관심사의 분리
애플리케이션을 하나의 공연이라고 생각하고 각각의 인터페이스를 배우 역할이라고 생각해 보자.
로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지, 줄리엣 역할을 누가 할지 배우들이 정하지는 않는다.
이전 코드는 마치 로미오 역할을 하는 A 배우가 여자 주인공 B를 직접 초빙하는 코드와 같다.
배우 A는 공연도 해야 하고 동시에 여자 주인공도 직접 초빙해야 하는 다양한 책임을 갖고 있다.
AppConfig 생성
애플리케이션의 전체 동작 방식을 구성(config) 하기 위해 구현 객체를 생성하고, 연결하는 책임을 갖는 별도의 설정 클래스를 만든다.
위의 예시로 들자면 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실하게 분리하는 역할을 한다.
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
- MemberServiceImpl
- MemoryMemberRepository
- OrderServiceImpl
- FixDiscountPolicy
AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결) 해준다.
- MemberServiceImpl ➡️ MemoryMemberRepository
- OrderServiceImpl ➡️ MemoryMemberRepository, FixDiscountPolicy
아래 코드와 같이 수정하고 생성자까지 만들어야 컴파일 오류가 발생하지 않는다.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// ... 생략
}
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
// private DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
// ... 생략
}
설계 변경으로 MemberServiceImpl은 MemoryMemberRepository를 의존하지 않게 된다.
단지 MemberRepository 인터페이스에만 의존할 뿐이다.
MemberServiceImpl의 생성자를 통해 어떤 구현 객체를 주입할지 AppConfig에서 결정되므로 의존 관계에 대한 고민은 외부에 맡기고 실행에만 집중할 수 있다. (OrderServiceImpl도 마찬가지)
클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해 주는 것 같다고 해서 DI(Dependency Injection), 의존관계 주입 또는 의존성 주입이라고 한다.
사용 클래스 및 테스트 코드 수정
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();
// ... 생략
}
}
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
// ... 생략
}
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
// ... 생략
}
AppConfig 리팩토링
현재 AppConfig를 보면 중복도 있고 역할에 따른 구현이 잘 보이지 않는다.
중복을 제거하고 역할에 따른 구현이 잘 보이도록 리팩토링 하자. (mac 단축키: command + option + m)
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
새로운 할인 정책 적용(2)
다시 처음으로 돌아가서 정액 할인 정책을 정률 % 할인 정책으로 변경해 보도록 하자.
FixDiscountPolicy를 RateDiscountPolicy로 변경해도 구성 영역만 영향을 받고, 사용 영역에는 전혀 영향을 주지 않는다.
구성 영역은 당연히 변경되는 것이다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자.
공연 기획자는 당연히 공연 참여자인 구현 객체들을 모두 알아야 한다.
public class AppConfig {
// ... 생략
public DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
좋은 객체 지향 설계 5가지 원칙의 적용
SRP 단일 책임 원칙
- 한 클래스는 하나의 책임만 가져야 한다.
- 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당한다.
- 클라이언트 객체는 실행하는 책임만 담당한다.
DIP 의존 관계 역전 원칙
- 프로그래머는 추상화에 의존하고 구체화에 의존하면 안 된다.
- 클라이언트 코드가 DiscountPolicy 추상화 인터페이스에만 의존하도록 코드를 변경한다.
- AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입한다.
OCP 개방-폐쇄의 원칙
- 소프트웨어 요소는 확장에는 열려 있고 변경에는 닫혀 있어야 한다.
- 애플리케이션을 사용 영역과 구성 영역으로 나눈다.
- AppConfig가 의존 관계를 FixDiscountPolicy에서 RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 된다.
🐣 출처: 인프런 김영한님 강의
🐣 참고: 애자일 소프트웨어 개발 선언 https://agilemanifesto.org/iso/ko/manifesto.html
이 글은 인프런의 김영한님 스프링 강의를 보고 작성한 글입니다.
강의를 들으면서 정리한 글이므로 틀린 내용이나 오타가 있을 수 있습니다.