본문 바로가기

Spring Introduction/스프링 핵심 원리 - 기본

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

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 핵심 원리 - 기본편에 대해 공부하고, 정리한 내용입니다.

1. 들어가며

 지난 게시물에서는 스프링 프레임워크의 탄생 배경과 그 특징, 그리고 스프링이 어떻게 자바의 대표적인 프레임워크로 자리매김하게 되었는지를 정리하였습니다. 

 

01. 객체 지향 설계와 스프링 - (1) 자바 진영의 추운 겨울과 스프링의 탄생 (이야기)

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 핵심 원리 - 기본편에 대해 공부하고, 정리한 내용입니다. 1) 자바 생태계의 한겨울 2000년대 초기 자바 표준 모델로서의 EJB(Enterprise Java Bea

soo99.tistory.com

 

 또한, 객체 지향 프로그래밍의 핵심 원칙인 SOLID 중에서도 특히 개방-폐쇄 원칙(OCP)의존 관계 역전 원칙(DIP)이 왜 제대로 구현되지 못하는지에 대해 알아보았습니다.

 

스프링 핵심 원리 이해 1 - 순수 자바 코드로 예제 만들기

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 핵심 원리 - 기본편에 대해 공부하고, 정리한 내용입니다. 1. 스프링 프로젝트 생성 및 설정 1) 필요한 설치 항목 Java 11 IntelliJ 2) 프로젝트

soo99.tistory.com

 

이를 위해 스프링 없이 회원-주문 시스템의 예제 코드를 통해 요구사항 분석부터 구현까지의 과정을 진행해 보았고, OCP와 DIP 원칙을 아직 완전히 지키지 못한다는 점을 확인하였습니다. 이를 통해, 단순히 원칙을 이해하는 것 이상으로 실제 코드에 어떻게 적용하는지, 그리고 그로 인해 어떤 문제점이 발생하는지를 확인하였습니다.

 

이번 게시물에서는 이러한 문제점들을 어떻게 해결할 수 있을지, 그리고 객체지향 원리를 어떻게 적용할 수 있는지에 대해 정리하고자 합니다. 이를 통해 스프링과 객체지향 프로그래밍 원칙 사이의 연결고리를 더욱 명확하게 이해하게 될 것입니다.


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

1) 기존의 할인 정책

 처음에 우리가 개발한 회원-주문 애플리케이션에서는, 할인 정책은 주로 VIP 회원에게만 1,000원의 고정 할인을 적용하는 방식이었습니다. 이를 구현하기 위해 `FixDiscountPolicy`라는 구현체를 작성하고 이를 프로그램에 적용시켰습니다. 이 구현체는 VIP 회원에게만 1,000원의 할인을 제공하는 역할을 수행하였습니다.


2) 할인 정책의 변경 요구

 하지만 시간이 지나면서 기획자의 요구가 변화하였습니다. 이제 기획자는 고정 금액 할인이 아닌, 주문 금액의 10%를 할인해주는 방식으로 할인 정책을 변경하는 요구를 제시하였습니다. 이런 요구사항의 변경이 제품 출시 직전에 발생한다면, 기존의 코드를 크게 수정해야 하므로 개발 과정에 큰 어려움을 겪을 수 있습니다.


3) 객체지향적 접근

 그러나 우리는 이미 객체지향 설계 원칙을 준수하여 `DiscountPolicy`라는 역할과 그 구현체인 `FixDiscountPolicy`를 분리해두었습니다. 이렇게 역할과 구현을 분리해두면, 구현체가 변경되더라도 해당 역할만 제대로 수행한다면 코드에는 문제가 발생하지 않습니다. 이는 객체지향적 접근법의 핵심 원칙 중 하나인 '개방-폐쇄 원칙'을 잘 보여줍니다.

할인 도메인 클래스 다이어그램

 


4) 새로운 할인 정책의 구현

 따라서 우리는 새로운 할인 정책인 `RateDiscountPolicy`를 쉽게 구현할 수 있었습니다. 이 구현체는 VIP 회원에게 주문 금액의 10%를 할인해주고, 일반 회원에게는 할인을 적용하지 않는 역할을 수행합니다.

