[Spring Boot][2] 3-1. 스프링 핵심 원리 이해 - 객체 지향 원리 적용

sorzzzzy·2021년 8월 22일
2

Spring Boot - RoadMap 1

목록 보기
5/46
post-thumbnail

🏷 새로운 할인 정책 개발

새로운 할인 정책을 확장해보자!

우리가 열심히 개발을 하고 있는데, 요구사항을 자꾸 변경하는 😡악덕기획자😡가 와서는,
서비스 오픈 직전에 할인 정책을 주문 금액당 퍼센트로 해주는 정률할인으로 변경해달라고 요구!

순진개발자 : 제가 첨부터 고정 할인 금액은 아니라고 했잖아요🤬
악덕기획자 : ㅇㅉㄹㄱ 변화에 대응하셈
순진개발자 : (Ha 니가 그럴 줄 알았다ㅋ 그럴 줄 알고 난 객체지향 설계 원칙을 준수했음 ㅅㄱ)

❗️순진 개발자가 정말 객체지향 설계 원칙을 잘 준수했는지 확인해보고, 이번엔 주문한 금액의 %를 할인해주는 새로운 정률할인 정책을 추가하자❗️

✔️ RateDiscountPolicy 추가

기존에 있었던 것들은 건들지 않고 RateDiscountPolicy 만 새로 개발하면 된다!

✔️ RateDiscountPolicy 코드 추가

hello.core/discount/RateDiscountPolicy.java

package hello.core.discount;

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

public class RateDiscountPolicy implements DiscountPolicy {
    
    // 10% 할인
    private int discountPercent = 10;   

