할인 정책이 변경되었다. 고정 금액이 아닌 금액당 할인하는 방법으로 변경하고 싶다.
DiscountPolicy 인터페이스를 구현한 RateDiscountPolicy를 추가해준다.
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10; //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(0);
}
}
테스트는 잘 작동한다. 이 할인 정책을 애플리케이션에서 적용하고 싶다.
앞 장에서 만들었던 구현체를 다시 수정해줘야한다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
...
}
문제점
역할과 구현을 잘 구현하였다. 다형성을 활용하고 인터페이스와 구현 객체도 분리하였다.
OCP 위반 : 현재 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 주게 된다.
DIP 위반 : 클래스 의존관계를 분석해보면, 추상 뿐만 아니라 구현 클래스에도 의존하고 있다.
->RateDiscountPolicy
나FixDiscountPolicy
가 아닌DiscountPolicy
인터페이스에 의존해야한다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
...
}
구현체가 없어서 null pointer exception
이 발생한다.
=> OrderServiceImpl
에 DiscountPolicy
의 구현 객체를 대신 생성하고 주입해줄 뭔가가 필요하다.
책임을 분산시켜라. 객체를 생성하고 연결하는 역할과 실행하는 역할을 분리하자.
구현 객체를 생성하고 연결하는 책임을 가지는 뭔가가를 만들어준다.
AppConfig
에서는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
생성한 객체 인스턴스의 참조를 생성자를 통해서 주입해준다.
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;
}
}
- 설계 변경으로
MemberServiceImpl
는 더이상MemoryMemberRepository
를 의존하지 않는다.
=>MemberRepository
인터페이스를 의존한다.MemberServiceImpl
의 생성자를 통해서 어떤 구현 객체를 주입하는지는AppConfig
에서 결정된다.MemberServiceImpl
의 의존관계는 외부에 맡기고 실행에만 집중한다.
Dependency Injection 로 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라고 한다.
public class OrderServiceImpl implements OrderService {
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
앞서 만들었던 Test 코드에서 사용하기 위해서는 AppConfig를 선언하고 해당 서비스를 사용하면 된다.
MemberService
class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
}
FixDiscountPolicy => RateDiscountPolicy 로 변경하려고 한다.
이제는 AppConfig에서 DiscountPolicy에 대한 구현만 변경해준다.
아래는 한번 중복을 제거하고 조금더 명시적으로 리팩터링했다.
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();
return new RateDiscountPolicy();
}
}
이제는 할인 정책을 변경해도,클라이언트 코드인
OrderServiceImpl
를 변경하지 않고 애플리케이션의 구성 역할을 담당하는AppConfig
만 변경하면 된다.
지금까지 순수한 자바 코드만을 이용해 AppConfig을 생성하고, DI를 적용했다.
이 AppConfig를 DI 컨테이너라고도 부른다.
이제 이 코드를 스프링으로 전환해보자
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
@Configuration
과 @Bean
이 추가되었다.
@Configuration
: AppConfig에 설정을 구성한다.@Bean
: 스프링 컨테이너에 스프링 빈으로 등록한다.사용할 때 조금은 복잡해 보일 수 있다.
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
- ApplicationContext 를 스프링 컨테이너라고 부른다.
- 기존에는 AppConfig를 이용해 직접 객체를 생성하고 DI를 했지만, 지금부터는 스프링을 이용한다.
- 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정 정보로 사용한다.
- @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
- 이전에는 개발자가 필요한 객체를 AppConfig를 사용해서 조회했지만, 이제부터는 스프링 컨테이너를 통해 빈을 찾는다 (getBean)
코드가 더 복잡해진 것 같은데 다음장에서 스프링의 장점을 찾아보자