본문 바로가기

공부/Spring

스프링 - DI 2

스프링 IoC/DI - 제어의 역전/의존성 주입 02

의존성 주입 or 인젝션(Injection) : 어떤 클래스가 필요로 하는 컴포넌트를 외부에서 생성한 후, 내부에서 사용 가능하게 만들어 주는 과정

1. DI 컨테이너 : 의존성 주입을 자동으로 처리하는 기반

이전 예제에서의 타이어를 장착하려면 DI 컨테이너에서 꺼내서 장착하면 된다.

ApplicationContext context = ...; // 스프링 DI 컨테이너
Tire tire = context.getBean(KoreaTire.class);
sequenceDiagram
    인스턴스A->>+DI컨테이너: 취득
    DI컨테이너->>+인스턴스B: 생성
    인스턴스B-->>-DI컨테이너: .
    DI컨테이너->>-인스턴스A: 사용            

DI 컨테이너에서 인스턴스를 관리하는 방식의 장점

  • 인스턴스의 스코트를 제어할 수 있다.
  • 인스턴스의 생명 주기를 제어할 수 있다.
  • AOP 방식으로 공통 기능을 집어넣을 수 있다.
  • 의존하는 컴포넌트 간의 결합도를 낮춰서 단위 테스트하기 쉽게 만든다.

2. ApplicationContext와 빈 정의

스프링 프레임워크에서는 ApplicationContext가 DI컨테이너의 역할을 한다.

// 1. 설정 클래스(configuration class)를 인수로 전달하고 DI 컨테이너를 생성한다. 
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

// 2. DI 컨테이너에서 인스턴스를 가져온다.
Tire tire = context.getBean(KoreaTire.class);

설정 클래스 : https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html

@Target(value=TYPE)
@Retention(value=RUNTIME)
@Documented
@Component
public @interface Configuration

@Configuration 과 @Bean 어노테이션을 사용해서 DI 컨테이너에 컴포넌트를 등록한다.

설명 : Indicates that a class declares one or more @Bean methods and may be processed by the Spring container to generate bean definitions and service requests for those beans at runtime, for example:

자바 기반 설정 방식
ex) AppConfig 클래스는 DI컨테이너에서 설정 파일 역할을하며, 자바 컨피규레이션 클래스라고도 한다.

@Configuration
public class AppConfig {
    @Bean
    public MyBean myBean() {
        // instantiate, configure and return bean ...
    }
}

설명 : @Configuration classes are typically bootstrapped using either AnnotationConfigApplicationContext or its web-capable variant, AnnotationConfigWebApplicationContext. A simple example with the former follows:

ex)

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class);
ctx.refresh();
MyBean myBean = ctx.getBean(MyBean.class);
// use myBean ...

애플리케이션과 ApplicationContext의 관계

  • 스프링 프레임워크에서는 DI컨테이너에 등록하는 컨포넌트를 빈이라고 한다.
  • 이 빈에 대한 설정(Configuration) 정보를 '빈 정의(Bean Definition)' 라고 한다.
  • DI 컨테이너에서 빈을 찾아오는 행위를 '룩업(lookup)' 이라고 한다.
sequenceDiagram
    Bean(Tire)->>Bean(Car): DI
    Bean(Tire)->>DI컨테이너: 컴포넌트 등록
    Bean(Car)->>DI컨테이너: 컴포넌트 등록
    DI컨테이너-->ApplicationContext: 스프링 프레임워크
    자바 애플리케이션->>+ApplicationContext: 빈 찾기(getBean)
    Note left of 자바 애플리케이션: Bean
    ApplicationContext-->>-자바 애플리케이션: .

DI컨테이너에서 빈 가져오기

Car car = context.getBean(Car.class); // --------1
Car car = context.getBean("car", Car.class); // -2
Car car = (Car)context.getBean("car"); // -------3
  1. 가져오려는 빈의 타입을 지정함 (지정 타입에 해당하는 빈이 DI컨테이너에 오직 하나만 있을 때 사용한다.)
  2. 빈의 이름과 타입을 지정하는 방법
  3. 빈의 이름을 지정하는 방법 (반환값이 Object 타입이여서 형변환이 필요하다.)

