Lined Notebook

[Spring MVC - 핵심 원리 기본] 03. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용

by ymkim

01. 새로운 할인 정책 개발

새로운 할인 정책을 확장 해보자
악덕 기획자 : 서비스 오픈 직전에 할인 정책을 지금처럼 고정 금액 할인이 아니라 좀 더 합리적인 주문 금액당 할인하는 정률% 할인으로 변경하고 싶어요. 예를 들어서 기존 정책은 VIP가 10000원을 주문하든 20000원을 주문하든 항상 1000원을 할인했는데, 이번에 새로 나온 정책은 10%로 지정해두면 고객이 10000원을 주문시 1000원을 할인해주고, 20000원 주문시에 2000원을 할인해주는 거에요!
  • 순진 개발자 : 제가 처음부터 고정 금액 할인은 아니라고 했잖아요.
  • 악덕 기획자 : 애자일 소프트웨어 개발 선언 몰라요?? 계획을 따르기보다 변화에 대응하기를
  • 순진 개발자 : (하지만 난 유연한 설계가 가능하도록 객체지향 설계 원칙을 준수했지 후후)

순진 개발자가 정말 객체지향 설계 원칙을 잘 준수하였는지 확인 해보자. 핵심은 고정 할인율이 아닌, 상품 가격에 맞는 유동 할인율 계산 요청.

01-1. RateDiscountPolicy 추가

  • 이번에는 주문 금액에 10%의 할인율 적용해달라는 요구사항이 들어왔다

RateDiscountPolicy 클래스 생성

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

public class RateDiscountPolicy implements DiscountPolicy {

    private final int discountPercent = 10;

    /* 상품 가격 * 할인율% / 100 */
    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}
  • 기존 DisCountPolicy(인터페이스)를 구현하는 RateDiscountPolicy 생성
    • 상위 인터페이스의 메서드를 오버라이딩하여 재정의
  • 할인율 : 상품 가격 * 할인율(10) / 100 = 상품별 10%

RateDiscountPolicyTest 클래스 생성

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.assertThat;

class RateDiscountPolicyTest {

    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("VIP는 10% 할인이 적용되어야 한다.")
    public void VIP인_경우_할인율_테스트() {
        //given
        Member member = new Member(1L, "memberVIP", Grade.VIP);

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

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

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
    public void VIP가_아닌경우_할인율_테스트() {
        //given
        Member member = new Member(2L, "memberBasic", Grade.BASIC);

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

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

}
  • 새로운 Member 1, 2명을 생성
  • RateDiscountPolicy의 discount(member, price) 호출
    • 해당 함수에 의해 할인율이 나오게 됨
  • assertThat 라이브러리를 통해 검증 진행

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


위에서 추가한 할인 정책을 적용 해보자. 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 수정해야 한다. 즉, DIP 원칙을 위배하는 소스라는 점이다.

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

    // 변경 되어야 하는 부분
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

}

문제점 발견

  • 우리는 역할과 구현을 충실하게 분리 하였음
  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리
  • OCP(개방 폐쇄), DIP(의존 역전 법칙)도 충실히 준수
    • 그렇게 보이지만 사실은 아님
  • 의존 역전 법칙을 준수하는 것 같지만, 인터페이스뿐만이 아닌 구체(구현) 클래스에도 의존하고 있기 때문에 준수 한다고 볼 수 없다.
    • 추상(인터페이스) 의존 : DiscountPolicy(I)
    • 구체(구현) 클래스: FixDiscountPolicy(C), RateDiscountPolicy(C)
  • 지금 코드는 확장해서 변경하면, 클라이언트 코드에 영향을 줌, 그래도 OCP 위반하는 상태

OrderServiceImpl 코드 수정

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /* 기존 Client 소스를 변경 하여야 한다 */
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//    TODO: 03. 관심사의 분리 내용 진행
//    private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 이 부분이 문제임, DIP 원칙 위배

