스프링 핵심 원리 이해2 - 객체 지향 원리 적용

udonehn·2023년 1월 24일
0

새로운 할인 정책 개발

  • 기획자가 갑작스럽게 고정 금액 할인에서 정률 할인으로 기획을 바꾼 상황이다.
    • 객체지향 설계 원칙을 준수하였다면 요구사항을 쉽게 반영할 수 있다.

discount.RateDiscountPolicy

  • discount 패키지 아래 DiscountPolicy interface의 구현체 RateDiscountPolicy 클래스를 생성한다.

<코드>

test.java.hello.core.discount.RateDiscountPolicyTest

  • RateDiscountPolicy.java에서 discount 메소드 이름 위에 커서를 두고 IntelliJ 단축키 Ctrl+Shift+T를 누르면 테스트를 쉽게 생성할 수 있다.
  • Assertions를 입력하고 Alt+Enter를 이용하면 쉽게 static으로 import 할 수 있다.

<코드>

오류 코드

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
    void vip_x(){
        //given
        Member member = new Member(1L, "memberVIP", Grade.BASIC);
        int discount = discountPolicy.discount(member,10000);
        Assertions.assertThat(discount).isEqualTo(1000);
    }
  • VIP가 아니라면 할인이 적용되지 않아야 한다. 따라서 discount의 값은 0이 되어야 한다.
    • isEqualTo에 1000을 입력하였기 때문에 테스트 오류가 발생한다.
      • 기대값은 1000이었지만 0이 입력되었다.

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

새로 작성한 정률 할인 정책 코드를 적용한다.

  • order.OrderServiceImplFixDiscountPolicy()RateDiscountPolicy()로 변경한다.

문제점

OrderServiceImpl는 SOILD 원칙에 위반된다.
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

DIP (Dependency inversion principle)위반

  • 클라이언트 코드 OrderServiceImplDiscountpolicy 인터페이스 뿐만 아니라 FixDiscountPolicy 구현 클래스도 의존하고 있다.

OCP (Open/closed principle)위반

  • 할인 정책을 변경하기 위해서는 클라이언트 코드 OrderServiceImpl을 수정해야 한다.

해결 방법(X)

  1. 인터페이스에만 의존하도록 코드를 변경한다.
    • private DiscountPolicy discountPolicy로 코드를 변경한다.
  2. 하지만 구현체 없이 인터페이스로만 실행은 불가능하다.
  3. 문제를 해결하기 위해 누군가 클라이언트 OrderServiceImplDiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 한다.

관심사의 분리

  • 로미오와 줄리엣 공연에서 로미오 역할(인터페이스)를 하는 디카프리오(구현체)는 본인의 역할인 배역을 수행하는 것에 집중해야 한다.
  • 줄리엣 역할(인터페이스)를 하는 여자 주인공(구현체)의 섭외는 별도의 공연 기획자(AppConfig)가 담당한다.
  • 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 구분해야 한다.

AppConfig

  • hello.core 밑에 AppConfig 클래스를 생성한다.
  • 애플리케이션의 전체 동작 방식을 구성(config)하기 위해 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 생성한다.

<코드>

MemberServiceImpl

  • MemberServiceImpl를 다음과 같이 수정한다.
    • 생성자 주입 : MemoryMemberRepository() 구현 객체를 직접 만들지 않고 생성자를 통해 만든다.
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository
    }
  • 이제 MemberServiceImplMemoryMemberRepository는 존재하지 않는다. (의존하지 않는다)
    • MemberRepository 인터페이스만 존재한다.
      • 이는 추상화에만 의존하는 것으로, DIP를 준수하고 있다.
  • 어떤

OrderServiceImpl

  • OrderServiceImpl 또한 MemberServiceImpl와 같은 형식으로 수정한다.
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;
    }

다이어그램

Test 코드

public class MemberServiceTest {

    MemberService memberService;

    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }
  • 지금까지 생성한 Test 코드에도 AppConfig를 적용한다.
  • @BeforeEach 는 각 @Test가 실행될 때마다 실행 전에 호출되는 애노테이션이다.

