스프링 핵심 원리 (기본편) 강의 정리 - 3 객체 지향 적용
2023. 9. 19. 23:29ㆍSpring/Spring 김영한
📌 새로운 할인 정책 개발 📌
❗ 목표 FixDiscountPolicy(정액 할인 정책)에서 RateDiscountPolicy(정률 할인 정책)으로 변경
❗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;
}
}
}
❗TestCode 작성
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);
}
}
❗새로운 할인 정책 적용과 문제점
애플리케이션에 변경 내용을 적용시키면 아래와 같이 클라이언트 코드인 OrderServiceImpl을 수정해야 한다.
public class OrderServiceImpl implements OrderService {
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}
❗문제점
- 역할과 구현을 분리했는가? (O) 인터페이스를 만들고 각 구현은 클래스에서 함.
- 다형성 활용, 인터페이스와 구현 객체를 분리했는가? (O)
- OCP, DIP 같은 객체지향 설계 원칙을 준수했는가? (X)
- DIP(X)
- OrderServiceImpl은 DiscountPolicy인터페이스에 의존한 것처럼 느껴지지만 사실은 new를 통해 직접 구현체를 할당해주고 있다. 즉, 추상화에 의존하는 것이 아닌 구체클래스에도 의존하고 있는 것이다.
- OCP : 변경하지 않고 확장가능한가?
- 기능을 확장, 변경하기 위해서는 클라이언트 코드를 수정해야 한다.
❗해결법
- OrderServiceImpl이 인터페이스에만 의존하게 수정한다.
- 하지만 여전히 문제가 생긴다. 구현체가 존재하지 않아 NullPointerExceptions를 발생시킨다.
- 어떻게 해야 할까,,? -> 누군가 외부에서 주입해 보자!
public class OrderServiceImpl implements OrderService {
//private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
}
❗관심사의 분리란?
애플리케이션을 공연이라고 생각하면, 각 인터페이스가 배역(ex:로미오)이라고 생각하자. 그러면~ 배역에 맞는 캐스팅은 누가 하는 게 맞는가? 당연히 공연 기획자여야 한다. 하지만 우리의 코드에서는 클래스가 클래스에 의존하고 있으므로 로미오역에 레오나르도 다빈치가 있다면 레오나르도 다빈치가 줄리엣 역할까지 캐스팅하는 모순적인 모습인 것이다. 즉, 다양한 책임을 지고 있는 것이다. 🛠앞으로는 공연기획자 역할의 무언가가 필요할 것이다.🛠
*❗AppConfig(공연기획자) 사용하기 *
AppConfig 코드
public class AppConfig {
public MemverService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(),new FixDiscountPolicy());
}
}
⚒ 기능: 구현객체를 생성해 주고 외부(AppConfig)에서 생성자를 통해서 주입해 줌.
- MemberServiceImpl -> MemoryMemberRepository
- OrderServiceImpl -> MemoryMemberRepository, FixDiscountPolicy
⚒ 이제 AppConfig객체를 이용해 나머지 코드의 오류를 수정해 주자!
❗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, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order =" + order);
}
}
❗OrderServiceTest코드 오류 수정
public class MemberServiceTest {
AppConfig appConfig=new AppConfig();
MemberService 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);
}
}
❗MemerServiceTest코드 오류 수정
public class OrderServiceTest {
AppConfig appConfig=new AppConfig();
MemberService memberService = appConfig.memberService();
OrderService 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);
}
}
- 정리
- AppConfig를 이용해서 관심사를 확실하게 분리함.
- 배역과 , 배우 예시
- AppConfig는 기획자 역할, 구현은 별도로 분리할 수 있게 해 줌.
- 구체 클래스를 선택하고, 생성자를 주입해 줌으로써 구현 클래스들은 기능을 실행하는 책임만 지면 됨.
- AppConfig를 이용해서 관심사를 확실하게 분리함.
❗AppConfig 리팩토링
기존 코드
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(
new MemoryMemberRepository(),
new FixDiscountPolicy());
}
}
❗기존코드의 문제점
MemoryMemberRepositort부분이 중복되어 들어간다. 이 부분을 별도로 분리해 보자!
더욱 역할에 따른 구현이 명료하게 보인다.
❗기대하는 그림
❗리팩토링 후
중복을 제거하고 역할에 따른 구현을 더 잘 보이게 하자.
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
❗새로운 구조와 할인 정책 적용
- 정률할인 정책으로 변경해 보자.
- 생각해 보자 어떻게 하면 될까?
- 역할과 구현을 분리했으니, 기획자만 수정하면 된다. 즉, AppConfig의 DiscountPolicy를 수정해 주면 손쉽게 정책을 변경할 수 있다.
- 생각해 보자 어떻게 하면 될까?
❗정률할인정책 적용코드
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(
memberRepository(),
discountPolicy());
}
public DiscountPolicy discountPolicy(){
return new RateDiscountPolicy();
}
}
❗좋은 객체 지향 설계의 5가지 원칙의 적용
우리는 SOILD 5개 중 SRP, DIP, OCP를 적용함.
하나씩 살펴보자!
SRP 단일 책임 원칙
- 한 클래스는 하나의 책임을 가진다.
- 클라이언트 객체는 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임이 따릅니다.
- SRP 단일 책임 원칙에 의해 관심사를 분리함.
- 구현객체를 생성하고 연결해주는 역할을 AppConfig가 담당함
- 클라이언트 객체는 실행하는 책임을 담당함.
DIP 의존관계 역전 원칙
- 프로그래머는 "추상화에 의존해야지 구체화에 의존해서는 안된다"
- DIP를 따르는 방법중 하나인 의존성 주입을 사용한다.
- OrderServiceImpl은 추상화인 DiscountPolicy 인터페이스에 의존하는 것 같았지만, 실상은 RateDiscountPolicy에 같이 의존하는 형태였음.
- 새로운 할인 정책을 개발하고 적용하려면 클라이언트 코드를 수정해야 했으므로, DIP를 위반하는 상태였음.
- AppConfig가 FixDiscountPolicy 객체 인스턴스를 대신 생성하여 클라이언트 코드에 의존관계를 주입함. 이렇게 해서 DIP 원칙을 따르면서 문제도 해결했다!
OCP
- 소프트웨어 요소는 확장에는 열려있으나, 변경에는 닫혀 있어야한다.
- 이전의 우리는 불가능한 이상적인 얘기라고만 생각했지만, 의존성 주입으로 해결가능함을 알았다.
- AppConfig가 FixdiscountPolicy를 RateDIscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됐다.
- 즉 우리가 새로운 소프트웨어 요소를 개발해 확장해도 클라이언트 코드는 변경할 필요가 없다.
- DIP와 다형성을 활용한 결과이다.
❗IoC, DI, 그리고 컨테이너
- 기본 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.
한마디로 구현 객체가 프로그램의 제어 흐름을 조정함. - AppConfig가 등장한 이후에 구현 객체는 스스로 로직을 실행하는 역할만 담당한다.
- 프로그램의 제어 흐름은 이제 AppConfig가 가져감.
- 예를 들어, OrderSerivceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.
- 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전 (IoC)이라고 한다.
프레임워크 vs 라이브러리
- 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다.
- 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.
❗의존관계 주입 DI
- OrderServiceImpl은 DIscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
- 의존 관계는 정적인 클래스 의존관계와, 실행 시점에 결정되는 동적인 객체 의존관계 둘을 분리해서 생각해야 한다.
- 애플리케이션 런타임에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.
'Spring > Spring 김영한' 카테고리의 다른 글
스프링 핵심 원리(기본편) 강의정리 - 2 (멤버편) (0) | 2023.09.16 |
---|---|
스프링 핵심 원리(기본편) 강의정리 - 1 (0) | 2023.09.12 |