정률 할인 정책을 추가 개발한다.
package hello.core.discount;
import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;
public class RateDiscountPolicy implements DiscountPolicy {
private final int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
정률 할인에 대한 테스트 코드를 작성한다.
package hello.core.discount;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.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
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);
}
}
지금까지 구현한 코드를 보면, DIP와 OCP를 준수하지 않았다.
할인 정책을 변경하려면 클라이언트인 OrderServiceImpl의 코드를 고쳐야한다.
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!!
애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스.
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에 생성자를 만들어 주입할 수 있도록 수정한다.
package hello.core.member;
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public void join(Member member) {
memberRepository.save(member);
}
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
} }
MemberServiceImpl은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
OrderServiceImpl도 마찬가지로 수정해보자.
package hello.core.order;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
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를 추가해서 오류를 수정한다.
class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
}
class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
} }
여기까지 객체 지향의 5가지 원칙 중 3가지를 따르며 순수 자바 코드로 구현해보았다.
* SRP: 구현 객체를 생성하는 책임은 AppConfig가 맡고, 클라이언트 객체는 실행하는 책임만 담당하게 했다.
* DIP: AppConfig가 클라이언트 코드에 의존관계를 주입하게 해서 클라이언트 객체는 추상화에 의존하게 했다.
* OCP: 애플리케이션을 사용 영역과 구성 영역으로 나눠 새로운 확장에는 열려있고, 변경에는 닫혀있게 했다.
<IoC, DI, 그리고 컨테이너>
*IoC(Inversion of Control): 제어의 역전
- 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 IoC라고 한다.
- 프로그램의 제어 흐름에 대한 권한은 모두 AppConfig에 있기 때문에 IoC가 적용되었다.
*DI(Dependency Injection): 의존관계 주입
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해 클라이언트와 서버의 실제 의존관계가 연결되는 것을 DI라고 한다.
- DI를 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
*IoC 컨테이너, DI 컨테이너
- AppConfig와 같이 객체를 생성, 관리, 의존 관계를 연결해 주는 것을 IoC 컨테이너, 또는 DI 컨테이너 라고 한다.(최근에는 의존 관계 주입에 초점을 맞추어 주로 DI 컨테이너라고 한다.)
'공부 > Spring' 카테고리의 다른 글
[스프링 핵심](4) 싱글톤 컨테이너 (1) | 2024.01.07 |
---|---|
[스프링 핵심](3) 회원, 주문과 할인 예제를 스프링으로 전환 (0) | 2024.01.07 |
[스프링 핵심](1) 회원, 주문과 할인 예제 (0) | 2024.01.06 |
[스프링 입문](5) AOP (0) | 2024.01.06 |
[스프링 입문](4) 회원 관리 예제를 통한 웹 MVC 개발 2 (1) | 2024.01.06 |