*빈을 설정하는 방법

  • 자바 기반 설정 방식
    : 자바 클래스에 @Configuration 어노테이션을 메소드에 @Bean 어노테이션을 사용해 빈을 정의하는 방법
    : 스프링 부트에서 이 방법을 많이 사용한다.
  • XML 기반 설정 방식
    : XML 파일을 사용하는 방법 <bean> 요소 사용 ..
  • 어노테이션 기반 설정 방식
    : @Component 같은 마커 어노테이션이 부여된 클래스를 탐색해서 DI 컨테이너에 빈을 자동으로 등록하는 방법

3. 빈 설정

3.1. 자바 기반 설정 방식

  • 자바 코드로 빈을 설정한다. = 자바 컨피규레이션 클래스
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration // ---------------------------------------- 1.
public class AppConfig {
    @Bean // --------------------------------------------- 2.
    TireRepository tireRepository(){
        return new TireRepositoryImpl();
    }

    @Bean
    CarService carService(){
        return new CarServiceImpl(tireRepository()); // -- 3.
    }
}
  1. 클래스에 @Configuration 어노테이션을 붙여 설정 클래스를 선언한다. 여러 개 정의 할 수 있다.
  2. 메소드에 @Bean 어노테이션을 부여해서 빈을 정의한다. 메소드명이 빈의 이름이 되고 그 빈의 인스턴스가 반환값이 된다.
    : @Bean(name="tireRepo") 재정의 가능
  3. 다른 컴포넌트를 참조해야 할 떄는 컴포넌트의 메소드를 호출한다. 의존성 주입이 프로그램적인 방법으로 처리된다.

*자바 기반 설정 방식에서는 메소드에 매개변수를 추가하는 방법으로 다른 컴포넌트의 의존성을 주입할 수 있다.
단, 인수로 전달될 인스턴스에 대한 빈은 별도로 정의돼 있어야한다.

@Bean
CarService carService(TireRepository tireRepository){ // 매개변수
    return new CarServiceImpl(tireRepository);
}

*자바 기반 설정 방식만 사용해서 빈을 설정할 떄는 모든 컴포넌트를 빈으로 정의해야 하므로 어노테이션 기반 설정 방식과 조합하면 설정 내용의 많은 부분을 줄일 수 있다.

3.2. 어노테이션 기반 설정 방식

빈을 정의하는 어노테이션을 빈의 클래스에 부여하는 방식을 사용한다.
이후 이 어노테이션이 붙은 클래스를 탐색해서 DI컨테이너에 자동으로 등록하는데 이러한 탐색 과정을 컴포넌트 스캔(Component Scan)이라고 한다. 또한 의존성 주입도 이제까지처럼 명시적으로 설정하는 것이 아니라 어노테이션이 붙어 있으면 DI컨테이너가 자동으로 필요로 하는 의존 컴포넌트를 주입하게 한다. 이러한 주입 과정을 오토와이어링이라 한다.

@Component // ----- 1
public class TireRepositoryImpl implements TireRepository{
    // 생략
}
  1. 빈 클래스에 @Component 어노테이션을 붙여 컴포넌트 스캔이 되도록 만든다.
    : @Component("tireRepo) 와 같이 빈이름 정의 가능함
@Component
public class CarServiceImpl implements CarService{
    @Autowired // -- 2
    public CarServiceImpl(TireRepository tireRepository){
        // 생략
    }
}
  1. 생성자에 @Autowired 어노테이션을 부여해서 오토와이어링 되도록 만든다. 기본적으로 주입 대상과 같은 타입의 빈을 DI컨테이너에서 찾아 와이어링 대상에 주입하게 된다.

4. 의존성 주입

  • 설정자 기반 의존성 주입 방식
    : 설정자 메소드의 인수를 통해 의존성을 주입하는 방식이다. = 세터 인젝션
  • 생성자 기반 의존성 주입 장식
    : 생성자의 인수를 사용해 의존성을 주입하는 방식 = 컨스트럭터 인젝션
  • 필드 기반 의존성 주입 방식
    : 생성자나 설정자 메소드를 사용하지 않고 DI컨테이너의 힘을 빌려 의존성을 주입하는 방식 = 필드 인젝션
    : 의존성을 주입하고 싶은 필드에 @Autowired 어노테이션 추가
    주의점 : 반드시 DI컨테이너를 사용한다는 것을 전제해야 함

5. 오토와이어링(autowiring)

@Autowired 어노테이션을 이용하면 설정자 메소드를 이용하지 않고도 주입이 가능하다.
@Autowired 는 type 기준 매칭이다.

사용 예 :

예1) 안되는 경우.

