Notice
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
01-09 04:37
Today
Total
관리 메뉴

그날그날 공부기록

스프링 DI 이해하기 본문

Spring 공부

스프링 DI 이해하기

given_dragon 2022. 7. 5. 17:10

스프링의 원리를 이해하기 위해 스프링을 사용하지 않고 자바로 구현해보았다.

 

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    
    @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);
    }
}

고객 정보와 상품의 정보를 받아 주문 객체를 만드는 클래스이다.

할인 정책은 변경 가능하도록 DiscountPolicy라는 인터페이스를 만들고,

그 일정한 금액을 할인하는 FixDiscountPolicy, 상품 금액의 비율로 할인하는 RateDiscountPolicy를 구현체로 만들었다.

얼핏 보면 OrderServiceImpl은 DiscountPolicy 인터페이스에만 의존하는 것 같지만 구현체까지 모두 의존하고 있는 형태이다. → 추상화에만 의존해야 하지만 그렇지 않아 DIP위반이다~

구현체에도 의존하여 DIP를 위반하는 코드이기 때문에 할인 정책을 Fix → Rate로 변경할 시 클라이언트인 OrderServiceImpl의 소스코드를 변경해야 한다.OCP를 위반해버린다!


해결방법

클라이언트가 추상화에만 의존하도록 하면 된다.

아래의 클라이언트 코드는 구현체에 의존하지 않고 인터페이스에만 의존한다. → DIP를 위반하지 않는다. (우선 DiscountPolicy에 대해서만 생각)

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private 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);
    }
}

하지만 할인 정책의 구현체가 없으므로 코드가 실행되지 않는다. → 누군가 DiscountPolicy의 구현체를 생성하고 주입해주어야 한다.


관심사의 분리

김영한 님이 설명하신 배역(인터페이스)과 배우(구현체)의 비유가 이해가 잘 되었다.

  • 배우는 자신의 역할인 배역에만 집중해야 한다.
  • 같은 배역을 맡은 배우라면 항상 같은 역할을 해야 한다. → 어떤 구현체든 인터페이스를 따라야 함.

제일 처음 나온 DIP, OCP를 위반하는 코드 역시 OrderService의 구현체이다.

현제 주문 객체를 만드는 것에만 집중해야 할 OrderServiceImpl 구현체가 할인 정책까지 스스로 결정하고 있다. → 비유하자면 배역에만 집중해야 하는 배우가 다른 배우까지 섭외하고 있다고 볼 수 있다.

이를 해결하려면 배우가 배역에만 집중할 수 있도록 배우를 섭외하는 사람인 기획자를 만들어야 한다.

즉, DiscountPolicy의 구현체를 생성하고 주입해주는 별도의 클래스를 생성해야 한다.


AppConfig 생성

이 전 코드는 MemberRepository의 구현체도 의존했지만 아래의 코드처럼 생성자를 통해 별도의 클래스를 통해 구현체를 주입받도록 했다. 이제 OrderServiceImpl은 완전히 구현체에 의존하지 않고, 인터페이스에만 의존한다.

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 클래스는 클라이언트에게 구현체를 생성하여 주입해준다.

public class AppConfig {

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

    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new RateDiscountPolicy());
    }
}
  • DIP 준수: OrderServiceImpl은 의존관계에 대한 고민은 AppConfig에게 맡기고 자신의 역할에만 집중하면 된다.
  • 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 분리되었다

클라이언트의 입장에서 보면 의존관계를 외부에서 주입해주는 것 같다고 해서 DI(Dependency Injection) | 의존관계 주입 | 의존성 주입이라고 한다.


AppConfig 리팩터링 하기

new MemoryMemberRepository()가 중복되고 한눈에 역할과 구현을 파악하기 어렵다.

public class AppConfig {

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

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

아래의 코드처럼 리팩터링을 하면 코드의 중복도 제거하고 전체적인 구성을 빠르게 파악할 수 있다.

→ 멤버 저장소는 어떤것인지, 할인 정책은 무엇을 쓰는지 빠르게 파악 가능

public class AppConfig {

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

    private MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    public DiscountPolicy discountPolicy(){
        return new RateDiscountPolicy();
    }
}

이제 클라이언트 코드인 사용 영역을 변경하지 않고 AppConfig의 discountPolicy()만 변경하면 할인 정책을 변경할 수 있다.

김영한님 강의 자료의 그림이다. 지금까지의 구조를 잘 파악할 수 있다.


테스트 코드 작성

public class OrderServiceTest {
    MemberService memberService;
    OrderService orderService;

    //각 테스트 메서드가 실행되기 전 호출되는 메서드이다.
    @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);
        memberService.join(member);

        //when
        Order order = orderService.createOrder(member.getId(), "itemA", 10000);

        //then
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

@BeforeEach로 테스트 메서드가 여러 개 있을 시 편리하게 사용할 수 있다.

 


스프링으로 전환하기

설정을 담당하는 AppConfig클래스에 @Conficuration을 적어준다.

메서드마다 @Bean을 붙여주면 반환되는 객체들이 스프링 컨테이너에 등록이 된다.

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 OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy();
    }
}

 

기존 코드에서 구현체를 가져오는 부분을 주석처리 하고 다음 2줄을 추가한다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext를 스프링 컨테이너라고 한다.

스프링 컨테이너는 @Bean이 있는 모든 메서드를 호출하여 반환된 객체를 등록한다. 이 객체를 스프링 빈이라고 한다.

스프링은 기본적으로 객체를 생성하고, 메서드 이름으로 등록한다. → (key:메서드이름, value:객체) @Bean(name=”${NAME}”)이라고 적으면 별도로 설정이 가능하다.

 

MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

스프링 컨테이너에서 스프링 빈을 가져오기 위해 다음과 같이 적어준다.

getBean(스프링 빈 이름, 반환 타입)이다.

 

위의 2줄을 추가한 전체 코드이다.

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

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

        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("find emember = " + findMember.getName());
    }
}

 

그 후 실행을 해보면 결과는 같지만 로그들이 추가되어 출력된다.

12:58:25.434 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@33e5ccce
12:58:25.440 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
12:58:25.493 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
12:58:25.494 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
12:58:25.494 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
12:58:25.495 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
12:58:25.498 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
12:58:25.500 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberService'
12:58:25.508 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberRepository'
12:58:25.509 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
12:58:25.510 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'
new member = memberA
find emember = memberA

스프링 빈들이 등록이 되는 로그인데 Bean이 생성되는 것을 확인할 수 있다.

첫 5개는 스프링이 내부적으로 필요해서 등록하는 Bean이고 그 다음부터는 직접 등록해준 Bean들이 등록되는 것을 볼 수 있다.


출처

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/lecture/55343

 

스프링 핵심 원리 - 기본편 - 인프런 | 학습 페이지

지식을 나누면 반드시 나에게 돌아옵니다. 인프런을 통해 나의 지식에 가치를 부여하세요....

www.inflearn.com

'Spring 공부' 카테고리의 다른 글

스프링 빈 조회  (0) 2022.07.11
스프링 컨테이너 생성 과정  (0) 2022.07.07
IoC, DI, 컨테이너  (0) 2022.07.06
SOLID  (0) 2022.07.05
스프링과 객체지향  (0) 2022.07.05
Comments