(스프링) IoC, DI, 컨테이너

UiJu Hong / 홍의주·2024년 4월 19일

스프링

목록 보기
1/2
post-thumbnail

제어의 역전 IoC(Inversion of Control)

: 쉽게 말하면 Config 클래스가 프로그램에 대한 제어 흐름을 가져가는 것이다. 예를들어 Impl 클래스가 직접 로직을 수행하지만 호출하는 인터페이스의 어떤 구현 객체를 실행시킬지 제어하는 것이다.

이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)라고 하며, 이는 객체지향 설계의 5가지 원칙 중 대표적으로 SRP, DIP, OCP를 지키기 위함이다.


✏️ SRP 단일 책임 원칙
: 한 클래스는 하나의 책임만 가져야 한다.

  • 구현 객체를 생성하고 연결하는 책임은 Config가 담당
  • 클라이언트 객체(ex Impl클래스)는 실행하는 책임만 담당
  • SRP 단일 책임 원칙을 따르면서 관심사를 분리함

[역할과 구현의 분리]
: 역할과 구현으로 구분하면 세상이 단순, 유연해지며 변경도 편리해진다.

클라이언트는 대상의 역할만 알면 된다.
클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
클라이언트는 구현 대상의 내부 구조가 변경되어도, 대상 자체를 변경해도 영향 받지 않는다.

자바에서도 이런 특징을 활용한다.

  • 역할 = 인터페이스
  • 구현 = 인터페이스를 구현한 클래스, 구현 객체

객체를 설계할 때 역할과 구현을 명확히 분리한다. 객체를 설계할 때 역할(인터페이스)를 먼저 부여하고, 그 역할을 수행하는 구현 객체를 만든다.

✏️ DIP 의존관계 역전 원칙
: 추상화가 아닌 구체화에 의존하면 안된다. 의존성 주입은 이 원칙을 따르는 방법 중 하나이다.

public class OrderServiceImpl implements OrderService {
 //  DiscountPolicy 인터페이스의 구현객체 변경으로 코드 수정해야함. Fix => Rate
 //  private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
 
     private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
 }

이는 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존하고 있다. 따라서 DIP위반, 변경해야 하기 때문에 OCP 모두 위반한다. 인터페이스에만 의존하도록 변경하면 아래와 같다.

애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 Config 클래스를 만든 뒤, 생성자 주입을 사용한다.
빈 Bean 활용을 통한 설정은 아래에 추가적으로 소개하고, 순수 코드로 작성하였다.

public class AppConfig {
     //FixDiscountPolicy 구현체 사용
     public OrderService orderService() {
         return new OrderServiceImpl(new FixDiscountPolicy());
     }
}
public class OrderServiceImpl implements OrderService {
     private final DiscountPolicy discountPolicy;
     
     public OrderServiceImpl(DiscountPolicy discountPolicy) {
         this.memberRepository = memberRepository;
         this.discountPolicy = discountPolicy;
     }
}

AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의 존관계를 주입했다. 이렇게해서 DIP 원칙을 따르면서 문제도 해결했다.

✏️ OCP 개방 폐쇄의 원칙
: 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다

  • 다형성을 사용하고 클라이언트가 DIP를 지킨다.
  • 애플리케이션을 사용 영역과 구성 영역으로 나눈다.
  • 위의 예시에서 AppConfig가 의존관계를 FixDiscountPolicy RateDiscountPolicy 로변경해서 클라이언트코드에 주입하므로 클라이언트 코드는 변경하지 않아도 되므로, 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다!

의존관계 주입 DI(Dependency Injection)

: 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.

✏️ 정적인 클래스 의존관계
클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있다. 그러나 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 주입될지 알 수 없다.

✏️ 동적인 객체 인스턴스 의존관계
: 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.

애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 그 참조값을 전달해서 연결하는데, 이렇게 클라이언트에 전달해서 서버와 실제 의존관계가 연결되는 것을 의존관계 주입이라 한다.
즉, 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경 할 수 있다.


DI 컨테이너 (IoC 컨테이너)

: Config 설정 클래스처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 DI 컨테이너라고 한다. 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.

[스프링으로 전환하기]
위에서는 순수한 자바 코드만으로 DI를 적용했다. 스프링을 사용하면 다음과 같이 수정할 수 있다. 아래와 같이 하면 스프링 컨테이너에 스프링 빈으로 등록한다.

@Configuration
public class AppConfig {

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(discountPolicy());
    }
    
    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이 라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록하며, 이를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean 이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다. 스프링 컨테이너에 객체를 스프링 빈으로 등록 하고, 스프링 빈을 찾아서 사용할 수 있다.

📍 스프링 컨테이너를 통 해서 필요한 스프링 빈(객체)를 찾는 법

ApplicationContext applicationContext 
		= new AnnotationConfigApplicationContext(AppConfig.class);

OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
    

ApplicationContext 를 스프링 컨테이너라 하며, applicationContext.getBean() 메서드를 사용 해서 찾을 수 있다.

0개의 댓글