    // 의존관계 역전 법칙 적용, 의존성 주입을 안해주고 있기 때문에 NullPointerException 발생!!
    // 누군가 대신 의존성 주입을 해줘야 Client 코드를 건드리지 않고 DIP 원칙을 지킬 수 있다
    // 누구?? -> 스프링 IOC 컨테이너 + DI 원칙
    private DiscountPolicy discountPolicy; // 중요 라인

    /**
     * @param memberId : 회원 번호
     * @param itemName : 상품명
     * @param itemPrice : 상품 가격
     * @return 주문 정보 반환
     */
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        // memberId를 기반으로 하여 회원 정보를 찾는다
        Member member = memberRepository.findById(memberId);

        // 회원 정보를 기반으로 할인율을 받아온다
        int discountPrice = discountPolicy.discount(member, itemPrice);

        // 회원 정보를 그대로 반환 해준다, DB는 사용하지 않음
        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
  • 여기서 핵심은 discountPolicy 변수를 누군가는 초기화 해주어야 한다는 점
    • Client의 소스 변경이 없이도 실행이 가능
    • 누구? : 스프링 IoC 컨테이너에 의한 DI(Dependency Injection) 수행
  • DI 원칙은 개발자가 직접 bean 객체를 초기화 하는 것이 아닌 Ioc 컨테이너에 의해 객체의 생성과 소멸을 담당하게 하여 개발자는 비즈니스 로직에만 초점을 맞춰 개발을 할 수 있게 하는 스프링의 기술 중 하나이다

03. 관심사의 분리

