DI(의존성주입)

2022. 5. 27. 12:27중요개념

DI 개요

DI 의존성 주입이라고도 하며, IoC라고 하는 소프트웨어 디자인 패턴 중 하나다. 이때의 IoC는 인스턴스를 제어하는 주도권이 역전된다는 의미로 사용되는데, 컴포넌트를 구성하는 인스턴스의 생성 의존 관계의 연결 처리를 해당 소스코드가 아닌 DI 컨테이너에서 대신해주기 때문에 제어가 역전됐다고 보는 것이다.

 

DI 컨테이너에서 인스턴스를 관리하는 방식에는 다음과 같은 장점이 있다.

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

 

 

 

 

DI 컨테이너(ApplicationContext) 와 빈 정의 방법

[Configuration Class 작성하기]

DI 컨테이너를 생성하려면, 먼저 설정 클래스(Configuration Class)를 작성해야 한다. 아래의 AppConfig클래스는 DI 컨테이너 설정 파일 역할을 하며, 자바로 작성돼 있어서 Java Configuration Class라고도 한다. 그리고 이렇게 Java Configuration Class로 설정하는 방식을 자바 기반 설정 방식이라고 하며, 다음과 같이 작성한다.

 

  • 자바 기반 설정 방식의 예
@Configuration
public class AppConfig {
    @Bean
    UserRepository userRepository() {
        return new UserRepositoryImpl();
    }
    
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    UserService userService() {
        return new UserServiceImpl(userRepository(), passwordEncoder());
    }
}

 

@Configuration 애너테이션을 붙여 Configuration Class임을 나타내고, 메서드 위에 @Bean 애너테이션을 붙여 DI 컨테이너 Bean(빈)으로 등록한다.

 

위와 같이 @Configuration @Bean 애너테이션을 사용해서 DI 컨테이너에 컴포넌트를 등록하면 애플리케이션은 DI 컨테이너에 있는 빈(Bean) ApplicationContext 인스턴스를 통해 가져올 수 있다. DI 컨테이너에서 을 가져오는 행위는 '룩업(lookup)'이라고 한다.

 

 

 

 

 

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

DI 컨테이너에서 을 가져오는 방법에는 세 가지 유형이 있다. 다음 예를 살펴보자.

 

  •  룩업하는 세 가지 예시
UserService userService = context.getBean(UserService.class);                    --- (1)
UserService userService = context.getBean("userService", UserService.class);     --- (2)
UserService userService = (UserService) context.getBean("userService");          --- (3)

 

(1): 가져오려는 빈의 타입(Type)을 지정하는 방법이다. 지정한 타입에 해당하는 빈이 DI 컨테이너에 오직 하나만 있을 때 사용한다.

(2): 가져오려는 빈의 이름과 타입을 지정하는 방법이다. 지정한 타입에 해당하는 빈이 DI 컨테이너에 여러 개 있을 때 이름으로 구분하기 위해 사용한다.

(3): 가져오려는 빈의 이름을 지정하는 방법이다. 반환값이 Object 타입이라서 원하는 빈의 타입으로 형변환해야 한다.

 

 

 

 

 

[빈 설정 방법]

한편 빈을 설정하는 방법에도 몇 가지 유형이 있는데 대표적인 방법은 아래의 표와 같다.

 

방법설명

자바 기반 설정 방식
(Java-based configuration)
자바 클래스에 @Configuration 애너테이션을, 메서드에 @Bean 애너테이션을 사용해 빈을 정의하는 방법. 최근에는 스프링 기반 애플리케이션 개발에 자주 사용되고 특히 스프링 부트에서 이 방식을 많이 활용한다.
XML 기반 설정 방식
(XML-based configuration)
XML 파일을 사용하는 방법으로 <bean> 요소의 class 속성에 FQCN(Fully-Qualified Class Name)을 기술하면 빈이 정의된다. <constructor-arg> <property> 요소를 사용해 의존성을 주입한다.
애너테이션 기반 설정 방식
(Annotation-based configuration)
@Component 같은 마커 애너테이션(Marker Annotation)이 부여된 클래스를 탐색해서(Component Scan) DI 컨테이너에 빈을 자동으로 등록하는 방법이다.

 

 

 

 

[ApplicationContext를 생성하는 다양한 방법]