public class RateDiscountPolicy implements DiscountPolicy {

    private int discountPercent = 10;

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

 


5) 새로운 할인 정책의 테스트

 이제 이 새로운 할인 정책을 테스트해보겠습니다. 테스트를 통해 VIP 회원에 대해서는 10%의 할인이 정상적으로 적용되며, 일반 회원에 대해서는 할인이 적용되지 않는 것을 확인할 수 있습니다.

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.Nested;
import org.junit.jupiter.api.Test;

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

@DisplayName("RateDiscountPolicy 클래스의 ")
class RateDiscountPolicyTest {

    private RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Nested
    @DisplayName("discount 메서드는")
    class Describe_discount{
        @Nested
        @DisplayName("VIP 회원정보와 10000원을 전달하면")
        class Context_with_vip_and_10000 {
            @DisplayName("10% 할인금액을 반환한다.")
            @Test
            void it_is_return_1000() {
                //given
                Member vip = new Member(1L, "VIP_MEMBER", Grade.VIP);

                //when
                int discount = discountPolicy.discount(vip, 10000);

                //then
                assertThat(discount).isEqualTo(1000);
            }
        }

        @Nested
        @DisplayName("BASIC 회원정보와 10000원을 전달하면")
        class Context_with_basic_and_10000 {
            @DisplayName("0원을 반환한다.")
            @Test
            void it_is_return_zero() {
                //given
                Member basic = new Member(2L, "BASIC_MEMBER", Grade.BASIC);

                //when
                int discount = discountPolicy.discount(basic, 10000);

                //then
                assertThat(discount).isEqualTo(0);
            }
        }
    }
}
참고: Windows는 Ctrl + Shift + T 단축키를 통해 테스트 클래스를 생성할 수 있습니다.

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

1) 새로운 할인 정책의 교체 적용

 새로운 할인 정책인 `RateDiscountPolicy`를 적용하려고, `OrderServiceImpl`이라는 주문 서비스에서 기존 할인 정책인 `FixDiscountPolicy`를 새로운 할인 정책인 `RateDiscountPolicy`교체하여 코드 수정을 진행하였습니다. 

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    ...
}

이렇게 변경을 하면 새로운 할인 정책은 정상적으로 동작합니다. 그러나, 예상치 못한 문제가 발생했습니다. 


2) 새로운 할인 정책 적용의 문제점

 소프트웨어를 개발할 때에는 변화에 유연하게 대응할 수 있도록 설계 원칙을 준수하는 것이 중요한데, 현재의 개발 방식은 객체지향 설계 원칙인 OCP(Open-Closed Principle)와 DIP(Dependency Inversion Principle)를 위반하게 됩니다.

 

  • DIP 위반:  `OrderServiceImpl`는 `DiscountPolicy`라는 인터페이스에 의존하고 있으면서, 동시에 `FixDiscountPolicy``RateDiscountPolicy`라는 구체적인 클래스에도 의존하고 있습니다. 즉, `OrderServiceImpl`이 추상화된 인터페이스와 그 구체적인 구현체 모두에 의존하게 되는 상황이 발생하는데, 이 원칙에 따르면, 계획에만 의존하고 구체적인 방법에는 의존하면 안됩니다. 이는 DIP의 기본 원칙을 위반하는 것입니다. 
    • 추상(인터페이스) 의존 : DiscountPolicy
    • 구체(구현) 클래스 : FixDiscountPolicy, RateDiscountPolicy


  • OCP 위반: 할인 정책을 교체하려고 진행할 때, `OrderServiceImpl`이라는 코드도 함께 변경해야 합니다. 하지만 객체지향 설계 원칙에 따르면, 기능을 추가하거나 변경할 때 기존 코드는 그대로 유지하고, 새로운 코드만을 추가해야 합니다.  그렇기 때문에 이는 OCP의 기본 원칙을 위반하는 것 입니다.


3) 해결방안

 이 문제를 해결하려면, `OrderServiceImpl`이 `DiscountPolicy` 인터페이스에만 의존하도록 변경해야 합니다.