  • 애플리케이션을 하나의 공연이라고 생각 해보자. 각각의 인터페이스(MemberService, DiscountPolicy)를 배역(배우 역할)이라고 생각하자. 그런데! 실제 배역에 맞는 배우를 선택하는 것은 누구인가? → 배우가 선택하지 않고, 공연 기획자(감독)이 배역의 배우 선택
  • 로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지 줄리엣 역할을 누가 할지는 배우들이 정하는게 아니다. 이전 코드는 마치 로미오 역할(인터페이스)을 하는 레오나르도 디카프리오(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다. 디카프리오는 공연도 해야하고 동시에 여자 주인공도 공연에 직접 초빙해야 하는 “다양한 책임”을 가지고 있다.

03-1. 관심사를 분리하자

  • 배우는 본인의 역할인 배역을 수행(구현)하는 것에만 집중해야 한다
  • 디카프리오는 어떤 여자 주인공이 선택되어도 똑같이 공연을 할 수 있어야 한다
  • 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우을 지정하는 공연 기획자가 필요
  • 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리하자

03-2. AppConfig의 등장

  • 애플리케이션 전체 동작 방식을 구성(config)하기 위해, “구현 객체를 생성”하고, 연결하는 책임을 가진 별도의 설정 클래스를 만들자
  • 스프링에서는 Ioc 컨테이너가 빈의 생성과 소멸을 담당한다

AppConfig

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
    • MemoryMemberRepository
    • OrderServiceImpl
    • FixDiscountPolicy
  • AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 “생성자를 통해 주입” 해준다
    • MemberServiceImpl → MemoryMemberRepository
    • OrderServiceImpl → MemoryMemberRepository, FixDiscountPolicy

MemberServiceImpl

package hello.core.member;

public class MemberServiceImpl implements MemberService {

    //AS-IS 
    //private final MemberRepository memberRepository = new MemoryMemberRepository();

    //DI(Dependency Injection)을 AppConfig가 담당하여 객체의 생성을 대신 진행 해준다 
    private final MemberRepository memberRepository;

    /* (상속 구조) MemoryMemberRepository extends 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);
    }
}
  • memberRepository는 더 이상 MemoryMemberRepository에 의존하지 않는다

클래스 다이어그램 - 회원

  • 객체의 생성과 연결은 AppConfig가 담당한다
  • “DIP 완성” MemberServiceImpl은 MemberRepository인 추상에만 의존하면 된다. 이제 구체 클래스를 몰라도 된다
  • “관심사의 분리”: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확히 분리 되었다

그림 - 회원 객체 인스턴스 다이어그램

  • appConfig 객체는 memoryMemberRepository 객체를 생성하고 그 참조값을 memberServiceImpl을 생성 하면서 생성자로 전달한다
  • 클라이언트인 memberSerivceImpl 에서 보면 의존 관계를 마치 외부에서 주입 해준 것과 같다고 해서 DI(Dependency Injection) 우리말로 의존 관계 주입 또는 의존성 주입이라 한다

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 {

    /* 기존 Client 소스를 변경 하여야 한다 */
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//    TODO: 03. 관심사의 분리 내용 진행
//    private final DiscountPolicy discountPolicy = new RateDiscountPolicy(); // 이 부분이 문제임, DIP 원칙 위배

    // 의존관계 역전 법칙 적용, 의존성 주입을 안해주고 있기 때문에 NullPointerException 발생!!
    // 누군가 대신 의존성 주입을 해줘야 Client 코드를 건드리지 않고 DIP 원칙을 지킬 수 있다
    // 누구?? -> 스프링 IOC 컨테이너 + DI 원칙
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    /**
     * @param memberId : 회원 번호
     * @param itemName : 상품명
     * @param itemPrice : 상품 가격
     * @return 주문 정보 반환
     */
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        // memberId를 기반으로 하여 회원 정보를 찾는다
        Member member = memberRepository.findById(memberId);

        // 회원 정보를 기반으로 할인율을 받아온다
        int discountPrice = discountPolicy.discount(member, itemPrice);

        // 회원 정보를 그대로 반환 해준다, DB는 사용하지 않음
        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
  • 위 내용과 동일, 의존성 주입을 외부에서 해준다

AppConfig 실행

MemberApp 실행

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;

public class MemberApp {

    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig(); // AppConfig 객체 생성
        MemberService memberService = appConfig.memberService(); /**/ memberSerivce 객체를 가져온다**

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

        Member findMember = memberService.findMember(1L);
        System.out.println("new Member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());
    }
}

OrderApp 실행

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;

public class OrderApp {

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

//        MemberService memberService = new MemberServiceImpl(null);
//        OrderService orderService = new OrderServiceImpl(null, null);

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);

        // when
        memberService.join(member); // 회원 가입
        Order order = orderService.createOrder(member.getId(), "itemA", 10000); // 상품 주문

        // then
        // order = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=1000}
        System.out.println("order = " + order); // 상품 결과
        System.out.println("order = " + order.calculatePrice()); // 상품 결과
    }
}

04. AppConfig 리팩터링

04-1. AppConfig 리팩터링

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
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(memberRepository());
    }

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

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

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

}
  • new MemoryMemberRepository() 이 부분의 중복 제거
  • MemoryMemberRepository를 다른 구현체로 변경할 때 한 부분만 변경하면 된다

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

  • 처음으로 돌아가서 정액 할인 정책을 정률 할인 정책으로 변경 (악덕 기획자 요청)
  • FixDiscountPolicy → RateDiscountPolicy
  • 어떤 부분만 변경하면 될까?

AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성(Configuration) 하는 영역으로 분리 되었다

05-1. 그림 - 사용, 구성의 분리

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
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(memberRepository());
    }

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

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

    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy(); // 변경한 부분
        return new RateDiscountPolicy();
    }
}
  • 기존에는 Client 영역인 OrderServiceImpl 소스에 영향이 있었다. 하지만 현재는 discountPolicy 메서드만 수정 하여도 다른 로직에 영향을 주지 않는다
  • 위 사진을 봐도 AppConfig에서 생성하는 부분만 RateDiscountPolicy 객체로 변경 해주는 것을 확인을 할 수 있다

06. 전체 흐름 정리

