Spring 핵심원리 기본편 - 객체 지향 원리 적용(1)

1c2·2023년 11월 2일
0

1. 새로운 할인 정책 개발

이전에 만든 고정할인 정책 대신에 비율 할인 정책으로 변경하려고 한다.
비율 할인 정책을 개발해보자.

.../core/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;
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP) {return discountPercent*price / 100;}
        else{
            return 0;
        }
    }
}

해당 코드에 ctrl + shift + t를 누르면 테스트 코드를 바로 생성할 수 있다.

위 창에서 ok버튼을 누르면 테스트코드를 바로 만들 수 있다.

test/java/hello/core/discount/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 rateDiscountPolicy = new RateDiscountPolicy();
    @Test
    @DisplayName("VIP는 10% 할인이 적용되어야 한다")
    void vip_o(){
        //given
        Member member = new Member(1L, "memberVIP", Grade.VIP);
        //when
        int discount = rateDiscountPolicy.discount(member, 10000);
        //then
        Assertions.assertThat(discount).isEqualTo(1000);
    }
    @Test
    @DisplayName("VIP가 아니면 할인이 적용되면 안된다")
    void VIP_x(){
        //given
        Member member = new Member(1L, "memberVIP", Grade.BASIC);
        //when
        int discount = rateDiscountPolicy.discount(member, 10000);
        //then
        Assertions.assertThat(discount).isEqualTo(0);
    }
}

위와 같이 코드를 짜면 테스트를 통과하게 된다.

2. 새로운 할인 정책 적용과 문제점

할인 정책을 비율 할인 정책으로 바꿔보자.
OrderServiceImpl을 다음과 같이 수정한다.

문제점 발견

  • 우리는 역할과 구현을 충실하게 분리했다. -> OK
  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리했다. -> OK
  • OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다
    ->그렇게 보이지만 사실은 아니다 DIP: 주문서비스 클라이언트(OrderServiceImpl)는 DiscountPolicy인터페이스에 의존하면서 DIP를 지킨 것 같은데?

클래스 의존관계를 분석해 보자. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다.

추상(인터페이스) 의존: DiscountPolicy
구체(구현) 클래스: FixDiscountPolicy, RateDiscountPolicy
OCP: 변경하지 않고 확장할 수 있다고 했는데!

지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 준다!
따라서 OCP를 위반한다.

=>
그림으로 표현하면?

잘보면 클라이언트인 OrderServiceImplDiscountPolicy 인터페이스 뿐만 아니라 FixDiscountPolicy인 구체 클래스도 함께 의존하고 있다. 실제 코드를 보면 의존하고 있다!
=> DIP 위반


중요!: 그래서 FixDiscountPolicyRateDiscountPolicy로 변경하는 순간 코드도 함께 변경해야 한다!
=>
OCP 위반

어떻게 하면 이런 문제점을 해결하고

이렇게 설계할 수 있을까?

코드를 이런식으로 바꾸면 된다
-> 사실 안된다. discountPolicy에 아무것도 할당이 안되어 있기에 Nullptr exception이 터지게된다 .

해결 방안
=> 누군가가 클라이언트인 OrderServiceImplDiscountPolicy의 구현 객체를 대신 생성하고 주입하면 된다.

누군가 == AppConfig

3. 관심사 분리

DIP를 지키기 위해서 AppConfig를 생성한다.

.../core/AppConfig

package hello.core;

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;
import hello.core.discount.FixDiscountPolicy;


public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    public OrderService orderService() {
        return new OrderServiceImpl(
                new MemoryMemberRepository(),
                new FixDiscountPolicy()
        );
    }
}

위 처럼 그냥 의존 관계를 따로 정리해준다. (생성자를 통해서 의존관계를 주입한다)
이에 맞게 MemberServiceImplOrderServiceImpl의 생성자 또한 수정해주자.

.../core/member/MemberServiceImpl

package hello.core.member;

public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

.../core/order/OrderServiceImpl

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

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);
    }
}

생성자를 확인하면 어떠한 구현체가 사용되는지 알지 못하고 모두 인터페이스에만 의존하고 있다
=>DIP가 잘 지켜짐.

이에 대한 클래스 다이어그램을 그리면 다음과 같다.

AppConfig가 직접 MemberServiceImpl을 생성을 하고 이에 필요한MemberRepository의 구현체인 MemoryMemberRepository또한 생성해 주는 것이다.

객체 인스턴스 다이어그램을 그려보자

클라이언트인 MemberServiceImpl 입장에서는 누군가(AppConfig)가 주입해주는 memoryMemberRepository를 받아서 생성자를 통해 구현체를 리턴해주는 것이다.
=> DI(Dependenct Injection)

이전에 만들었던 memberApp을 수정해보자.

.../core/MemberApp

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;

public class MemberApp {
    public static void main(String[] args){
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        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());

    }
}

이제 AppConfig를 통해서 MemberService를 생성한다.

마찬가지로 OrderApp을 수정해보자.

.../core/OrderApp

package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
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

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;
import static org.junit.jupiter.api.Assertions.*;
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);
    }
}

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);
    }
}

모두 다 통과한다.

0개의 댓글