즉, 우리는 인터페이스에만 의존하도록 의존관계를 변경해야 합니다. `OrderServiceImpl`는 할인 정책에 대한 계획만 알고있어야 하며, 그 계획을 실제로 어떻게 실행하는지는 알 필요가 없습니다.

 

아래와 같이 OrderServiceImpl의 코드를 수정하여, DiscountPolicy의 구체적인 클래스에 대한 의존성을 제거해야 합니다.

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy;
    ...
}

 

 

 그러나, 이렇게 코드를 수정하면 `DiscountPolicy`의 구현 객체가 없어서 `NullPointerException`이 발생하게 됩니다.

 

 이 문제를 해결하기 위해서는 누군가가 `OrderServiceImpl` 외부에서 `DiscountPolicy`의 구현 객체를 생성하고 주입해주어야 합니다. 이렇게 하면 `OrderServiceImpl`은 `DiscountPolicy` 인터페이스에만 의존하게 되어 DIP를 준수하게 됩니다.


4. 관심사의 분리 

1) 문제 상황: OCP, DIP 원칙의 위배

 지금까지 회원-주문 시스템의 설계와 구현, 그리고 테스트 케이스 작성을 통해 진행하였습니다. 그러나, 개방-폐쇄 원칙(OCP)의존성 역전 원칙(DIP)을 준수하지 못한 문제점이 있습니다.

 

 이 원칙들이 지켜지지 않는 주된 이유는, 모든 구현체가 다른 역할의 구현체를 사용하려고 생성자를 호출하고 의존성이 생기기 때문입니다. 변경이 필요할 경우 코드까지 변경해야 하는 번거로움이 있습니다. 구현체를 대입하지 않으면 NullPointerException이 발생할 수도 있습니다. 각 코드가 다른 역할의 구현체를 알 필요가 없습니다. 이는 DIP 원칙에 어긋납니다. 

 

2) 문제 해결: 관심사의 분리

 이 문제를 해결하기 위해, 각각의 코드는 원래 자신의 역할에만 집중해야 합니다. 이는 DIP 원칙에 따른 것으로, 연극에 비유하자면 남자 주인공이 자신의 배역과 역할만 연기하고, 여자 배우의 초빙이나 소품 준비 등에는 관여하지 않는 것과 같습니다. 이러한 과도한 책임은 오히려 코드의 복잡성을 증가시킵니다.

 

 따라서, 우리는 관심사를 분리해야 합니다. 이는 배우가 자신의 연기에만 집중하고, 공연 구성이나 담당 배우 영입, 배역 지정 등의 책임은 공연 기획자가 맡는 것과 같습니다. 각각이 자신의 역할만 수행하도록 함으로써 책임을 분리하고 관심사를 분리해야 합니다.


5. AppConfig

 위에서 배우의 과중한 책임을 내려놓게하기 위해 해당 책임들을 공연기획자가 담당하도록 한다고 했습니다.

 이처럼 회원 - 서비스 프로젝트에서는 OCP, DIP원칙을 위배하게하는 각각의 구현체에서 다른 역할의 구현체를 의존하는 부분들을 없애고 이렇게 구현체를 생성 및 연결하는 책임을 전담하는 별도의 설정 클래스AppConfig를 만들고자 합니다.

public class AppConfig {

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

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository()
																		,new FixDiscountPolicy());
    }
}

 

 위 코드에서 볼 수 있듯이, AppConfig는 구현체를 생성하고 연결하는 역할을 합니다. 예를 들어, MemberService와 OrderService의 구현체를 생성하고, 필요한 종속성을 제공합니다. 이렇게 함으로써 각 구현체는 자신의 주요 역할에 집중할 수 있게 되며, 의존성 관리는 AppConfig가 담당하게 됩니다.

 

 즉, AppConfig애플리케이션의 실제 동작에 필요한 구현체를 생성하고 연결해주는 역할을 담당합니다. 이를 통해 각 구현체는 자신의 역할에 집중하고, 의존성 관리는 AppConfig가 수행하게 됨으로써, OCP DIP 원칙을 준수하게 됩니다.

 

 이제, 모든 구현체에서 직접적으로 객체를 생성하는 대신, 생성자를 통해 외부(`AppConfig`)에서 객체를 주입받을 수 있도록 코드를 변경하는 과정을 진행하려 합니다. 이러한 변경을 위한 첫 단계로, `MemberServiceImpl` 클래스를 살펴보면, 해당 클래스는 다음과 같은 형태로 작성되어 있습니다.