KoreaTire.java

@Component
public class KoreaTire implements Tire {

    @Override
    public String getBrand() {
        return "코리아 타이어!!";
    }
}

Car.java

@Component
public class Car {
    Tire tire;
    public String getTireBrand(){
        return "장착 타이어 : " + tire.getBrand();
    }
}

실행

Car car = applicationContext.getBean(Car.class);
System.out.println(car.getTireBrand());

결과 : Car 에서 Tire 객체가 주입되지 않으므로 NullPointerException 발생

Exception in thread "main" java.lang.NullPointerException
    at com.soon.ex05.Car.getTireBrand(Car.java:12)
    at com.soon.SoonSpringApplication.main(SoonSpringApplication.java:18)

예2) 되는 경우.

Car.java

import org.springframework.beans.factory.annotation.Autowired;

@Component
public class Car {

    @Autowired
    Tire tire;

    public String getTireBrand(){
        return "장착 타이어 : " + tire.getBrand();
    }
}

실행 결과

장착 타이어 : 코리아 타이어!!

예2 번에서 Car 에서 Tire를 주입하였는데, 왜 코리아 타이어가 나타난 것인가?

// Car.java 
@Autowird Tire tire;

// KoreaTire.java
@Component public class KoreaTire implements Tire

인터페이스의 구현 여부 답이다.
스프링의 @Autowired 는 type 기준으로 매핑되기 때문이다.

@Autowired를 통한 속성 매칭 규칙

  1. type을 구현한 빈이 있는가?
    No : No matching bean 에러
    Yes : 2번으로 이동
  2. 빈이 한 개 인가?
    No : 3번으로 이동
    Yes : 완료 - 유일한 빈을 객체에 할당
  3. id가 일치하는 하나의 빈이 있는가?
    No : No unique bean 에러
    Yes : 완료 - 유일한 빈을 객체에 할당

예3) bean의 id매칭보다 type매칭이 우선이다.

KoreaTire.java

@Component("korTire")
public class KoreaTire implements Tire {

    @Override
    public String getBrand() {
        return "코리아 타이어!!";
    }
}

Car.java

@Component
public class Car {

    @Autowired
    Tire tire;

    public String getTireBrand(){
        return "장착 타이어 : " + tire.getBrand();
    }
}

실행 결과

장착 타이어 : 코리아 타이어!!

예4) type 매칭 안되는 경우 - 위 예2 에서 추가적으로 AmericaTire 가 있다면 위 실행 결과는?

AmericaTire.java 추가

@Component
public class AmericaTire implements Tire {
    @Override
    public String getBrand() {
        return "아메리카 타이어!";
    }
}

실행 결과

Field tire in com.soon.ex05.Car required a single bean, but 2 were found:
    - americaTire: defined in file [/Users/soon/app/git_workspace/soon_spring/out/production/classes/com/soon/ex05/AmericaTire.class]
    - koreaTire: defined in file [/Users/soon/app/git_workspace/soon_spring/out/production/classes/com/soon/ex05/KoreaTire.class]

Action:

Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

Process finished with exit code 1

둘 다 똑같은 인터페이스 타입(Tire.java)을 구현하고 있다면 id를 명시한다.

예5) 아메리카 타이어에 id = "tire"로 설정 후 실행

@Component("tire")
public class AmericaTire implements Tire {
    @Override
    public String getBrand() {
        return "아메리카 타이어!";
    }
}

실행 결과

장착 타이어 : 아메리카 타이어!

예6) Car에서 원하는 빈 id를 설정 후 실행

@Qualifier 를 이용하여 주입하려는 빈의 id를 명시한다.

@Component("korTire")
public class KoreaTire implements Tire {

    @Override
    public String getBrand() {
        return "코리아 타이어!!";
    }
}

