[spring 핵심 기본] AppConfig, Spring 컨테이너로 DI 적용

채원·2024년 4월 1일

스프링

목록 보기
11/18
post-thumbnail

출처) 인프런 스프링 핵심 원리 기본편 강의

지난 강의에서는 순수 java 코드로 회원, 구매, 할인 도메인을 만들었는데 그 코드에 추가로 spring을 사용해서 확장해보자

애자일 = 계획보다는 변화에 대응

할인 정책 추가

주문 금액의 %를 할인해주는 정률할인 정책 추가
DiscountPolicy 구현체로 퍼센트 할인 해주는 정책을 추가함
VIP인 경우에는 금액의 퍼센트만큼 할인

package hello.core.discount;

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

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;
       }
       else{
           return 0;
       }
    }
}

추가된 할인 정책 test

comand + shift + T : 자동으로 Junit 테스트 생성해줌
@DisplayName("콘솔에 띄울 문구") 를 통해 테스트 텍스트 출력 가능
Assertions의 isEqualTo를 통해 할인금액이 맞는지 (퍼센트 할인이 잘 되었는지 확인)
Grade.BASIC일 때 할인이 적용되는지도 확인해본다.

...
class RateDiscountPolicyTest
{
    RateDiscountPolicy rateDiscountPolicy = new RateDiscountPolicy();
    @Test
    @DisplayName("VIP는 10% 할인이 적용되어야 한다")

    void vip_o(){
        // given
        Member member = new Member(1L, "memberVIP", Grade.VIP);
        // when
        int discount = rateDiscountPolicy.discount(member,10000);
        // then
        Assertions.assertThat(discount).isEqualTo(1000);

    }

}

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

위에서 생성한 정률할인 정책을 적용해보자
OrderServiceImpl에서 기존 할인 정책을 주석처리 후 새 정책으로 바꾸면 된다.

여기서 역할, 구현 분리했지만, OCP, DIP와 같은 객체 지향 설계 원칙을 준수하지 않은 것을 알 수 있다.

  • DIP 위반
    OrderServiceImpl은 DiscountPolicy의 추상(인터페이스)에 의존하고 있지만, 한 편으로 구현 클래스에도 의존하고 있다.
  • OCP 위반

DiscountPolicy의 변경을 원할 때는 OrderServiceImpl (클라이언트) 코드 변경도 필요하다.

이를 해결하기 위해서는 추상(인터페이스)에만 의존하도록 의존관계 변경이 필요

아래처럼 코드를 수정하면 구현체가 없기 때문에 오류가 발생한다.
-> ⭐️ 누군가가 구현 객체를 대신 생성하게끔 주입해야함

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

AppConfig

기존 코드는 OrderServiceImpl(구현체)가 OrderService 뿐만 아니라, DiscountPolicy의 구현체를 만들어야하는 역할까지 해야하는 다양한 책임을 가지고 있음

인터페이스가 다양한 책임을 가지지 않도록, 관심사의 분리가 필요하다

➡️ 구현 객체를 생성하고, 연결하는 책임을 가지는 설정 클래스를 생성하자 (관심사의 분리)
== AppConfig

hello.core 바로 아래에 클래스파일 생성

1. MemberServiceImpl에서의 관심사 분리

기존 코드에서 MemberServiceImpl이 MemoryMemberRepository를 만드는 코드가 있었는데
이를 삭제하고, AppConfig에서 생성하는 것으로 변경

AppConfig
생성자 주입 (의존성 주입 DI) 형태로 구현체를 넘겨줌

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

MemberServiceImpl
아래 코드를 통해, MemberServiceImpl은 MemberRepository 인터페이스에만 의존하고, 구현에는 의존하지 않게 됨

...
    private final MemberRepository memberRepository;
    public MemberServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

2. OrderServiceImpl에서의 관심사 분리

OrderServiceImpl에서는 MemberRepository와 DiscountPolicy 2개의 구현을 의존하고 있었기 때문에 AppConfig에서 생성자 주입시 인자를 2개로 넘겨줘야함

AppConfig

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

OrderServiceImpl
구현 객체를 Impl에서 만들지 않고, 생성자를 통해서 주입한다.

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

이제부터 Impl는 어떤 구현 객체가 들어올지 알 수 없고, 이는 외부인 AppConfig에서 결정된다.

이러한 형태의 의존 관계를 가지면서, DIP를 위반하지 않는다!
Impl은 추상 (인터페이스)에만 의존하고, 구현 클래스에는 의존하지 않기 때문
객체의 생성과 연결 역할은 AppConfig가 하고, 실행하는 건 Impl이 한다. (관심사의 분리)

DI - 의존 관계 주입

3. App에서의 관심사 분리

테스트하기 위해 만들었던 OrderApp, MemberApp에서도 구현 객체를 직접 생성하지 않고 AppConfig를 통해 꺼낸다

MemberApp

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

OrderApp

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

4. Test 코드에서의 관심사 분리

BeforeEach를 통해 실행 전 AppConfig를 생성하고, 주입해주는 코드

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