public class MemberServiceImpl implements MemberService{
    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);
    }
}


위의 `MemberServiceImpl` 클래스는 `MemberRepository`를 생성자 인자로 받아서 내부 변수에 저장하고 있습니다. 이는 '의존성 주입(Dependency Injection)'이라는 개념을 통해 이루어집니다. 즉, `MemberServiceImpl` 클래스가 `MemberRepository`에 의존하고 있음을 명확하게 보여주는 것입니다. 

다음으로, `OrderServiceImpl` 클래스가 이 '의존성 주입(Dependency Injection)' 개념을 어떻게 사용하고 있는지 살펴보겠습니다.

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 discount = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discount);
    }
}


`OrderServiceImpl` 클래스는 `MemberRepository`와 `DiscountPolicy`를 생성자 인자로 받아서 내부 변수에 저장하고 있습니다. 이 클래스 역시 '의존성 주입(Dependency Injection)'이라는 개념을 적용하고 있음을 알 수 있습니다.

마지막으로, 테스트 클래스인 `OrderServiceTest`를 살펴보면 다음과 같습니다.

@DisplayName("OderService 클래스의")
class OrderServiceTest {
    private Member basicMember;
    private Member vipMember;

    private OrderService orderService;
    private MemberService memberService;

    @BeforeEach
    void setup() {
        AppConfig appConfig = new AppConfig();

        orderService = appConfig.orderService();
        memberService = appConfig.memberService();
		...
}

 `OrderServiceTest` 클래스에서는 `AppConfig` 객체를 생성하고, 이 객체의 메서드를 사용하여 `OrderService`와 `MemberService` 객체를 가져옵니다. 이렇게, 서비스 객체들은 외부(`AppConfig`)에서 의존성이 주입되며, 이를 통해 각 서비스 클래스는 본인의 로직 실행에만 집중하게 됩니다. 어떤 구현체가 주입될지는 외부에서 결정되게 됩니다.

 이처럼, 의존성 주입을 통해 각 클래스는 자신의 주요 역할에 집중할 수 있으며, 어떤 구현체가 사용될지에 대한 결정은 외부에 맡기게 됩니다. 이렇게 하면, 코드의 유연성과 확장성이 향상되며, 유지보수에도 용이해집니다.

 

※ 클래스 다이어그램

  • 클래스 다이어그램에서 보면, 객체의 생성과 연결은 `AppConfig` 클래스가 담당하고 있습니다.
  • 이렇게 구현체인 `MemberServiceImpl`은 `MemberRepository` 인터페이스에만 의존하게 되고, 실제 구현체에 대한 정보는 필요 없게 됩니다. 이로써, 우리는 DIP(Dependency Inversion Principle) 원칙을 지키게 되었습니다. 이렇게 객체를 생성하고 연결하는 역할과 실제로 실행하는 역할이 분리되었습니다.
  • 여기서 스프링에서 자주 들어본 키워드인 DI(Dependency Injection)가 등장합니다.
  • `AppConfig`는 필요에 따라 구현체들의 객체를 생성한 후, 해당 객체의 참조값을 생성자로 전달하여 만들어진 객체를 반환합니다.
  • 이 과정에서, 구현체의 시점에서 바라보면, 자신이 필요로 하는 다른 역할들에 대한 구현체들을 `AppConfig`라는 외부에서 주입받는 것처럼 보입니다. 이러한 과정을 우리는 '의존관계 주입' 혹은 '의존성 주입'이라고 부릅니다.

 

=> 요약하면, `AppConfig`를 통해 구현체들은 자신이 필요로 하는 다른 구현체들을 외부에서 주입받게 되고, 이를 통해 각 클래스는 자신의 역할에만 집중할 수 있게 되어 코드의 유연성이 향상됩니다.

※  우리가 `AppConfig`를 통해 관심사를 분리하는 방법을 정리해보겠습니다.

1. `AppConfig`는 설정 클래스로서의 역할을 수행합니다. 이 클래스는 구체 클래스를 선택하고, 각 역할에 맞는 구현체를 선택하여 주입하는 책임을 가지고 있습니다.

 

2. 각 구현체들은 자신의 책임에만 집중하면 됩니다. 즉, 각 구현체는 자신의 역할을 수행하는 것에 집중하며, 어떤 구현체가 주입될지에 대한 결정은 `AppConfig`가 맡게 됩니다. 이렇게 `AppConfig`를 통해 관심사를 분리하면, 각 클래스는 자신의 역할에만 집중할 수 있게 되어 코드의 유연성과 가독성이 향상됩니다.


6. AppConfig 리팩터링

위에서 `AppConfig` 클래스를 통해 서비스 로직의 관심사를 성공적으로 분리했습니다. 하지만, `AppConfig` 설정 정보 클래스를 살펴보면, 코드 중복이 발생하고 있으며, 코드의 의도와 역할이 명확하게 드러나지 않습니다.

 

이를 해결하기 위해 중복된 코드를 모듈화하고, 이로써 의도를 좀 더 명확히 드러내보겠습니다.

아래는 중복 코드를 제거한 이후의 `AppConfig` 클래스입니다:

public class AppConfig {

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

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

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

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

 

이제 `MemberRepository`를 변경하거나 할인 정책(`DiscountPolicy`)을 변경하려면 해당 라인만 수정하면 됩니다. 이로써 코드의 가독성이 향상되었습니다.

 