ApplicationContext에는 위와 같은 다양한 설정 방식을 지원하기 위한 구현 클래스가 준비되어 있다. 다음 예를 살펴보자.

 

  • ApplicationContext 생성 예시
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);       --- (1)
ApplicationContext context = new AnnotationConfigApplicationContext("com.example.app");     --- (2)
ApplicationContext context = 
    new ClassPathXmlApplicationContext("META-INF/spring/applicationContext.xml");           --- (3)
ApplicationContext context = 
    new FileSystemXmlApplicationContext("./spring/applicationContext.xml");                 --- (4)

 

(1): 자바 기반의 설정 방식으로 AnnotationConfigApplicationContext의 생성자에 @Configuration 애너테이션이 붙은 클래스를 인수로 전달한다.

(2): 애너테이션 기반의 설정 방식으로 AnnotationConfigApplicationContext의 생성자에 패키지명을 인수로 전달한다. 지정된 패키지 이하의 경로에서 컴포넌트를 스캔한다.

(3): XML 기반의 설정 방식으로 ClassPathXmlApplicationContext의 생성자에 XML 파일을 인수로 전달한다. 경로에 접두어(Prefix)가 생략된 경우에는 클래스패스 안에서 상대 경로로 설정 파일을 탐색한다.

(4): XML 기반의 설정 방식으로 FileSystemXmlApplicationContext의 생성자에 XML 파일을 인수로 전달한다. 경로에 접두어(Prefix)가 생략된 경우에는 JVM의 작업 디렉터리 안에서 상대 경로로 설정 파일을 탐색한다.

 

 

 

 

 

빈 설정

[자바 기반 설정 방식]

자바 기반 설정 방식에서는 자바 코드로 빈을 설정한다. 이때 사용되는 자바 클래스를 Java Configuration Class라고 한다.

 

 

  • 자바 기반 설정 방식으로 빈 정의
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// 기타 임포트 문은 생략

@Configuration    --- (1)
public class AppConfig {
    @Bean     --- (2)
    UserRepository userRepository() {
        return new UserRepositoryImpl();
    }
    
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    UserService userService() {
        return new UserServiceImpl(userRepository(), passwordEncoder());  --- (3)
    }
}

 

(1): 클래스에 @Configuration 애너테이션을 붙여 설정 클래스를 선언한다. 설정 클래스는 여러 개 정의할 수 있다.

(2): 메서드에 @Bean 애너테이션을 부여해서 빈을 정의한다. 메서드명이 빈의 이름이 되고 그 빈의 인스턴스가 반환값이 된다. 이 예제에서는 userRepository가 빈의 이름이다. 만약 빈 이름을 다르게 명시하고 싶다면 @Bean(name = "userRepo")와 같이 name 속성에 빈의 이름을 재정의하면 된다.

(3): 다른 컴포넌트를 참조해야 할 때는 해당 컴포넌트의 메서드를 호출한다. 의존성 주입이 프로그램적인 방법으로 처리된다.

 

 

 

 

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

 

  • 메서드의 매개변수를 통한 의존성 주입
@Bean
UserService userService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
	return new UserServiceImpl(userRepository, passwordEncoder);
}

 

자바 기반 설정 방식만 사용해서 빈을 설정할 때는 애플리케이션에서 사용되는 모든 컴포넌트를 빈으로 정의해야 한다. 다만 뒤에 설명할 애너테이션 기반 설정 방식과 조합하면 설정 내용의 많은 부분을 줄일 수 있다.

 

 

 

 

[XML 기반 설정 방식]

XML 기반의 설정 방식은 XML 파일을 이용해 빈을 설정한다. 다음 예를 살펴보자.

 

  • XML 기반 설정 방식으로 빈 정의
<?xml version="1.0" encoding="UTF-8"?>
<beans
	xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    	http://www.springframework.org/schema/beans/spring-beans.xsd
    	http://www.springframework.org/schema/context
    	http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
    <bean id="userRepository" class="com.example.demo.UserRepositoryImpl" />
    <bean id="passwordEncoder" class="com.example.demo.BCryptPasswordEncoder" />
    <bean id="userService" class="com.example.demo.UserServiceImpl">
    	<constructor-arg ref="userRepository" />
    	<constructor-arg ref="passwordEncdoer" />
	</bean>
</beans>

 

 