    @Override
    public int discount(Member member, int price) {
        // 멤버 등급이 VIP인 경우에
        if(member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}

✔️ 테스트 작성

💡 command + shift + t 를 누르면 바로 테스트 클래스를 만들 수 있다!

test/../hello.core/discount/RateDiscountPolicyTest.java

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;


class RateDiscountPolicyTest {

    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("VIP는 10% 할인이 적용되어야 한다.")
    // VIP 라면
    void vip_o() {
        // given
        // 임의의 멤버 하나 생성
        Member member = new Member(1L, "memberVIP", Grade.VIP);

        // when
        // 10000원 일 때는 1000이 할인되어야 함
        int discount = discountPolicy.discount(member, 10000);

        // then
        // Assertions(org.assertj.core.api.Assertions)
        // 할인 금액이 1000이 맞는지 검증
        Assertions.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
        Assertions.assertThat(discount).isEqualTo(1000);
    }
}

성공 테스트도 중요하지만, 실패 테스트도 중요함❗️

⬆️ 두 가지 경우를 모두 돌려본 결과 👍🏻



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

이제, 새로 변경된 할인 정책을 적용할 차례❗️
할인정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.

hello.core/order/OrderServiceImpl.java 코드 수정

.
.
public class OrderServiceImpl implements OrderService {
    // 멤버 리포지토리에서 회원을 찾기 위함
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    // 고정 할인 정책
    // private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

    // 정률 할인 정책 ( 이 한줄만 새로 추가!! 정말 기가막히게 대단하죠❓)
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
.
.

여기서 문제점 발견🤚🏻

  • 우리는 역할과 구현을 충실하게 분리했나? -> 맞다❗️
  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리했나? -> 맞다❗️
  • OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했나 -> 그렇게 보이지만 사실 아니다❗️
    DIP : 주문서비스 클라이언트( OrderServiceImpl )는 DiscountPolicy 인터페이스에 의존하면서 DIP를 지킨 것 같은데🤔?
    아니^^? 클래스 의존관계를 분석해 봐봐. 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있잖아 !
  • 추상(인터페이스) 의존: DiscountPolicy (클라이언트 입장에서 서버)
  • 구체(구현) 클래스: FixDiscountPolicy , RateDiscountPolicy
    OCP: 변경하지 않고 확장할 수 있다고 했는데..🙁 지금 코드는 기능을 확장해서 변경하면, 클라이언트 코드에 영향을 주는구나! 따라서 OCP를 위반하게 되네!

Q. 왜 클라이언트 코드를 바꿔야 하는 거지🤔?
A. 클래스 다이어그램으로 의존관계를 분석해보자 !

지금까지 단순히 DiscountPolicy 인터페이스만 의존한다고 생각했다.


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


💡 중요 : 그래서 FixDiscountPolicyRateDiscountPolicy 로 변경하는 순간 OrderServiceImpl 의 소스 코드도 함께 변경해야 한다! OCP 위반

Q. 그럼 이 문제를 어떻게 해결할 수 있지?
A. 인터페이스에만 의존하도록 설계를 변경하면 돼!

  • 클라이언트 코드인 OrderServiceImpl 은 DiscountPolicy 의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존한다.
  • 그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 한다.
  • DIP 위반 -> 추상에만 의존하도록 변경(인터페이스에만 의존)
  • DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경하면 된다.

hello.core/order/OrderServiceImpl.java 코드 수정

.
.
public class OrderServiceImpl implements OrderService {
    // 멤버 리포지토리에서 회원을 찾기 위함
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    // 고정 할인 정책
    // private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

    // 정률 할인 정책 ( 이 한줄만 새로 추가!! 정말 기가막히게 대단하죠❓)
    // private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    
    private DiscountPolicy discountPolicy;
.
.

그런데, 그런데 구현체가 없는데 어떻게 코드를 실행할 수 있을까❓❓
실제 실행을 해보면 NPE(null pointer exception)가 발생한다❗️❗️

💡 해결 방안

  • 이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImplDiscountPolicy구현 객체를 대신 생성하고 주입해주어야 한다.


🏷 관심사의 분리

🌟 되게 중요한 이야기 시-작 🌟

  • 애플리케이션 = 공연, 각각의 인터페이스 = 배역(배우 역할)이라 생각하자!
  • 그런데! 실제 배역 맞는 배우를 선택하는 것은 누가 하는가?
  • (로미오와 줄리엣 공연을 하면 )로미오 역할을 누가 할지 줄리엣 역할을 누가 할지는, 배우들이 정하는게 아니다.
  • 이전 코드는 마치 로미오 역할(인터페이스)을 하는 레오나르도 디카프리오(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다.
  • 디카프리오는 공연도 해야하고 동시에 여자 주인공도 공연에 직접 초빙해야 하는 다양한 책임을 가지고 있다😂

💡관심사를 분리하자💡

  • 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다.
  • 디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야 한다!
  • 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 나와야 한다.
  • 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리하자❗️

애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 만들자!

✔️ hello.core/Appconfig.java 코드 작성

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 {
    // 어디서든 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

✔️ hello.core/member/MemberServiceImpl.java 코드 수정 (생성자 주입)

package hello.core.member;

public class MemberServiceImpl implements MemberService{
    // 가입을 하고 회원을 찾으려면 앞서 만든 MemberRepository 인터페이스가 필요함
    // private final MemberRepository memberRepository = new MemoryMemberRepository();

    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);
    }
}
  • 설계 변경으로 MemberServiceImplMemoryMemberRepository 를 의존하지 않는다!
  • 단지 MemberRepository 인터페이스만 의존한다.
  • MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될 지)는 알 수 없다.
  • MemberServiceImpl 의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부 (AppConfig)에서 결정된다.
  • MemberServiceImpl 은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.

❗️클래스 다이어그램❗️

  • 객체의 생성과 연결은 AppConfig 가 담당한다.
  • DIP 완성 : MemberServiceImplMemberRepository 인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다.
  • 관심사의 분리 : 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리되었다.

❗️회원 객체 인스턴스 다이어그램❗️

  • appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl 을 생성하면서 생성자로 전달한다.
  • 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) 우리말로 의존관계 주입 또는 의존성 주입이라 한다.

✔️ hello.core/order/OrderServiceImpl.java 코드 수정 (생성자 주입)

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

        // OrderService 입장에서 할인에 대한 건 잘 모름. 할인에 대한 건 discountPolicy 네가 알아서 해 줘!
        // 이것이 바로 단일 체계 원칙! 잘 설계된 것.
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
  • 설계 변경으로 OrderServiceImplFixDiscountPolicy 를 의존하지 않는다! 단지 DiscountPolicy 인터페이스만 의존한다.
  • OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없다.
  • OrderServiceImpl 의 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부(AppConfig)에서 결정한다.
  • OrderServiceImpl 은 이제부터 실행에만 집중하면 된다.
  • OrderServiceImpl 에는 MemoryMemberRepository , FixDiscountPolicy 객체의 의존관계가 주입된다.


🏷 AppConfig 실행

✔️hello.core/MemberApp.java 코드 수정

package hello.core;

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

public class MemberApp {

    public static void main(String[] args) {
        // AppConfig를 하나 만들어서 이를 통해 멤버 서비스를 생성
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        // new Member(1L, "memberA", Grade.VIP);
        // 이 상태에서 command+option+v 를 누르면 아래의 내용과 같이 자동 생성이 됨
        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());
    }
}

✔️hello.core/OrderApp.java 코드 수정

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class OrderApp {
    public static void main(String[] args) {
        // AppConfig를 사용해 멤버서비스와 오더서비스를 꺼내도록 수정
        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);
//        System.out.println("order.calculatePrice = " + order.calculatePrice());
    }
}