 이제 이렇게 최적화된 코드의 효용을 확인하기 위해 새로운 요구사항을 구현해보겠습니다. 기존 요구사항 중에서 변경 가능성이 있었던 할인 정책을, 기존의 정액 할인에서 비율 할인으로 변경해보겠습니다.

 

 현재 `AppConfig`를 통해 애플리케이션은 사용 영역과 구성 영역으로 나뉘어져 있습니다. 그렇기 때문에 객체를 생성하고 주입해주는 구성 영역에서 할인 정책만 변경해서 주입해주면, 사용 영역을 변경할 필요 없이 할인 정책을 변경할 수 있습니다.

 

 `FixDiscountPolicy`에서 `RateDiscountPolicy`로 변경해도, `AppConfig`(구성 영역)에서 생성하는 것만 변경하면 됩니다. 아래는 `AppConfig` 클래스에서 할인 정책을 변경한 코드입니다:

public class AppConfig {

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

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

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

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

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

 위에서 좋은 객체 지향 설계의 5가지 원칙 중 3가지 원칙인 SRP, DIP, OCP를 적용하였습니다.

1) SRP(단일 책임 원칙): 한 클래스는 하나의 책임만 가져야 합니다. 처음 회원-주문 예제에서는 클라이언트 객체가 구현 객체를 직접 생성, 연결, 실행하는 과도한 책임을 가지고 있었습니다. 하지만 SRP 원칙을 따라 관심사를 분리하여, 구현 객체를 생성하고 연결하는 책임을 `AppConfig`가 담당하도록 변경하였습니다. 결과적으로 클라이언트 객체는 로직을 수행하는 책임만 가지게 되었습니다.

 

2) DIP(의존관계 역전 원칙): 추상화에 의존해야 하며, 구체화에 의존하면 안 됩니다. `AppConfig`가 등장하기 전까지는 각 구현체가 필요한 다른 역할의 구현체를 직접 참조하였습니다. 이로 인해 DIP 원칙이 지켜지지 않았습니다. 하지만 `AppConfig`를 통해 할인 정책이나 회원 조회 레파지토리를 생성자를 통해 주입함으로써, 클라이언트 객체는 다른 구현체를 알 필요가 없어졌습니다.

 

3) OCP(개방-폐쇄 원칙): 소프트웨어 요소는 확장에는 열려 있지만 변경에는 닫혀 있어야 합니다. 다형성을 사용하여 클라이언트가 DIP를 지키고, 애플리케이션을 사용 영역과 구성 영역으로 나누었습니다. 할인 정책 변경이나 리포지토리를 인 메모리에서 DB로 변경하더라도, `AppConfig`에서 의존 관계 주입을 변경해주기만 하면 사용 영역의 클라이언트 코드를 수정하지 않아도 됩니다. 이로써 소프트웨어 요소를 새롭게 확장하거나 변경해도 사용 영역의 변경은 닫혀있게 되었습니다.


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

 이제 IoC, DI 그리고 컨테이너에 대해 알아보겠습니다.