지금까지의 흐름을 간략히 정리

  • 새로운 할인 정책 개발 ( 악덕 기획자 요청 → 고정 할인율 → 10% 유동 할인율 )
  • 새로운 할인 정책 적용 및 문제점
    • xxServiceImpl 클라이언트 소스를 수정 해야 하는 문제점
  • 관심사 분리
    • AppConfig 생성 후 객체의 생성과 소멸을 담당하도록 수정
  • AppConfig 리팩터링
  • 새로운 구조와 할인 정책 적용

06-1. 새로운 할인 정책 개발

  • 다형성으로 인해 새로운 할인 정책을 개발하는데 큰 무리가 없었음

06-2. 새로운 할인 정책 적용 및 문제점

추상화에 의존하고 구체화에는 의존하지 마라 → DIP

  • 새로 개발한 정률 할인 정책을 적용하려고 하니 클라이언트 코드 인 주문 서비스 구현체도 함께 변경해야함. 주문 서비스 클라이언트(OrderServiceImpl)가 인터페이스인 DiscountPolicy 뿐만 아니라, 구체 클래스인 FixDiscountPolicy도 함께 의존 → DIP 위반

06-3. 관심사의 분리

  • 애플리케이션을 하나의 공연으로 생각
  • 기존에는 클라이언트가 의존하는 서버 구현 객체를 직접 생성 및 실행
  • 공연 기획자인 AppConfig(IOC Container 역할)가 등장
  • AppConfig는 애플리케이션의 전체 동작 방식을 구성(Config)하기 위해 구현 객체를 생성하고 연결하는 책임을 가짐
  • 클라이언트 객체는 자신의 역할을 실행하는 것만 집중

06-4. AppConfig 리팩터링

  • 구성 정보에서 역할과 구현을 명확하게 분리
  • 역할이 잘 들어남
  • 중복 제거

06-5. 새로운 구조와 할인 정책 적용

  • new FixDiscountPolicy() → new RateDiscountPolicy()로 변경해도 이상 없음

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

여기서 3가지 SRP, DIP, OCP 적용

07-1. SRP

한 클래스는 하나의 책임만 가져야 한다 (SRP: Single Responsibility Principle)

  • 클라이언트 객체는 직접 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있었음, 단일 책임 원칙을 위반
  • SRP 단일 책임 원칙을 따르면서 관심사(역할)를 분리
  • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당
  • 클라이언트 객체(OrderServiceImpl, MemberServiceImpl)는 실행하는 책임을 담당

07-2. DIP

프로그래머는 “추상화에 의존해야지” 구체화에 의존하면 안된다. 의존성 주입의 원칙

  • 새로운 할인 정책을 개발하고, 적용하려고 하니 클라이언트 코드 변경 필요
  • 막상 DIP 원칙도 지킨줄 알았는데, memberRepository, discountPolicy 직접 초기화 하는 소스가 존재하여 DIP 원칙에도 위반되고 있었음
  • public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository = new MemoryMemberRepository(); private final DiscountPolicy discountPolicy = new FixDiscountPolicy(); ... }
  • 클라이언트 코드가 DiscountPolicy 인터페이스에만 의존 하도록 코드 변경
  • 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 DiscountPolicy discountPolicy; public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; } ... }
  • 하지만 클라이언트 코드(OrderServiceImpl) 만으로는 객체가 null이기에 사용 불가능
  • AppConfig를 통해 의존 관계 설정을 해주었다

07-3. OCP

소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다

  • 다형성을 사용하고 클라이언트가 DIP를 지킴
  • 애플리케이션을 사용 영역과 구성 영역으로 나눔
  • AppConfig가 의존 관계를 FixDiscountPolicy → RateDiscountPolicy로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨

08. IoC, DI, 그리고 컨테이너

08-1. 제어의 역전 - IoC(Inversion of Control)

  • 기존 프로그램은 클라이언트의 구현 객체(OrderserviceImpl)가 필요한 구현 객체를 생성, 연결, 실행 하였다
    • 한 마디로 구현 객체가 제어 흐름을 스스로 조종
  • 반면 AppConfig의 등장 이후 구현 객체는 자신의 로직을 실행하는 것만 담당하고 제어의 흐름은 AppConfig에게 책임을 위임 하였다
  • 이렇듯 프로그램의 ‘제어 흐름’을 직접 제어하는 것이 아니라 외부(IoC 컨테이너)에서 관리하는 것을 제어의 역전(IoC)라고 지칭한다

