인프런 강의 김영한
9 회원 도메인 개발
9.1 회원 엔티티
9.1.1 회원등급
열거클래스로 만든다.
public enum Grade {
BASIC,
VIP
}9.1.2 회원 엔티티
public class Member {
private Long id;
private String name;
private Grade grade;
//getter, setter
}9.1.3 MemberReository 인터페이스
public interface MemberReository {
void save(Member mebmer);
Member findById(Long memberId);
}9.1.4 구현클래스
Member클래스를 모으는 Map
public class MemoryMemberReository implements MemberReository{
public static Map<Long, Member> store = new HashMap<>();
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}9.1.5 MemberSrvice
join과 findMember기능 두가지만 가지고 있다.
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}9.1.6 MemberService 구현클래스
public class MemberServiceImpl implements MemberService{
private MemberReository memberReository = new MemoryMemberReository();
@Override
public void join(Member member) {
memberReository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberReository.findById(memberId);
}
}10. 회원 도메인 실행과 테스트
도메인이 정상적으로 되는지 테스트하기
클라이언트 -> 회원서비스 -> 메모리회원저장소로 이어진다.
메인메소드를 가진 MemberApp를 만들어서 테스트 해볼것이다.
public class MemberApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
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());
}
}순수한 자바코드로 로직을 구현하것이다. 스프링과는 관계가없다.
그러나 메인메소드 로직은 한계가 잇어서 JUNIT을 사용하는 것이좋다.
main에서 할때는 눈으로 검증을해야하는데 JUNIT은 다 볼 수 있다.
테스트코드는 선택이 아니라 필수이다.
class MemberServiceTest {
MemberService memberService = new MemberServiceImpl();
@Test
void join() {
// given
Member member = new Member(1L, "memberA", Grade.VIP);
// when
memberService.join(member);
Member findMember = memberService.findMember(1L);
// then
assertThat(member).isEqualTo(findMember);
}
}회원 도메인 설계의 문제점이 존재한다.
이 코드의 설계상 문제점은 무엇일까요?
다른 저장소로 변경할 때 OCP 원칙을 잘 준수할지, DIP를 잘 지키고 있는지
의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있다.
-> 문제점을 변경해야할 필요가 있다. 추상화에도 의존하고 구체화에도 의존한다.
11. 주문과 할인 도메인 설계
주문과 할인 정책 목표
회원은 상품을 주문할 수 있다.
회원 등급에 따라 할인 정책을 적용할 수 있다.
할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라.
(나중에 변경 될 수있다.)
할인 정책은 변경 가능성이 높다.
회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을미루고 싶다.
최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
클라이언트
1.주문 생성: 클라이언트는 주문 서비스에 주문 생성을 요청한다.
주문서비스
회원저장소역할
2.회원 조회: 할인을 위해서는 회원 등급이 필요하다.
그래서 주문 서비스는 회원 저장소에서 회원을 조회한다.
할인정책역할
3.할인 적용: 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임한다.
4.주문 결과 반환: 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다
실제로는 주문 데이터를 DB에 저장하겠지만, 예제가 너무 복잡해 질 수 있어서 생략하고, 단순히 주문결과를 반환한다.
역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계해야한다.
그러면 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다
클라이언트 -> 주문서비스 구현체 -> 메모리회원저장소, 정액할인정책
이게 변경되더라도 db회원저장소, 정률할인을 하면 주문서비스 구현체는 변경될 필요가 없는 것이다.
12. 주문과 할인 도메인 개발
12.1 DiscountPolicy 인터페이스
public interface DiscountPolicy {
// return 할인 대상 금액
int discount(Member member, int price);
}12.2 DiscountPolicy 구현객체
vip면 1000원할인
public class FixedDiscountPolicy implements DiscountPolicy {
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}12.3 order entity
public class Order {
private Long memberId;
private String itemName;
private int itemPrice;
private int discountPrice;
// 비즈니스 로직
public int calculatePrice() {
return itemPrice - discountPrice;
}
//생성자 getter setter
}12.3 order Service
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}12.3 orderService 구현
OrderService입장에서는 얼마가 할인되는지 모른다.
discountPolicy에게 할인을 해달라고 시키고 값만 받아온다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
@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);
}
}13. 주문과 할인 도메인 실행과 테스트
main메소드로 자바 앱을 실행시켜보면 다음과 같다.
public class OrderApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
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.toString());
}
}물론 자동화된 테스트를 하는게 좋다.
스프링테스트는 오래걸리니 단위테스트 순수자바코드로만 테스트하는게 좋다.
class OrderServiceTest {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();
@Test
void createOrder() {
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}역할 구현을 잘하긴했는데 과연 깔끔하게 나누엇는가?
쉽게 바꿀수있는가?를 알아보자.
스프링 핵심 원리 이해2 - 객체 지향 원리 적용
14.새로운 할인 정책 개발
기획자가 새로운 할인정책을 요구했다.
금액당 할인하는 정률% 할인으로 변경하고싶다.
그런데 문제들이 발생하게 된다.
이전 강의들에서 정말 객체지향 설계 원칙을 잘 준수 했는지 확인해보자.
이번에는 주문한 금액의 %를 할인해주는 새로운 정률 할인 정책을 추가해보자
public class RateDiscountPolicy implements DiscountPolicy {
private int discountPercent = 10;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}꼭 작성하고 TEST를 돌려봐서 맞는지 확인하자.
class RateDiscountPolicyTest {
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("vip는 10% 할인이 적용되어야한다.")
void vip_o() {
// given
Member member = new Member(1L, "memverVip", 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(1L, "memverBASIC", Grade.BASIC);
// when
int discount = discountPolicy.discount(member, 10000);
// then
assertThat(discount).isEqualTo(0);
}
}로직을 실제 실무에서는 테스트가 정말많다.
딱할인만 만들어놧기때문에 테스트가 쉬운 것이다.
실제 실무에서는 테스트할게 정말많다.
15. 새로운 할인 정책 적용과 문제점
실제 적용하려면 OrderServiceImpl로가서
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
Injection하는 것을 직접 바꿔줘야한다.
문제점 발견
우리는 역할과 구현을 충실하게 분리했다. -> OK
다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. -> OK
OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다?
그렇게 보이지만 사실은 아니다.
DIP: 주문서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데?
클래스 의존관계를 분석해 보자.
추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.
추상(인터페이스) 의존: DiscountPolicy
구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
결국 DIP위반하고있는 것이다.
OCP: 변경하지 않고 확장할 수 있다고 했는데?
지금 코드는 기능을 확장해서 변경하면 OrderServiceImpl도 변경해야한다.
즉 클라이언트 코드에 영향을 주니 OCP를 위반이다.
어떻게 문제를 해결할 수 있을가?
클라이언트 코드인 OrderServiceImpl 은 DiscountPolicy 의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존한다.
그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 했다.
DIP 위반 추상에만 의존하도록 변경(인터페이스에만 의존) DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다
private final DiscountPolicy discountPolicy;그런데 구현체가 할당되지 않아서 NullPointerException이 발생하게 된다.
해결방안
이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에
DiscountPolicy 의 구현 객체를 대신 생성하고 주입해주어야 한다.
16. 관심사의 분리
애플리케이션을 하나의 공연이라 생각해보자.
각각의 인터페이스를 배역(배우 역할)이라 생각하자.
그런데 실제 배역 맞는 배우를 선택하는 것은 누가 하는가?
로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지 줄리엣 역할을 누가 할지는 배우들이 정하는게 아니다.
이전 코드는 마치 로미오 역할(인터페이스)을 하는 레오나르도 디카프리오(구현체, 배우)가
줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다.
디카프리오는 공연도 해야하고 동시에 여자 주인공도 공연에 직접 초빙해야 하는 다양한 책임을 가지고 있다.
관심사를 분리해야한다. 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다.
디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야 한다.
공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 따로 있어야한다.
공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리해야한다.
OrderServiceImpl(로미오)이 DiscountPolicy(줄리엣)의 구현객체를 지정한 것이다.
애플리케이션도 이걸 분리해서 만들어야한다.
해결방법으로 AppConfig이 등장하였다.
애플리케이션의 전체 동작 방식을 구성, 설정하기 위해,
구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들어주자.
MemberServiceImpl가 어떤 Repository를 쓸건지를 생성자로 넣는다.
진짜로 세팅하는 것은 AppConfig에서 세팅한다.
public class MemberServiceImpl implements MemberService {
private MemberRepository memberReository;
public MemberServiceImpl(MemberRepository memberReository) {
this.memberReository = memberReository;
}
@Override
public void join(Member member) {
memberReository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberReository.findById(memberId);
}
}OrderServiceImpl이 사용하는 것도 따로 두고 AppConfig에서 뭐가 들어올지 정해준다.
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);
}
}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
이걸 마치 주입해준다고 한다. 생성자로 주입했기 때문에 생성자 주입이라고 한다.
의존성 주입 Dependency Injection DI
설계 변경으로 MemberServiceImpl 은 MemoryMemberRepository 를 의존하지 않는다
단지 MemberRepository 인터페이스만 의존한다.
MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부( AppConfig )에서 결정된다.
MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.
Test를 해보자. AppConfig객체를 만들고 AppConfig에 의해서 서비스 객체를 생성하면된다.
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
assertThat(member).isEqualTo(findMember);
}
}정리
AppConfig를 통해서 관심사를 확실하게 분리했다.
배역, 배우를 생각해보자.
AppConfig는 공연 기획자다.
AppConfig는 구체 클래스를 선택한다.
배역에 맞는 담당 배우를 선택한다.
애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다.
각 배우들은 담당 기능을 실행하는 책임만 지면 된다.
OrderServiceImpl, MemberServiceImpl은 기능을 실행하는 책임만 지면 된다.
17. AppConfig 리팩터링
AppConfig은 중복이 있고 역할에 따른 구현이 잘 안보인다.
설정정보는 한눈에 역할이 보이는게 중요하다.
public class AppConfig {
private MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(),discountPolicy());
}
}new MemoryMemberRepository() 이 부분이 중복 제거되었다.
역할과 구현 클래스가 한눈에 들어온다.
애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다
'기초단계 > SPRING' 카테고리의 다른 글
| 2023.05.16 Spring (0) | 2023.05.16 |
|---|---|
| 2023.05.13 Spring (0) | 2023.05.16 |
| 2023.05.10 Spring (0) | 2023.05.11 |
| 2023.05.09 Spring (0) | 2023.05.09 |
| 2023.05.04 Spring (0) | 2023.05.09 |