XML 기반 설정 방식을 사용할 때도 앞의 자바 기반 설정 방식과 마찬가지로 XML만으로 설정하려 하면 애플리케이션에서 사용하는 모든 컴포넌트의 빈을 정의해야 하는 번거로움이 있다. 그래서 자바 기반 설정 방식에서와 같이 뒤에 설명할 애너테이션 기반 설정 방식과 조합해서 사용하는 것이 일반적이다.

 

 

 

 

참고로 의존성 주입에서 주입할 대상이 다른 빈이 아니라 특정 값인 경우, ref 속성을 사용하지 않고 value 속성을 사용한다.

 

생성자의 인수에 빈이 아닌 특정 값 주입

<constructor-arg value="wikibook" />
<constructor-arg value="secret" />

 

XML 파일의 value 속성에 문자열을 지정한다고 해서 실제 자바 코드에서 그 값을 받는 타입이 반드시 문자열 타입일 필요는 없다. XML 파일에는 문자열로 기재돼 있더라도 필요한 경우 DI를 하는 과정에서 숫자나 날짜 타입, 혹은 파일(File)이나 프로퍼티(Properties) 같은 여러 가지 타입으로 형변환할 수 있다.

 

 

 

 

[애너테이션 기반 설정 방식]

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

 

 

  • 애너테이션 기반 설정 방식으로 빈 정의(UserRepositoryImpl.java)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
// 기타 임포트 문은 생략

@Component
public class UserRepositoryImpl implements UserRepository {
    // 생략
}

 

 

  • 애너테이션을 활용한 빈 설정(BCryptPasswordEncoder.java)
@Component
public class BCryptPasswordEncoder implements PasswordEncoder {
	// 생략
}

 

Bean 클래스 @Component 애너테이션을 붙여 컴포넌트 스캔이 되도록 만든다.

 

 

 

  • 애너테이션을 활용한 빈 설정(UserServiceImpl.java)
@Component
public class UserServiceImpl implements UserService {
	@Autowired
    public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        // 생략
    }
}

 

생성자에 @Autowired 애너테이션을 부여해서 Autowiring되도록 만든다. Autowiring을 사용하면 기본적으로 주입 대상과 같은 타입의 빈을 DI 컨테이너에서 찾아 Wiring 대상에 주입하게 된다.

 

 

 

 

컴포넌트 스캔을 수행할 때는 스캔할 범위를 지정해야 하는데 설정 방식으로는 자바 기반 설정 방식이나 XML 기반 설정 방식을 사용할 수 있다.

 

  • 자바 기반 설정 방식으로 컴포넌트 스캔 범위를 설정
import org.springframework.context.annotation.ComponentScan;
// 기타 임포트문 생략

@Configuration
@ComponentScan("com.example.demo")
public class AppConfig {
    // 생략
}

컴포넌트 스캔이 활성화되도록 클래스에 @ComponentScan 애너테이션을 부여한다. 애너테이션의 value속성이나 basePackages 속성에 컴포넌트를 스캔할 패키지를 지정한다. 이 예제의 경우 com.example.demo 패키지 이하의 범위에서 클래스를 스캔하고, 스캔 대상이 되는 애너테이션이 부여된 클래스를 DI 컨테이너에 자동으로 등록한다. 이 속성을 생략할 경우 설정 클래스가 들어있는 패키지 이하를 스캔한다.

 

 

  • XML 기반 설정 방식으로 컴포넌트 스캔 범위를 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans
	xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    	http://www.springframework.org/schema/beans/spring-beans.xsd
    	http://www.springframework.org/schema/context
    	http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
    <context:component-scan base-package="com.example.demo" />
</beans>

DI 컨테이너에 등록되는 빈의 이름은 기본적으로 클래스명의 첫 글자를 소문자로 바꾼 이름과 같다. 단, 첫 글자 이후에 대문자가 연속되는 경우에는 첫 글자를 소문자로 변환하지 않고 클래스명이 그대로 빈 이름으로 사용된다.

 

 

 

만약 빈 이름을 명시적으로 지정하고 싶다면 @Component 애너테이션에 원하는 이름을 넣어주면 된다.

 

애너테이션 기반 설정 방식에서 빈 이름 명시

@Component("userService")
public class UserServiceImpl implements UserService {
	// 생략
}

출처:https://kgvovc.tistory.com/43