AppConfig 리팩토링

  • 코드에 역할이 잘 드러나도록 AppConfig를 리팩터링한다.
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 FixDiscountPolicy();
    }
}
  • 메소드 이름으로 역할을 잘 알 수 있다.

새로운 구조와 할인 정책 적용

  • AppConfig가 있기에 FixDiscountPolicy에서 RateDiscountPolicy로 쉽게 변경할 수 있다.
    • 사용 영역의 코드 수정 없이 구성 영역의 코드 수정만으로 가능하다.
    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
  • 클라이언트 코드 (OrderServiceImpl 등)의 코드는 변경할 필요가 없다.
    • OCP와 DIP를 모두 만족하는 코드가 완성되었다.

전체 흐름 정리

<생략>

좋은 객체 지향 설계의 5가지 원칙의 적용

SRP

  • 단일 책임 원칙 : 한 클래스는 하나의 책임만 가져야 한다.
    • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당한다.
    • 클라이언트 객체는 실행하는 책임만 담당한다.

DIP

  • 의존관계 역전 원칙 : 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안 된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
    • AppConfig가 FixDiscountPolicy 객체 인스턴트를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입했다.

OCP

  • 개방-폐쇄 원칙 : 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
    • AppConfig에서 의존관계를 FixDiscountPolicy 에서 RateDiscountPolicy로 변경하여 클라이언트에 주입하므로 클라이언트 코드의 변경은 필요하지 않다.
      • 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다.

IoC, DI, 그리고 컨테이너

제어의 역전 IoC(Invesion of Control)

  • 프로그램의 제어를 직접 하지 않고 프레임워크와 같이 외부에서 관리하는 것을 뜻한다.
    • 프로그램의 제어 흐름은 모두 AppConfig가 가지고 있다.
    • OrderServiceImpl은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될 지 모른다.

프레임워크 VS 라이브러리

  • 프레임워크는 작성한 코드를 제어하고, 대신 실행한다(제어의 역전), ex)UJnit
  • 라이브러리는 단순 활용가능한 도구들의 집합으로, 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 라이브러리이다.

의존관계 주입 DI(Dependency Injection)

  • OrderServiceImplDiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
    • 정적인 클래스 의존관계와, 실행 시점에 결정되는 동적인 객체(인스턴트) 의존 관계를 분리하여 생각해야 한다.

클래스 다이어그램

  • 정적인 클래스 의존관계는 import 코드만 보고 쉽게 파악할 수 있다.
    • OrderServiceImplMemberRepositoy, DiscountPolicy에 의존한다는 것을 알 수있다.
      • 하지만 실행 시 실제로 어떤 객체가 주입될지는 알 수 없다.

객체 다이어그램

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.

컨테이너

  • AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라고 한다.

스프링으로 전환하기

AppConfig 수정

  • 클래스 위에 @configuration을 추가하고, 객 메소드 위에 @Bean을 추가한다.

<코드>

OrderApp 수정

기존 객체 생성 코드는 주석처리하고, 새로운 코드를 추가한다.

  • MemberApp도 같은 방식으로 수정한다.
//AppConfig appConfig = new AppConfig();
//MemberService memberService = appConfig.memberService();
//OrderService orderService = appConfig.orderService();

ApplicationContext applicationContext =  new AnnotationConfigApplicationContext(AppConfig.class);
MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
  • ApplicationContext를 스프링 컨테이너라 한다
  • 스프린 컨테이너는 @Bean이 적힌 메서드를 모두 호출해서 스프링 컨테이너의 등록한다. 빈의 이름은 메서드명으로 한다.

본 포스팅은 김영한 강사의 스프링 핵심 원리 강의를 수강하고 요약한 것으로, 해당 강의의 영상 및 강의자료를 참고하였습니다.

profile
안녕하세요. 만나서 반갑습니다.

0개의 댓글

관련 채용 정보