프레임워크 vs 라이브러리

  • 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 프레임워크(JUnit)
    • Spring F/W
  • 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 라이브러리
    • gson 라이브러리 호출하여 json 컨버팅 작업

08-2. 의존관계 주입 - DI(Dependency Injection)

  • DI(Dependency Injection)란 개발자가 스스로 객체를 생성하는 것이 아닌 외부 조립기인 Ioc 컨테이너에 의해 객체가 생성되고 소멸되는 일련의 과정을 의미한다. 그렇기에 개발자는 비즈니스 로직에만 집중을 할 수 있고, 의존성의 경우 외부에서 주입을 해준다
  • 의존관계는 ‘정적인 클래스 의존 관계와’, ‘실행 시점에 결정되는 동적인 객체(인스턴스)의 의존 관계를’ 분리해서 생각해야 한다.
    • 정적 클래스의 의존 관계 vs 실행 시점에 결정되는 동적 클래스의 의존 관계

정적인 클래스 의존관계

  • 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다
  • 정적인 클래스 의존관계는 애플리케이션을 실행하지 않아도 분석 가능
  • OrderServiceImpl은 MemberRepository, DiscountPolicy에 의존한다는 것을 알 수 있다. 그런데 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderServiceImpl에 주입되는지 알 수 없음

동적인 객체 인스턴스 의존 관계

  • 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계

객체 다이어그램

  • 객체 다이어그램은 애플리케이션 실행 시점에 외부에서 실제 구현 객체를 생성 후 클라이언트에게 전달하여 실제 의존 관계가 형성 되는 것
  • 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 변경할 수 있다

08-3. IoC 컨테이너, DI 컨테이너

  • AppConfig처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 역할을 담당
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 지칭함

09. 스프링으로 전환하기

지금까지 순수한 자바로 개발을 진행 → 스프링으로 전환 시작

09-1. AppConfig를 Spring 기반으로 변경

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberRepository;
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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

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

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

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

    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • @Configuration, @Bean 어노테이션 추가

09-2. MemberApp 수정

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {

    /*public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();

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

        Member findMember = memberService.findMember(1L);
        System.out.println("new Member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());
    }*/

    // V2 Java -> Spring boot 전환
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();

        // @Configuration
        // AppConfig.class 인수로 넣어줌으로 인해 스프링 컨테이너에 다 등록을 한다
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = ac.getBean("memberService", MemberService.class);

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

        Member findMember = memberService.findMember(1L);
        System.out.println("new Member = " + member.getName());
        System.out.println("findMember = " + findMember.getName());
    }
}
  • ApplicationContext를 사용하여 MemberService Bean을 가져온다
  • 이 때 AnnotationConfigApplicationContext(AppConfig.class)를 넣어 사용

09-3. OrderApp 수정

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class OrderApp {

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

//        MemberService memberService = new MemberServiceImpl(null);
//        OrderService orderService = new OrderServiceImpl(null, null);

        //FIXME: Java -> Spring Boot Bean DI
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = ac.getBean("memberService", MemberService.class);
        OrderService orderService = ac.getBean("orderService", OrderService.class);

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);

        // when
        memberService.join(member); // 회원 가입
        Order order = orderService.createOrder(member.getId(), "itemA", 10000); // 상품 주문

        // then
        // order = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=1000}
        System.out.println("order = " + order); // 상품 결과
        System.out.println("order = " + order.calculatePrice()); // 상품 결과
    }
}
  • OrderApp 역시 마찬가지로 ApplicationContext를 통해 Bean을 가져와 처리
  • ApplicationContext"를 “스프링 컨테이너”라 한다
  • 기존에는 AppConfig를 사용하여 직접 객체를 생성하고 DI를 수행, 이제는 DI 컨테이너가 해줌
    • @Bean으로 등록된 메서드를 스프링 컨테이너에 전부 등록
    • 해당 메서드의 이름을 bean 이름으로 등록
  • 지금부터는 스프링 컨테이너가 의존성 주입을 담당

