sol 개발 블로그 로고
Published on

Spring IoC와 DI 그리고 Bean 정리

Authors
  • avatar
    Name
    Chan Sol OH
    Twitter

목차

이번 포스팅은 객체지향 프로그래밍의 응용을 다루기 때문에 이전 포스팅 객체지향 프로그래밍 익히기 (1)를 보고오면 좋다.

제어 역전 (Inversion of Control ,IOC)

일반적으로 자바는 포함관계를 이용해서 한 클래스의 멤버변수로 다른 클래스의 인스턴스를 멤버변수로 선언한다. 여러 작은 클래스를 큰 클래스에 포함관계로 재사용하면 간결하게 클래스를 작성할 수 있다. 아래는 그 예시 코드다.

Inclusion.java
@RestController
public class NoDIController {
  // 큰 클래스에 여러 작은 클래스가 포함된다.
  private MyService1 service1 = new MyServiceImplement1(); // 다형성을 이용한 코드
  private MyService2 service2 = new MyServiceImplement2();

  @GetMapping("/no-di/hello")
  public Long getHello(){
    Long hello = service1.getHello() + service2.getHello();
    return hello;
  }
}

위와 같이 컨트롤러와 서비스의 포함관계를 사용하는 것도 좋다. 하지만 100개의 컨트롤러와 1000개의 서비스가 존재한다면 매 컨트롤러에 1000개의 서비스 클래스의 인스턴스를 포함시켜야할 것이다. 이런 문제를 해결하기 위해서 스프링은 다르게 동작한다.

스프링 프래임워크는 단위 클래스를 사용할 큰 클래스에서 단위 클래스의 인스턴스를 생성하지 않고, 인스턴스의 관리를 외부에 위임한다. 여기서 외부란 스프링 컨테이너IoC 컨테이너라고 한다. 인스턴스의 관리를 사용하는 클래스가 하는 것이 아니라 외부에 맡겨 제어권이 넘어간 것 을 제어 역전이라고 부른다.

스프링 컨테이너, IoC 컨테이너

스프링 컨테이너는 스프링 프레임워크의 중요한 기능이다. 컨테이너는 객체 생성, 연결, 설정, 관리 등 객체의 생명주기 전반을 관리한다. 컨테이너는 메타 데이터로 작성된 설정에 따라 객체를 생성하고 합친다. 이때 메타 데이터는 XML 문서나 Java 어노테이션, 그리고 Java code로 작성될 수 있다.

스프링은 두개의 컨테이너를 제공한다.

  • Spring BeanFactory Container
    • 객체의 DI를 제공하는 가장 간단한 컨테이너이고, BeanFactory 인터페이스에서 정의된다.
    • BeanFactory 같은 인터페이스는 다른 서드 파티 프레임워크와 호환성을 위해 사용된다.
    • 데이터 볼륨과 속도가 중요한 모바일 장치나 경량 애플리케이션에 권장된다.
  • Spring ApplicationContext Container
    • BeanFactory 보다 더 구체적인 기능을 가진 컨테이너
    • BeanFactory 컨테이너의 모든 기능을 포함하므로 일반적으로 BeanFactory 보다 권장된다.

아래 다이어그램은 스프링이 동작하는 과정을 나타낸다. 사용자가 작성한 클래스들과 메타 데이터에 작성한 설정을 합쳐서 ApplicationContext 같은 컨테이너에 객체를 생성하고 초기화한다. 그 후 시스템에 필요한 곳에 객체를 주입해서 실행 가능한 애플리케이션을 만든다.

ioc 컨테이너가 애플리케이션을 준비한는 도식

Bean 개요

스프링 IoC 컨테이너는 하나 이상의 bean을 가지고 있다. Bean은 컨테이너를 생성할 때 작성한 설정 메타 데이터로 생성된다. bean의 정의는 BeanDefinition 객체에 적혀있다. 여기에는 bean을 구현한 클래스 이름, 클래스의 인스턴스가 생명주기 동안 동작 방식, 의존이나 협력을 위해 다른 bean의 참조, Scope 등등 bean의 이름이나 동작에 관련된 정보가 들어있다.

컨테이너는 bean이 요청됐을 때 BeanDefinition에 저장된 설정 메타 데이터를 사용해서 실제 객체를 생성하기 때문에 bean이란 스프링 IoC 컨테이너에서 관리하는 객체라고 할 수 있다.

클래스의 인스턴스를 bean으로 관리하도록 설정하는 방법은 클래스에 @Component 어노테이션을 작성하거나 Bean Configuration에 직접 Bean 등록할 수 있다.

의존성 주입 (Dependency Injection, DI)

의존성 주입(DI)란 IoC의 방법 중 하나로 사용할 객체를 직접 생성하지 않고 외부 컨테이너에서 생성한 객체를 주입받아 사용하는 것이다.

스프링은 3가지 방법으로 DI를 구현한다.

  1. 생성자를 통한 의존성 주입
  2. 필드 객체 선언을 통한 의존성 주입
  3. setter 메서드를 통한 의존성 주입
constructorDI.java
@RestController
public class DIController {

  MyService myService;
  // 생성자를 통한 의존성 주입
  @Autowired
  public DIController(MyService myService){
    this.myService = myService;
  }
}
fieldDI.java
@RestController
public class DIController {
  // 필드 객체 선언을 통한 의존성 주입
  @Autowired
  MyService myService;
}
setterDI.java
@RestController
public class DIController {

  MyService myService;
  // setter를 통한 의존성 주입
  @Autowired
  public void setMyService(MyService myService){
    this.myService = myService;
  }
}

생성자를 통한 의존성 주입 방식을 쓰는 이유

  1. 명시성과 가독성
    • 생성자에만 의존성 주입 내용이 있기에 클래스의 비즈니스 코드와 분리되어 가독성을 향상시킨다.
  2. 불변성
    • 객체를 생성할 때 한번 의존성을 주입하고 다시 주입할 수 없으므로 주입된 종속성을 불변하게 유지될 수 있도록한다.
  3. 테스트 용이성
    • 유닛 테스트 및 통합 테스트에서 종속성을 쉽게 대체하거나 모의(mock) 객체로 대체할 수 있다,
  4. 컴파일 타임 검사
    • Bean 클래스가 생성자에 필요한 타입인지 컴파일 과정에서 검사할 수 있다.
  5. 의존성 순환 문제 해결
    • 생성자에 매개변수로 Bean이 입력될 수 있다는 것은 레퍼런스 객체가 이미 생성됐다는 것을 의미한다.
    • 서로 다른 두 클래스가 각자의 생성자로 의존성을 주입한다면, 어느 한 클래스의 객체가 생성되지 않으므로 순환 의존성이 생길 수 없다.

      If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.