본문 바로가기

Spring

[인프런 강의 복습] 스프링 핵심 원리 - 객체 지향 원리 적용

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

1) 새로운 할인 정책 추가
기존에 구현된 고정 할인 정책에서 정률 할인 정책(주문한 금액의 %를 할인)을 새롭게 추가

2) 문제점
- 할인 정책을 변경 시, 클라이언트(OrderServiceImpl)에서 코드 변경이 이루어진다.
- OCP, DIP 객체 지향 설계 원칙을 준수하지 못함


3) 해결안
- DIP를 위반하지 않도록 인터페이스에만 의존하도록 의존관계를 변경
- 클라이언트에 구현 객체를 대신 생성하고 주입해줄 수 있는 클래스를 생성한다. (AppConfig Class의 등장)

 

■ 클라이언트 코드 변경

import com.example.demo.member.Member;
import com.example.demo.member.MemberRepository;
import com.sun.org.apache.xpath.internal.operations.Or;

public class OrderServiceImpl implements OrderService{

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    
    //memberRepository 객체와 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);
    }
}

■ AppConfig.java 

import com.example.demo.member.MemberRepository;
import com.example.demo.member.MemberService;
import com.example.demo.member.MemberServiceImpl;
import com.example.demo.member.MemoryMemberRepository;
import com.example.demo.order.DiscountPolicy;
import com.example.demo.order.OrderService;
import com.example.demo.order.OrderServiceImpl;
import com.example.demo.order.RateDiscountPolicy;

public class AppConfig {

    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

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

    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

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

}

■ OrderApp.java

import com.example.demo.member.Grade;
import com.example.demo.member.Member;
import com.example.demo.member.MemberService;
import com.example.demo.order.Order;
import com.example.demo.order.OrderService;

public class OrderApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(member.getMemberId(), "itemA", 20000);
        System.out.println("할인 가격 = " + order.calculate());
    }
}

■ OrderService Test

import com.example.demo.App.AppConfig;
import com.example.demo.member.Grade;
import com.example.demo.member.Member;
import com.example.demo.member.MemberService;
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 {

    private MemberService memberService;
    private OrderService orderService;

    
    //@BeforeEach는 테스트를 실행하기 전에 호출
    @BeforeEach
    public void beforeEach(){
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
        orderService = appConfig.orderService();
    }

    @Test
    void createOrder(){
        //given
        Member member = new Member(1L, "memberA", Grade.VIP);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then
        Order order = orderService.createOrder(findMember.getMemberId(), "itemA", 20000);
        int price = order.calculate();

        Assertions.assertThat(price).isEqualTo(18000);
    }

}
AppConfig
 - AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성
 - AppConfig는 생성한 객체 인스턴스의 참조값을 생성자를 통해 주입(연결) 
 - AppConfig는 애플리케이션의 구성 역할을 담당

OrderServiceImpl
 -
OrderServiceImpl은 DiscountPolicy 인터페이스에만 의존
 - OrderServiceImpl은 생성자를 통해 어떤 구현 객체가 주입될 지 알 수 없다. 어떤 구현 객체를 주입할지는 AppConfig에서 결정
 - OrderServiceImpl은 실행에만 집중

 

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

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

DIP 의존관계 역전 원칙 : 추상화에 의존해야지, 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나이다.
- AppConfig가 RateDiscountPolicy 객체 인스턴스를 생성해서 클라이언트에 의존관계를 주입, DIP원칙을 지킴

OCP : 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
- 다형성을 사용하며, 클라이언트가 DIP를 지킴 
- 애플리케이션이 사용 영역가 구성 영역으로 나뉨 
- AppConfig가 의존관계를 FixedDiscountPolicy에서 RateDiscountPolicy로 변경해서 클라이언트에 주입하므로 클라이언트 코드는 변경할 필요가 없다. 

 

■ 제어의 역전, IoC, Inversion of Control

제어의 역전, IoC, Inversion of Control
- 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성, 연결, 실행하였다. 
- AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당 
- 프로그램에 대한 제어 흐름 권한은 AppConfig가 가져가게 되었다. 
- 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)라고 한다.

프레임워크 VS 라이브러리
- 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 라이브러리 (ex : JsonObject 라이브러리)
- 내가 작정한 코드를 제어하고 대신 실행한다면 그것은 프레임워크(ex : JUnit 프레임워크) 

 

■ 의존관계 주입, DI, Dependency Injection

의존관계 주입, DI, Dependency Injection
OrderServiceImpl은 DiscountPolicy 인터페이스에 의존, 실제 어떤 구현 객체가 사용될지는 알지 못한다.
- 의존관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 이 둘을 분리해서 생각해야 한다.

[정적인 클래스 의존관계]
- 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석이 가능
- OrderServiceImpl은 MemberRepository, DiscountPolicy 인터페이스에 의존한다는 것을 알 수 있다. 하지만 이러한 클래스 의존관계만으로는 실제 어떤 객체가 OrderServiceImpl에 주입되었는지는 알 수 없다.

[동적인 클래스 의존관계]
- 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존 관계가 연결 되는 것을 의존관계 주입(DI)이라고 한다. 
- 객체 인스턴스를 생성하고, 그 참조값을 (생성자를 통해) 전달해서 연결하게 된다. 
- 의존관계 주입을 사용하면, 클라이언트 코드는 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다. 
- 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

 

■ DI 컨테이너, IoC 컨테이너

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

 

[출처 : 인프런 스프링 핵심 원리 기본편]