10. 스프링 컨테이너와 빈

10-1. 스프링 컨테이너 생성

스프링 컨테이너가 생성되는 과정을 알아보자 스프링 빈을 생성하는 과정과 의존 관계를 연결하는 과정은 분리가 되어있다

ApplicationContext ac = new AnnotaionConfigApplicationContext(AppConfig.class)
  • ApplicationContext스프링 컨테이너라 지칭
  • ApplicationContext는 인터페이스
    • AnnotaionConfigApplicationContext 구현체가 ApplicationContext 구현
  • 스프링 컨테이너는 XML 혹은 어노테이션 기반 자바 설정 클래스로 생성 가능
  • 스프링 컨테이너의 종류로는 BeanFactory, ApplicationContext가 존재

10-2. 스프링 컨테이너의 생성 과정

  1. new AnnotaionConfigApplicationContext(AppConfig.class)
  2. 스프링 컨테이너를 생성할 때는 구성 정보를 지정해야 한다
  3. 여기서는 AppConfig.class를 구성 정보로 지정한다

10-3. 스프링 빈 등록

  1. 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용하여 스프링 빈을 생성
  2. 빈 이름 : 빈 객체
  3. 빈 이름은 메서드 이름을 사용
  4. 빈 이름은 직접 부여 가능 : @Bean(name = “memberService2”)

10-4. 스프링 빈 의존관계 설정 - 준비

  1. 스프링 빈을 등록한 후 해당 Bean에 의존 관계를 설정 한다

10-5. 스프링 빈 의존간계 설정 - 완료

  1. 스프링 컨테이너는 설정 정보를 참고해서 의존관계 주입
  2. 단순히 자바 코드를 호출하는 것 같지만, 이 차이는 뒤에 싱글톤 컨테이너에서 설명

스프링은 빈을 생성하고, 의존 관계를 주입하는 단계가 나누어져있다. 그런데 이렇게 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계도 한번에 처리된다.

10-6. 컨테이너에 등록된 모든 빈 조회

스프링 컨테이너에 등록된 모든 빈을 조회 해보자

package hello.core.beanfind;

import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ApplicationContextTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean() {
        //given
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + ", object = " + bean);
        }

        //when
        //then
    }

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean() {
        //given
        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); // 빈 이름으로 조회
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            // BeanDefinition.ROLE_APPLICATION : 직접 등록한 애플리케이션 빈
            // BeanDefinition.ROLE_INFRASTRUCTURE : 스프링 내부에서 사용하는 빈
            if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName + ", object = " + bean);
            }
        }

        //when
        //then
    }
    
}
  • 모든 빈 출력하기 : AnnotationConfigAppliocationContext
    • 실행 시 스프링에 등록된 모든 빈 정보 출력 가능
    • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름 조회
    • ac.getBean() : 빈 이름으로 빈 객체 조회
  • 애플리케이션 빈 출력하기
    • 스프링이 내부적으로 사용하는 빈은 제외하고, 내가 등록한 빈 출력
    • BeanDefinition.getRole()을 통해 구분 가능
      • BeanDefinition.ROLE_APPLICATION : 직접 등록한 애플리케이션 빈
      • BeanDefinition.ROLE_INFRASTRUCTURE : 스프링 내부에서 사용하는 빈

10-7. 스프링 빈 조회 기본

💡 ApplicationContext 안에 존재하는 bean을 조회하는 방법을 테스트 케이스로 작성 해보는 시간을 갖는다
  • 스프링 컨테이너에서 빈을 찾는 가장 기본적인 조회 방법
  • ac.getBean(빈이름, 타입)
  • ac.getBean(타입)
  • 조회 대상 스프링 빈이 없으면 예외 발생
    • NoSuchBeanDeinitionException : No bean named ‘xxxxx’ available