그 외 다른 테스트도 이러한 방식으로 수정해준다

정리

  • AppConfig를 통해, 구현체를 생성하고 연결하는 기능을 분리한다.
  • 구현체 주입 방식을 DI (의존관계 주입)이라 한다.
  • ServiceImpl은 실행하는 역할만 수행한다.

AppConfig 리팩토링

현재 AppConfig의 코드에서는 4가지 역할이 분리되어있지 않다는 문제점이 있다
이를 개선해보자

여기서 new MemoryMemberRepository() 부분을 드래그 후 command+option+M > 톱니바퀴 클릭, 후 Name 칸에 memberRepository 적기 > refactor 버튼 클릭

중복이 있는 코드는 알아서 수정해준다.

public class AppConfig {
    public MemberService memberService(){
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
    public OrderService orderService(){
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }
}

그러면 이렇게 리팩토링된다.
MemberRepository 객체를 반환하는 메서드를 따로 뺀 형태이다.
아래 코드에서 MemberService, Repository 역할이 분리 되어 나타낸 것을 볼 수 있다.

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

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

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

여기서 DiscountPolicy를 추가로 리팩토링해보자
FixDiscountPolicy() 객체를 반환하는 메서드를 따로 만들면 된다.

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

MemberService, MemoryMemberRepository, OrderService, DiscountPolicy 등 4가지 역할이 따로 분리되어 있다.
또한 각각 어떤 구현체를 사용하는지 한 눈에 알아보기 쉬워 프로그램 전체를 이해하는데 도움을 준다.
만약 구현체의 변경이 있다면, 객체 생성하는 메서드의 리턴부분만 수정하면 된다.

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

    private static MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository(); // 여기만 수정
    }

    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    public DiscountPolicy discountPolicy(){
        return new FixDiscountPolicy(); // 여기만 수정 
    }
}

AppConfig를 이용한 새로운 할인 정책 적용

할인 정책 구현체를 변경하기 위해서는 이제 AppConfig만 수정하면 된다
AppConfig를 통해서 사용 영역, 객체 생성 및 구성 영역이 분리되었다.

구현체를 생성하고 리턴하는 코드만 (구성영역) 수정하면된다.
더이상 OrderServiceImpl (사용 영역, 클라이언트 코드) 를 수정할 필요가 없.

    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy(); // 구체 클래스 변경
    }


용어 정리

IoC (제어 역전)

기존 프로그램은 클라이언트 객체가 스스로 서버구현 객체를 생성, 연결 및 실행했음
AppConfig가 생긴 이후에는 구현 객체는 실행하는 역할만 담당
제어 흐름 (어떤 걸 쓰고 주는지는 구현 객체는 모름) 권한은 AppConfig가 가짐

IoC == 제어 역전, 제어 흐름을 직접 제어하지 않고 외부에서 관리하는 것

프레임 워크 vs 라이브러리
프레임 워크 - 내가 작성한 코드를 대신 제어, 실행
라이브러리 - 내가 작성한 코드가 직접 제어 흐름 담당

DI (의존관계 주입), 의존 관계

구현 객체는 어떤걸 받을지 모른다.
의존 관계는 정적 클래스 의존 관계, 동적 객체(인스턴스) 의존관계로 나뉨

정적인 클래스 의존관계
import 문을 보고 의존 관계 판단 가능

동적 객체 의존 관계
실행 시점에 결정되는 의존관계로 애플리케이션 실행이 필요

아래에서 할인정책의 경우 실행 시점에 어떤한 정책이 생성되었는지 알 수 있음
== 의존 관계 주입
의존 관계 주입을 사용하면 정적 클래스의 의존관계 (애플리케이션 코드)는 변경할 필요가 없다.

⭐️ AppConfig == IoC 컨테이너 or DI 컨테이너
최근에는 DI 컨테이너로 불린다


스프링으로 전환

위 코드까지는 순수 자바 코드만으로 DI를 적용했다.
Spring으로 DI를 하기 위해서는 SpringContainer에 등록하는 것이 필요하다.
이를 등록하고, AppConfig가 아닌 ApplicationContext를 통해 꺼낸다.

1. Spring bean 등록

AppConfig에서 Configuration, @Bean 어노테이션을 통해 스프링 컨테이너에 빈 등록
key == memberService, value == 리턴되는 객체 인스턴스
이런 식으로 컨테이너에 등록하여, 사용이 필요할 때 getBean을 통해 꺼낸다

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

    @Bean
    private 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(); // 구체 클래스 변경
    }
}

2. 등록된 Bean을 꺼내서 사용
AppConfig에서 꺼내는 것이 아니라, ApplicationContext를 통해서 꺼내야한다.
getBean("가져올이름", 객체명.class)

// 서비스 테스트
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);

마찬가지로 OrderApp에서도 수정해준다

        // 기존 코드
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();
//        OrderService orderService = appConfig.orderService();

        // spring 코드
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

AppConfig를 사용하는 것보다 스프링 컨테이너에 등록하는 방식이 더 길지만, 무수한 장점이 있다 ~

0개의 댓글