본문 바로가기
공부/Spring

[스프링 핵심](2) 회원, 주문과 할인 예제2 + IoC, DI, 그리고 컨테이너

by 다음에바꿔야지 2024. 1. 7.

정률 할인 정책을 추가 개발한다.

package hello.core.discount;

import hello.core.annotation.MainDiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.springframework.stereotype.Component;

public class RateDiscountPolicy implements DiscountPolicy {

    private final int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}

 

정률 할인에 대한 테스트 코드를 작성한다.

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

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

 

지금까지 구현한 코드를 보면, DIP와 OCP를 준수하지 않았다.

할인 정책을 변경하려면 클라이언트인 OrderServiceImpl의 코드를 고쳐야한다.

public class OrderServiceImpl implements OrderService {
 //    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
     private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
 }

인터페이스에 의존해야하지만, 구현 클래스에 의존하고 있다.

따라서, 인터페이스에만 의존하도록 설계를 변경해야한다.

 

인터페이스에만 의존하도록 코드 변경

public class OrderServiceImpl implements OrderService {
     //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
     private DiscountPolicy discountPolicy;
}

이렇게 변경하면 인터페이스가 정의되지 않아, NPE가 발생한다.

그렇다면, 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 할 필요가 있다.

 

AppConfig!!

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

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는 실제 동작에 필요한 구현 객체를 생성하고, 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)해준다.

 

MemberserviceImpl에 생성자를 만들어 주입할 수 있도록 수정한다.

package hello.core.member;
 public class MemberServiceImpl implements MemberService {
     private final MemberRepository memberRepository;
     public MemberServiceImpl(MemberRepository memberRepository) {
         this.memberRepository = memberRepository;
}
     public void join(Member member) {
         memberRepository.save(member);
}
     public Member findMember(Long memberId) {
         return memberRepository.findById(memberId);
} }

MemberServiceImpl은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중하면 된다.

 

OrderServiceImpl도 마찬가지로 수정해보자.

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);
        int discountPrice = discountPolicy.discount(member, itemPrice);
        return new Order(memberId, itemName, itemPrice, discountPrice);
        }
     }

 

테스트 코드에도 AppConfig를 추가해서 오류를 수정한다.

class MemberServiceTest {
     MemberService memberService;
     @BeforeEach
     public void beforeEach() {
         AppConfig appConfig = new AppConfig();
         memberService = appConfig.memberService();
     }
}

class OrderServiceTest {
     MemberService memberService;
     OrderService orderService;
     @BeforeEach
     public void beforeEach() {
         AppConfig appConfig = new AppConfig();
         memberService = appConfig.memberService();
         orderService = appConfig.orderService();
} }

여기까지 객체 지향의 5가지 원칙 중 3가지를 따르며 순수 자바 코드로 구현해보았다.

* SRP: 구현 객체를 생성하는 책임은 AppConfig가 맡고, 클라이언트 객체는 실행하는 책임만 담당하게 했다.

* DIP: AppConfig가 클라이언트 코드에 의존관계를 주입하게 해서 클라이언트 객체는 추상화에 의존하게 했다.

* OCP: 애플리케이션을 사용 영역과 구성 영역으로 나눠 새로운 확장에는 열려있고, 변경에는 닫혀있게 했다.

 

 

<IoC, DI, 그리고 컨테이너>

*IoC(Inversion of Control): 제어의 역전

- 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 IoC라고 한다.

- 프로그램의 제어 흐름에 대한 권한은 모두 AppConfig에 있기 때문에 IoC가 적용되었다.

 

*DI(Dependency Injection): 의존관계 주입

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

- DI를 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.

 

*IoC 컨테이너, DI 컨테이너

- AppConfig와 같이 객체를 생성, 관리, 의존 관계를 연결해 주는 것을 IoC 컨테이너, 또는 DI 컨테이너 라고 한다.(최근에는 의존 관계 주입에 초점을 맞추어 주로 DI 컨테이너라고 한다.)