스프링 핵심 원리 (기본편) 강의 정리 - 3 객체 지향 적용

2023. 9. 19. 23:29Spring/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 리팩토링


기존 코드

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 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
  • 의존 관계는 정적인 클래스 의존관계와, 실행 시점에 결정되는 동적인 객체 의존관계 둘을 분리해서 생각해야 한다.
  • 애플리케이션 런타임에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.