빈 이름으로 조회

public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContenxt ac = new AnnotationConfigApplicationContenxt(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        // bean 이름은 메서드명으로 지정되기에 'memberService' 라는 이름을 갖는 bean을 출력
        // 실제로는 -> MemberServiceImpl(구현 : 클래스) extends MemberService(역할 : 인터페이스)
        MemberService memberService = ac.getBean("memberService", MemberService.class);

        // then
        // memberService 객체가 MemberServiceImpl.class의 인스턴스인가?
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
}

빈 타입으로 조회

public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContenxt ac = new AnnotationConfigApplicationContenxt(AppConfig.class);

    ...

    @Test
    @DisplayName("빈 이름없이 타입으로만 조회")
    void findBeanByType() {
		// 인수를 타입만 사용하여 bean 조회
		MemberService memberService = ac.getBean(MemberService.class);
		assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }
}
  • BeanDefinition을 빈 설정 메타정보라 지칭
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생

 

public class ApplicationContextInfoTest {
		
    AnnotationConfigApplicationContenxt ac = new AnnotationConfigApplicationContenxt(AppConfig.class);

    ...

    @Test
    @DisplayName("빈 이름으로 조회 x")
    void findBeanByType() {

            //when
    //then
    assertThrows(NoSuchBeanDefinitionException.class,
            () -> ac.getBean("xxxx", MemberServiceImpl.class));
    }
}

10-8. 스프링 빈 조회 - 동일한 타입이 둘 이상

💡 만약 bean을 조회하는 경우 bean 이름은 다르지만 타입이 같은 bean이 1개 이상이 존재 한다 가정 해보자
  • 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류 발생. 이때는 빈 이름 지정 필요
  • ac.getBeanOfType() 을 사용하면 해당 타입의 모든 빈을 조회할 수 있다

타입으로 조회시 같은 타입이 둘 이상이면 오류 발생