1) IoC(Inversion of Control, 제어의 역전): 초기에는 구현체에서 필요한 다른 구현체를 직접 생성, 연결, 사용했습니다. 하지만 `AppConfig`라는 구성 영역이 생기면서 각각의 구현체들은 필요한 구현체를 직접 생성하는 것이 아니라 주입받아서 자신의 책임만 다하게 되었습니다. 이처럼 제어의 흐름을 외부에서 관리하는 것을 IoC라고 합니다.

 

2) 프레임워크 vs 라이브러리: 프레임워크는 내가 작성한 코드를 제어하고 대신 실행합니다. 반면에 라이브러리는 내가 작성한 코드가 직접 제어의 흐름을 담당합니다.

 

3) DI(Dependency Injection, 의존관계 주입): `OrderServiceImpl`은 `DiscountPolicy` 인터페이스에 의존하며 실제 구현체가 무엇인지는 모릅니다. 이러한 의존관계는 정적인 클래스 의존관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계로 나뉩니다.

 

4) 정적인 클래스 의존관계: 클래스가 사용하는 import 코드만 보고 의존관계를 판단할 수 있습니다. 이는 애플리케이션을 실행하지 않아도 분석할 수 있습니다.

 

5) 동적인 객체 인스턴스 의존 관계: 애플리케이션 실행 시점에 생성된 인스턴스의 참조값이 연결된 의존관계입니다.

런타임 시점에 외부에서 실제 구현체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관계 주입이라 합니다.

 

 이런 방식으로, 클라이언트 코드를 수정하지 않고도 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있습니다. 이처럼, `AppConfig`와 같이 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 혹은 DI 컨테이너라 합니다. 의존관계 주입에 초점을 맞춰 최근에는 DI 컨테이너라 부릅니다. (또는 어셈블러, 객체 팩토리 등으로 불리기도 합니다.)


8. 스프링으로 전환하기

 이제 우리는 DI 컨테이너인 `AppConfig`를 스프링 기반으로 전환해보겠습니다.

 

1) AppConfig 수정

@Configuration
public class AppConfig {

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

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

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}

 

`@Configuration`은 `AppConfig`가 설정을 구성한다는 것을 나타내는 어노테이션입니다.

`@Bean`은 해당 메서드를 스프링 컨테이너에 빈으로 등록합니다.

2) OrderServiceTest 수정

@DisplayName("OderService 클래스의")
class OrderServiceTest {
    ...
    private OrderService orderService;
    private MemberService memberService;

    @BeforeEach
    void setup() {
        /*AppConfig appConfig = new AppConfig();

        orderService = appConfig.orderService();
        memberService = appConfig.memberService();*/
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        orderService = applicationContext.getBean("orderService", OrderService.class);
        memberService = applicationContext.getBean("memberService", MemberService.class);
    }

    ...
}

 

 여기서 `ApplicationContext`는 스프링 컨테이너를 나타냅니다. 기존에는 `AppConfig`를 사용해 직접 객체 생성 및 의존관계 주입을 했지만, 이제는 스프링 컨테이너를 통해 사용합니다.

 

 스프링 컨테이너는 `@Configuration`이 붙은 `AppConfig`를 설정 정보로 사용합니다. 이 때 `@Bean`이 붙은 메서드를 호출해 반환된 객체를 스프링 컨테이너에 등록합니다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 합니다.

 

 스프링 빈의 이름은 관례적으로 메서드 이름을 사용합니다. 만약 이름을 변경하고 싶다면, `@Bean` 어노테이션의 `name` 속성을 사용하면 됩니다.

 

`ApplicationContext`의 `getBean()` 메서드를 통해 스프링 빈을 찾을 수 있습니다. 기존에는 개발자가 직접 객체 생성 및 의존관계 주입을 했지만, 이제는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아 사용하도록 변경하였습니다.