본 포스팅은 인프런 - 스프링 핵심 원리(기본편)을 강의를 바탕으로 공부하고 정리한 글입니다.
새로운 할인 정책 도입
- 고정 할인(1000원)이 아닌 금액당 할인하는 정률 할인(10%)으로 변경
- 이전에 역할과 구현을 분리 해놓았기 때문에 RateDiscountPolicy 구현체만 추가해주면 된다.
개발
👉🏻 RateDiscountPolicy 추가
📁 discount/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; // VIP 등급만 할인 적용
else
return 0;
}
}
테스트
📁 test/discount/RateDiscountPolicyTest
class RateDiscountPolicyTest {
DiscountPolicy discountPolicy = new FixDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다.")
void vip_o() {
Member member = new Member(1L, "memberVIP", Grade.VIP);
int discount = discountPolicy.discount(member, 10000);
assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x() {
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
int discount = discountPolicy.discount(member, 10000);
assertThat(discount).isEqualTo(0);
}
}
- @DisplayName : 각각의 테스트에 이름을 표기할 수 있다.
실제 적용과 문제점
새로 개발한 정률 할인 정책을 실제로 적용하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
📁 order/OrderServiceImpl
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
💡 문제점
- DIP 원칙 위반 : 주문 서비스 클라이언트(OrderServiceImpl)는 인터페이스 뿐만 아니라 구현 클래스에도 의존하고 있다.
- 인터페이스 : DiscountPolicy
- 구현클래스 : RateDiscountPolicy, FixDiscountPolicy
- OCP 원칙 위반 : 기능을 확장, 변경하면 클라이언트 코드(OrderServiceImpl)에 영향을 준다.
문제 해결
DIP를 위반하지 않도록 인터페이스에만 의존하도록 변경해야 한다.
📁 order/OrderServiceImpl
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private final DiscountPolicy discountPolicy;
}
인터페이스에만 의존하도록 설계와 코드를 변경하였지만, 구현체가 없어 코드를 실행할 수 없다. (실제 실행시 null pointer exception 발생)
따라서 누군가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현체를 대신 생성하고 주입해줘야 한다.
관심사의 분리
AppConfig 생성
어플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들어 준다.
📁 AppConfig
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
생성자 주입
👉🏻 MemberServiceImpl 생성자 주입
📁 member/MemberServiceImpl
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
- MemberServiceImpl은 단지 MemberRepository 인터페이스만 의존한다.
- MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지는 알 수 없다.
- MemberServiceImpl의 생성자를 통해 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정된다.
👉🏻 OrderServiceImple 생성자 주입
📁 order/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;
}
}
💡 정리
- 객체의 생성과 연결은 AppConfig가 담당한다.
- DIP 완성 : MemberServiceImpl은 MemberRepository 인터페이스에만 의존하면 된다.
- 관심사의 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.
- appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImple을 생성하면서 생성자로 전달한다.
- 클라이언트인 memberServiceImple 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) , 의존관계 주입이라 한다.
AppConfig 실행
👉🏻 MemberApp 변경
public class MemberApp {
public static void main(String[] args) {
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
// 회원 생성
Member member = new Member(1L, "member1", Grade.VIP);
// 회원 가입
memberService.join(member);
// 회원 조회
Member findMember = memberService.findMember(1L);
System.out.println("find Member = " + findMember.getName());
System.out.println("new member = " + member.getName());
}
}
👉🏻 OrderApp 변경
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, "member1", Grade.VIP); // 회원 생성
memberService.join(member); // 회원 가입
Order order = orderService.createOrder(memberId, "item1", 10000); // 주문 생성
System.out.println("order = " + order);
}
}
테스트 수정
📁 member.MemberServiceTest
class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
...
}
📁 order.OrderServiceTest
class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
public void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
...
}
AppConfig 리팩토링
현재 AppConfig는 중복이 있고, 역할에 따른 구현이 잘 안보인다.
따라서 중복을 제거하고, 다음과 같이 역할에 따른 구현이 보이도록 리팩토링이 필요하다.
📁 AppConfig
public class AppConfig { // 어플리케이션의 전체 동작 방식
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
private DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
- new MemoryMemberRepository() 중복이 제거되었다. 따라서 MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다.
- AppConfig를 보면 역할과 구현 클래스가 한눈에 들어와 어플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.
새로운 구조와 할인 정책 적용
- 정액 할인 정책(FixDiscountPolicy) → 정률 할인 정책(RateDiscountPolicy)으로 변경
- AppConfig를 통해 어플리케이션이 크게 사용 영역과, 객체를 생성하고 구성하는 영역으로 분리되었기 때문에
다음과 같이 구성 영역만 변경해주면 된다.
👉🏻 할인 정책 변경 구성 코드
📁 AppConfig
public class AppConfig { // 어플리케이션의 전체 동작 방식
...
private DiscountPolicy discountPolicy() {
// return new FixDiscountPolicy(); // 정액 할인 정책
return new RateDiscountPolicy(); // 정률 할인 정책
}
}
- AppConfig에서 할인 정책 역할을 담당하는 구현을 FixDiscountPolicy → RateDiscountPolicy 객체로 변경했다.
- 할인 정책을 변경해도 클라이언트 코드인 OrderServiceImpl를 포함한 사용 영역의 어떤 코드도 변경할 필요 없이 구성 역할을 담당하는 AppConfig만 변경해주면 된다.
정리
- SRP 단일 책임 원칙
( 한 클래스는 하나의 책임만 가져야 한다. )- 클라이언트 객체가 직접 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있었음
- SRP 단일 책임 원칙을 따르면서 관심사를 분리함
- 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당, 클라이언트 객체는 실행하는 책임만 담당하게 됨.
- DIP 의존관계 역전 원칙
( 추상화에 의존해야지, 구체화에 의존하면 안된다. )- 새로운 할인 정책을 개발하고, 적용하려고 하니 클라이언트 코드도 함께 변경해야 했음
- 이유는 기존 클라이언트 코드(OrderServiceImpl)가 인터페이스(DiscountPolicy) 뿐만 아니라 구현 클래스(FixDiscountPolicy)도 함께 의존했기 때문
- 클라이언트 코드가 인터페이스에만 의존하도록 코드를 변경했지만, 클라이언트 코드는 인터페이스만으로는 아무것도 실행할 수 없는 문제가 발생
- AppConfig가 FixDiscountPolicy 객체 인스턴스를 대신 생성해 클라이언트 코드에 의존관계를 주입했다.
- 이렇게 해서 DIP 원칙을 따르면서 문제를 해결함
- OCP
( 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다. )- 다형성 사용하고 클라이언트가 DIP 원칙을 지킴
- 어플리케이션을 사용 영역과 구성 영역으로 나눔
- AppConfig가 의존관계를 FixDiscountPolicy → RateDiscountPolicy로 변경해 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨
- 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀있음.
'🌱 Spring > Core' 카테고리의 다른 글
[기본] #7 의존관계 자동 주입 (@Autowired) (0) | 2022.02.28 |
---|---|
[기본] #6 컴포넌트 스캔 (@ComponentScan, @Autowired) (0) | 2022.02.25 |
[기본] #5 싱글톤 (0) | 2022.02.24 |
[기본] #4 스프링 컨테이너, 스프링 빈 (0) | 2022.02.23 |
[기본] #2 회원, 주문, 할인 도메인 개발 및 테스트 (0) | 2022.02.21 |
[기본] #1 객체 지향 설계와 스프링 (0) | 2022.02.18 |
[입문] #7 AOP (0) | 2022.02.17 |
[입문] #6 DB 접근기술(JDBC, JdbcTemplate, JPA, SpringJPA) (0) | 2022.02.16 |