mojo's Blog
DI(Dependency Injection) 본문
새로운 할인 정책 개발
이번엔 10% 할인이 적용되도록 해야 한다.
우선 [discount] 폴더 아래에 RateDiscountPolicy 클래스를 생성하고 다음과 같이 코드를 작성한다.
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10;
public int discount(Member member, int price){
if(member.getGrade() == Grade.VIP){
return price * discountPercent / 100;
}
else{
return 0;
}
}
}
※ ctrl + shift + t : 현재 만든 RateDiscountPolicy 를 테스트 할 수 있도록 [test] 폴더 아래에 RateDiscountPolicyTest 클래스가 생성된다.
이때 유용한 점은 RateDiscountPolicy 의 경로가 [test] 폴더에도 동일하게 적용된다는 점이다.
ctrl + shift + t를 이용하여 RateDiscountPolicyTest 클래스를 생성하고 다음과 같이 코드를 작성하여 테스트를 진행한다.
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
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
Assertions.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
Assertions.assertThat(discount).isEqualTo(0);
}
}
이번엔 실제로 10% 할인이 적용되도록 하는 할인 정책을 애플리케이션에 적용해보도록 한다.
할인 정책을 변경하기 위해서 OrderServiceImpl 코드를 다음과 같이 수정해야 한다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); <= new!
...
}
※ 문제점
- 역할과 구현을 충실하게 분리하였고 다형성도 활용하며 인터페이스와 구현 객체를 분리하였다.
- 하지만 OCP, DIP 같은 객체지향 설계 원칙을 준수하지 못하였다.
- DIP : 주문서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같지만 구현 클래스에도 의존하고 있다. (추상(인터페이스) 의존 : DiscountPolicy, 구현 클래스 : FixDiscountPolicy, RateDiscountPolicy)
- OCP : 변경하지 않고 확장할 수 있지만 현재 코드로는 기능을 확장해서 변경하면 클라이언트 코드에 영향을 주므로 OCP를 위반한다.
왼쪽 사진은 클라이언트인 OrderServiceImpl이 DiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 인 구체(구현) 클래스도 함께 의존하고 있으므로 DIP를 위반하고 있다.
그리고 오른쪽 사진과 같이 정책을 변경하여 FixDiscountPolicy를 RateDiscountPolicy로 변경하는 순간 OrderServiceImpl의 소스코드도 함께 변경해야 한다는 점에서 OCP 또한 위반하고 있음을 알 수 있다.
이러한 문제점을 어떻게 해결할 수 있을까?
- 클라이언트 코드인 OrderServiceImpl은 DIscountPolicy의 인터페이스 뿐만 아니라 구체(구현) 클래스도 함께 의존한다.
- 따라서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
- DIP를 위반하므로 추상(인터페이스)에만 의존하도록 변경해야 한다. (즉, 인터페이스에만 의존)
- DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.
다음과 같이 인터페이스에만 의존하도록 설계를 변경하면 된다.
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
private DiscountPolicy discountPolicy;
...
}
위 코드로 DiscountPolicy 인터페이스에만 의존하도록 코드를 변경해주면 DIP를 위반하지 않게 된다.
그런데 구현체가 없는에 어떻게 코드를 실행할 수 있는지에 대한 고민을 해봐야 한다.
실제로 실행해보면 NPE(Null Pointer Exception)가 발생한다.
※ 해결방안
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해줘야 한다.
관심사의 분리
※ AppConfig
애플리케이션의 전체 동작 방식을 구성(config)하기 위해 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 설정 클래스를 만든다.
우선 [main] -> [java] -> [hello] -> [core] 아래에 AppConfig 클래스를 생성하고 다음과 같이 코드를 작성한다.
package hello.core;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.
- MemberServiceImpl
- MemoryMemberRepository
- OrderServiceImpl
- FixDiscountPolicy
AppConfig는 생성한 객체 인스턴스의 참조(래퍼런스)를 생성자를 통해서 주입한다.
- MemberServiceImpl -> MemoryMemberRepository
- OrderServiceImpl -> MemoryMemberRepository, FixDiscountPolicy(or RateDiscountPolicy)
MemberServiceImpl 에서 생성자를 통해 주입하는 코드는 다음과 같다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
...
}
- 실제 변경으로 MemberServiceImpl은 MemoryMemberRepository를 의존하지 않는다.
- 단지 MemberRepository 인터페이스만 의존한다.
- MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어오는지(주입될지) 알 수 없다.
- MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
- MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
클래스 다이어그램는 아래와 같다.
객체의 생성과 연결은 AppConfig가 담당한다.
MemberServiceImpl은 MemberRepository 의 추상(인터페이스)에만 의존하면 된다. (구현 클래스를 몰라도 됨 -> DIP 완성)
객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다. (관심사의 분리)
appConfig 객체는 memoryMemberRepository 객체를 생성하고 memberServiceImpl 을 생성하면서 생성자로 전달한다.
클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection)이라 한다.
이번엔 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;
}
...
}
실제 변경으로 OrderServiceImpl은 FixDiscountPolicy를 의존하지 않는다. (단지 DiscountPolicy를 의존)
OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지는 알 수 없다.
OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
OrderServiceImpl 은 이제부터 실행에만 집중하면 된다.
OrderServiceImpl 에는 MemoryMemberRepository, FixDiscountPolicy(or RateDiscountPolicy) 객체의 의존관계가 주입된다.
이제 AppConfig을 통해서 제대로 동작하는지 확인해보도록 한다.
① MemberApp 클래스
package hello.core.member;
import hello.core.AppConfig;
public class MemberApp {
// psvm : 자동으로 public static void main 생성
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
// ctrl + alt + v : 자동으로 Member member 생성
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find member = " + findMember.getName());
}
}
② OrderApp
package hello.core.order;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
public class OrderApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService orderService = appConfig.orderService();
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order);
System.out.println("order.calculatePrice = " + order.calculatePrice());
}
}
③ MemberServiceTest
package hello.core.member;
import hello.core.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join(){
// given
Member member = new Member(1L, "memberA", Grade.VIP);
// when
memberService.join(member);
Member findMember = memberService.findMember(1L);
// then
Assertions.assertThat(member).isEqualTo(findMember);
}
}
④ OrderServiceTest
package hello.core.order;
import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder(){
Long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}
모두 성공적으로 실행되는것을 확인할 수 있다.
'Spring' 카테고리의 다른 글
스프링 컨테이너와 스프링 빈 (0) | 2022.01.25 |
---|---|
AppConfig 리팩터링 및 IoC, DI, 그리고 컨테이너 (0) | 2022.01.24 |
스프링 핵심 원리 이해 - 예제 만들기 (0) | 2022.01.23 |
객체 지향 설계와 스프링 (0) | 2022.01.20 |
AOP (0) | 2022.01.15 |