public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByTypeDuplicate() throws Exception {
//				MemberRepository memberRepository = ac.getBean(MemberRepository.class);

		//then
		assertThrows(NoUniqueBeanDefinitionException.class,
		() -> ac.getBean(MemberRepository.class));
    }

    /* class 안에 inner class를 사용했다는 것은, 해당 클래스의 안에서(scope)만 사용하겠다는 것을 의미 함 */
  	@Configuration
  	static class SameBeanConfig {

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

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

타입으로 조회 시 같은 타입이 둘 이상이면 빈 이름을 지정

public class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

    ...

    @Test
    @DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByName() throws Exception {
		// given
		MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

		// when
		// then
		assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }


    /* class 안에 inner class를 사용했다는 것은, 해당 클래스의 안에서(scope)만 사용하겠다는 것을 의미 함 */
  	@Configuration
  	static class SameBeanConfig {

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

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

특정 타입을 모두 조회하기

public class ApplicationContextSameBeanFindTest {

	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);

        ...

	@Test
    @DisplayName("특정 타입을 모두 조회하기 -> bean 이름이 다르고 타입은 갖은 경우가 1개 이상인 경우")
    void findAllBeanByType() throws Exception {
		//given
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
        System.out.println("beansOfType = " + beansOfType);

        //when
        //then
        assertThat(beansOfType.size()).isEqualTo(2);
	}

	/* class 안에 inner class를 사용했다는 것은, 해당 클래스의 안에서(scope)만 사용하겠다는 것을 의미 함 */
	@Configuration
	static class SameBeanConfig {

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

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

10-9. 스프링 빈 조회 - 상속 관계

  • 부모 타입으로 조회하면, 자식 타입도 함께 조회
  • 그래서 모든 자바 객체의 최고 부모인 Object 타입으로 조회하면 모든 빈을 조회한다
  • 사실 ApplicationContenxt 내에서 직접 bean을 뽑을 일은 없음

부모 타입으로 조회하기

package hello.core.beanfind;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ApplicationContextExtendsFindTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회 시 자식(bean)이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() throws Exception {
        //given
        //when
        //then
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면, 빈 이름을 지정")
    void findBeanByParentTypeBeanName() throws Exception {
        //given
        DiscountPolicy bean = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);

        //when
        //then
        assertThat(bean).isInstanceOf(DiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() throws Exception {
        //given
        // getBean -> ApplicationContext 안에 있는 RateDiscountPolicy Type을 통해 추출
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);

        //when
        //then
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllByParentType() throws Exception {
        //given
        Map<String, DiscountPolicy> beans = ac.getBeansOfType(DiscountPolicy.class);
        for (String key : beans.keySet()) {
            System.out.println("key = " + key + ", value = " + beans.get(key));
        }

        //when
        //then
        assertThat(beans.size()).isEqualTo(2);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기 - Object")
    void findAllBeanByObjectType() throws Exception {
        //given
        Map<String, Object> beans = ac.getBeansOfType(Object.class);
        for (String key : beans.keySet()) {
            System.out.println("key = " + key + ", value = " + beans.get(key));
        }

        //when

        //then
    }

    @Configuration
    static class TestConfig {

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

        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }
}

10-10. BeanFactory와 ApplicationContext

💡 어떻게 보면 가장 중요한 부분이라 볼 수 있음

Spring Boot의 Cotainer인 BeanFactory, ApplicationContext

  • BeanFactory를 상속받는 ApplicationContext가 존재
  • ApplicationContext를 구현한 AnnotationConfigApplicatonContext가 존재

BeanFactory

  • 스프링 컨테이너최상위 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할 담당
  • getBean() 메서드 제공
  • 지금까지 사용한 기능은 BeanFactory가 제공하는 기능

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공
  • 스프링 빈을 관리하고 조회하는 역할을 BeanFactory가 담당하는데 얘는 뭐지?
  • 애플리케이션을 개발할 때 빈을 관리하고 조회하는 기능은 물론이고 수 많은 부가 기능이 필요하기 때문에 ApplicationContext를 사용한다

ApplicationContext가 제공하는 부가기능

실제 소스 레벨에서의 ApplicationContext

ApplicationContext가 구현한 인터페이스

  • 메시지 소스를 활용한 국제화 기능 (MessageSource Interface)
    • 예를 들어 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
  • 환경변수 (EnvironmentCapable Interface)
    • 로컬, 개발, 운영 등을 구분해서 처리
  • 애플리케이션 이벤트 (ApplicationEventPublisher Interface)
    • 이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회 (ResourceLoader Interface)
    • 파일, 클래스 패스, 외부 등에서 리소스를 편리하게 조회

10-11. XML 기반 설정

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- MemberService bean -->
    <bean id="memberService" class="hello.core.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>

    <bean id="memberRepository" class="hello.core.member.MemoryMemberRepository" />

    <!-- OrderService bean -->
    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref="discountPolicy" />
    </bean>

    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy" />

</beans>
package hello.core.xml;

import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class XmlAppContext {

    @Test
    void test() throws Exception {
        //givena
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
        MemberService memberService = ac.getBean("memberService", MemberService.class);

        //when
        //then
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

10-12. 스프링 빈 설정 메타 정보 - BeanDefinition

스프링 어떻게 이런 다양한 설정 형식(XML, Java 코드)을 지원하는 것일까? 그 중심에는 BeanDefinition 이라는 추상화가 있다. 이전에 우리는 ‘역할’과 ‘구현’이라는 개념으로 추상화를 시킨 적이 있는데..

  • 쉽게 말해서 역할과 구현을 개념적으로 나눈 것
    • XML을 읽어서 BeanDefinition 생성
    • 자바 코드를 읽어서 BeanDefinition 생성
    • 스프링 컨테이너는 자바 코드인지, XML 인지 몰라도 됨 → BeanDefinition만 알면됨
  • BeanDefinition을 빈 설정 메타정보라 지칭
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성

블로그의 정보

기록하고, 복기하고

ymkim

활동하기