이제 테스트 코드도 수정해보자❗️

✔️test/../hello.core/member/MemberServiceTest.java 테스트 코드 수정

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 : 각 테스트 시작 전 무조건 실행이 되는 것
    @BeforeEach
    public void beforeEach() {
        // AppConfig를 만들고 멤버서비스 할당해주기
        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);
    }
}

✔️test/../hello.core/order/OrderServiceTest.java 테스트 코드 수정

package hello.core.order;

import hello.core.AppConfig;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

    MemberService memberService;
    OrderService orderService;

    // @BeforeEach : 각 테스트 시작 전 무조건 실행이 되는 것
    @BeforeEach
    public void beforeEach() {
        // AppConfig를 만들고 멤버서비스, 오더서비스 할당해주기
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
        orderService = appConfig.orderService();
    }

    @Test
    void createOrder() {
        // long을 사용해도 되지만, 이를 쓰면 null을 넣을 수 없음
        // (이후에 DB생성시 null을 사용할 수 있기 때문에 미리 고려).
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        // VIP인 경우 할인 금액인 1000원이 맞는지? 검증
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

⬆️ 테스트 실행 결과 👍🏻


✔️ 정리

  • AppConfig를 통해 관심사를 확실하게 분리했다.
  • (배역과 배우에 대입해서 생각해봤을 때) AppConfig는 공연 기획자 역할을 한다!
  • AppConfig는 구체 클래스를 직접 선택하고, 배역에 맞는 담당 배우를 캐스팅하고, 애플리케이션이 어떻게 동작할지 전체 구성을 책임진다!
  • 이제 선택된 배우들은 담당 기능을 실행하는 책임만 지면 된다.
  • OrderServiceImpl은 기능을 실행만 하면 된다.


🏷 AppConfig 리팩터링

아직.. 안 끝났나요....?

현재 AppConfig를 잘보면 중복도 있고 역할에 따른 구현이 잘 보이지 않는다.
리팩터링을 해 보자 😂...!

우리가 기대하는 그림

역할들이 드러나게 코드를 잘 작성하는 것이 중요하다!

✔️ AppConfig 리팩터링

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 리팩터링

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
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 {
    // 어디서든 AppConfig를 통해 멤버 서비스를 불러다 쓸 수 있음
    // 생성자 주입
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    // 생성자 주입
    public OrderService orderService() {
        // 구체적인 것을 여기서 선택하도록 함
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }

    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    private DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}

💡 이렇게 리팩토링을 하게 되면,
1) 중복도 사라지고(new MemoryMemberRepository()),
2) 메서드 명만 봐도 역할이 확실히 드러나고,
3) MemoryMemberRepository 를 다른 구현체로 변경할 때 한 부분만 변경하면 된다 !!



현기증이 나서...요.......😂

profile
Backend Developer

0개의 댓글