Car.java

@Component
public class Car {

    @Qualifier("korTire") // bean id 명시함
    @Autowired
    Tire tire;

    public String getTireBrand(){
        return "장착 타이어 : " + tire.getBrand();
    }
}

실행 결과

장착 타이어 : 코리아 타이어!!

6. 컴포넌트 스캔

기본 설정으로 컴포넌트 스캔하기

별도의 설정이 없는 기본 설정에서는 다음과 같은 어노테이션이 붙은 클래스가 탐색 대상이 되고, 탐색된 컴포넌트는 DI컨테이너에 등록된다.

  • @Component
    : 아래에 해당되지 않는 클래스에 붙임
  • @Controller
    : MVC 패턴에서의 컨트롤러 역할을 하는 컴포넌트에 붙임
    : 클라이언트에서 오는 요청을 받고, 비즈니스 로직의 처리결과를 응답으로 돌려보내는 기능을 한다.
  • @Service
    : 비즈니스 로직을 처리하는 컴포넌트에 붙임
  • @Repository
    : 영속적인 데이터 처리를 수행하는 컴포넌트에 붙임
  • @Configuration
  • @RestController
  • @ControllerAdvice
  • @ManagedBean
  • @Named

컴포넌트 스캔을 하기 위해 @ComponentScan 어노테이션을 사용한다.
컴포넌트 스캔을 할 때는 클래스 로더에서 위와 같은 어노테이션이 분은 클래스를 찾아야 하기 때문에 탐색범위가 넒고 처리하는 시간도 오래 걸린다.

범위가 넓은 경우

@ComponentScan(basePackages = "com")
@ComponentScan(basePackages = "com.example")

통상 애플리케이션의 최상위나 한 단계 아래의 패키지까지만 스캔 대상으로 잡는 것이 적절하다.

범위가 적절한 경우

@ComponentScan(basePackages = "com.example.demo")
@ComponentScan(basePackages = "com.example.deom.app")

7. 빈 스코프

DI 컨테이너는 빈 간의 의존 관계를 관리할 뿐만 아니라 빈의 생존 기간도 관리한다.
빈의 생존 기간을 빈 스코프라고 하는데 개발자가 직접 빈의 스코프를 다루지 않아도 된다.

스코프 종류

  • singleton
    : DI컨테이너를 기동할 떄 빈 인스턴스 하나가 만들어지고, 이후부터는 그 인스턴스를 공유하는 방식
    : 기본 스코프이기 떄문에 별도로 스코프를 지정하지 않았다면 singleton으로 간주한다.
  • prototype
    : DI컨테이너에 빈을 요청할 때마다 새로운 빈 인스턴스가 만들어진다. 멀티 스레드 환경에서 오동작이 발생하지 않아야 하는(thread-safe) 빈인 경우 사용한다.
  • request
    : HTTP 요청이 들어올 떄마다 새로운 빈 인스턴스가 만들어진다. 웹 애플리케이션을 만들 떄만 사용할 수 있다.
  • session
    : HTTP 세션이 만들어질 떄마다 새로운 빈 인스턴스가 만들어진다. 웹 애플리케이션을 만들 떄만 사용할 수 있다.

스코프 설정

빈을 정의하는 단계에서 스코프를 명시해야 한다.
자바 기반 방식은 @Bean 어노테이션이 붙은 메소드에 @Scope 어노테이션을 추가한다.
어노테이션 기반 방식은 스캔 대상이 되는 클래스에 @Scope 어노테이션을 추가한다.

// 자바 기반
@Bean
@Scope("prototype")
TireService tireService(){
    return new TireServiceImpl();
}

아래의 인스턴스는 서로 다르다.

TireService tireService1 = context.getBean(TireService.class);
TireService tireService2 = context.getBean(TireService.class);
@Component
@Scope("prototype")
public class tireServiceImpl implements TireService{
    // ...
}

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

스프링 인터셉터 설정  (0) 2019.08.06
스프링 트랜잭션  (0) 2019.08.06
스프링 - DI  (0) 2019.07.23
스프링부트 아파치 mod_jk 연동하기  (0) 2019.07.14
[Boot] JPA 네이밍 전략  (0) 2019.03.11