Spring IoC와 DI에 대해 정리.

토비의 스프링 3.1과 스프링 철저입문 도서, 타 블로그의 정리 내용을 보고 정리했습니다.

책 위주로 정리한 내용이고 풀어서 정리되어있기 때문에 간단한 정리는 타 블로그 참고를 추천드립니다.

 

Reference

  • 토비의 스프링 3.1
  • 스프링 철저 입문

 

IoC와 DI에 대한 정리는 아래와 같은 순서로 진행.

  1. IoC Container란
  2. IoC Container의 종류와 사용방법
  3. Web Application의 IoC Container 구성
  4. Bean 설정 방식, DI 방식
  5. Autowiring, ComponentScan
  6. Bean Scope
  7. Bean 설정 분할과 profile별 설정
  8. Bean 생명주기, IoC Container 종료

 

아래 순서로 정리한다.

1. Bean 설정 방식

2. DI 방식

 

 

 

1. Bean 설정 방식

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

 

● 자바 기반 설정 방식(Java-based Configuration)

자바 클래스에 @Configuration   Annotation을, 메소드에 @Bean   Annotation을 사용해 Bean을 정의하는 방법으로 스프링 프레임워크 3.0.0부터 사용할 수 있다.

최근에는 스프링 기반 애플리케이션 개발에 자주 사용되고 특히 스프링부트에서 이 방식을 많이 사용한다.

 

● XML 기반 설정 방식(XML-based Configuration)

XML 파일을 사용하는 방법으로 <bean> 요소의 class 속성에 FQCN(Fully-Qualified Class Name)을 기술하면 Bean이 정의된다.

<constructor-arg>나 <property> 요소를 사용해 의존성을 주입한다. 스프링 1.0.0부터 사용할 수 있다.

 

● 어노테이션 기반 설정 방식(Annotation-based Configuration)

@Component 같은 마커 어노테이션(Maker Annotation)이 부여된 클래스를 탐색해서(Component Scan) IoC 컨테이너에 Bean을 자동으로 등록하는 방법이다. 스프링 2.5부터 사용할 수 있다.

 

 

하나의 방법만 사용해서 빈을 정의할 수도 있지만 대부분의 경우 자바 기반 + 어노테이션 기반을 조합하거나

XML 기반 + 어노테이션 기반을 조합해 사용한다.

 

그럼 이 설정 방식들에 대해 정리한다.

 

 

자바 기반 설정 방식(Java-based Configuration)

 

자바 기반 설정 방식에서는 자바 코드로 Bean을 설정한다.

이때 사용되는 자바 클래스를 Java Configuration Class라고 부른다.

 

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
....

//클래스에 @Configuration을 붙여 설정 클래스라고 선언한다. 설정 클래스는 여러개 정의할 수 있다.
@Configuration
public class AppConfig{
    
    /*
        메소드에 @Bean을 붙여 Bean을 정의한다.
        메소드명이 Bean의 이름이 되고 그 Bean의 인스턴스가 반환값이 된다.
        여기서는 userRepository가 Bean의 이름이된다.
        만약 다르게 하고 싶다면 
        @Bean(name="userRepo") 이렇게 어노테이션에서 속성을 재정의하면 된다.
    */
    @Bean
    UserRepository userRepository(){ //Bean의 이름 userRepository
        return new UserRepositoryImpl(); //Bean의 인스턴스 = UserRepositoryImpl
    }
    
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    UserService userService(){
        /*
            다른 컴포넌트를 참조해야 할 때는 해당 컴포넌트의 메소드를 호출한다.
            의존성 주입이 프로그램적인 방법으로 처리된다.
        */
        return new UserServiceImpl(userRepository(), passwordEncoder());
    }
    
    /*
        자바 기반 설정 방식에서는 메소드에 매개변수를 추가하는 방법으로
        다른 컴포넌트의 의존성을 주입할 수 있다.
        단, 인수로 전달될 인스턴스에 대한 Bean은 별도로 정의해야 한다.
    */
    @Bean
    UserService userService(UserRepository userRepository, PasswordEncoder passwordEncoder){
        return new UserServiceImpl(userRepository, passwordEncoder);
    }
    
    ...
}

자바 기반 설정 방식만 사용해서 Bean을 설정할 때는 애플리케이션에서 사용되는 모든 컴포넌트를 Bean으로 정의해야 한다.

어노테이션 방식과 조합하면 설정 내용의 많은 부분들을 줄일 수 있어서 조합해서 사용하면 편해진다.

 

 

XML 기반 설정 방식(XML-based Configuration)

 

<?xml version="1.0" encoding="UTF-8"?>
<!-- <beans> 요소 안에 Bean 정의를 여러개 한다 -->
<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>  요소에 bean 정의를 한다.
        id 속성에서 지정한 값은 bean의 이름이 되고 
        class 속성에서 지정한 클래스가 해당 bean의 구현 클래스다.
        이때 class 속성은 FQCN으로 패키지명부터 클래스명까지 정확하게 기재해야 한다. -->
    <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 속성에 주입할 Bean의 이름을 기재한다. -->
        <constructor-arg ref="userRepository"/>
        <construcotr-arg ref="passwordEncoder"/>
    </bean>
</beans>

XML 기반 설정 방식 역시 이것만 사용하려고 하면 모든 컴포넌트를 Bean으로 정의해야 한다.

그래서 자바기반과 마찬가지로 어노테이션 기반 방식과 조합해 사용하면 좀 더 편하게 사용할 수 있다.

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

 

xml 파일의 value 속성에서 문자열을 지정한다고 해서 실제 자바코드에서 그 값을 받는 타입이 반드시 문자열 타입일 필요는 없다.

xml에서 문자열로 기재되어 있더라도 필요한 경우 DI를 하는 과정에서 형변환 할 수 있기 때문이다.

 

 

어노테이션 기반 설정 방식(Annotation-based Configuration)

 

어노테이션 기반 설정 방식에서는 IoC 컨테이너에 관리할 Bean을 Bean 설정 파일에 정의하는 대신 Bean을 정의하는 어노테이션을 Bean의 클래스에 부여하는 방식을 사용한다.

이후 이 어노테이션이 붙은 클래스를 탐색해 IoC 컨테이너에 자동으로 등록하는데 이런 탐색과정을 ComponentScan(컴포넌트 스캔)이라고 한다.

또한 의존성 주입도 이제까지처럼 명시적으로 설정하는 것이 아니라 어노테이션이 붙어있으면 IoC 컨테이너가 자동으로 필요로 하는 의존 컴포넌트를 주입하게 한다.

이러한 주입 과정을 Autowiring(오토와이어링)이라고 한다.

 

//UserRepository

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

@Component
public class UserRepositoryImpl implements UserRepository{
    ....
}



//BcryptPasswordEncoder

@Component
public class BcryptPasswordEncoder implements PasswordEncoder{
    ....
}



//UserServiceImpl

@Component
public class UserserviceImpl implements UserService{
    
    @Autowired
    public userServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder){
        ....
    }
    ....
}

 

이렇게 @Component를 붙여 ComponentScan이 되도록 만들어 준다.

그리고 @Autowired를 생성자에 부여해 오토와이어링이 되도록 만들어준다.

오토와이어링을 사용하면 기본적으로 주입대상과 같은 타입의 Bean을 IoC 컨테이너에서 찾아 와이어링 대상에 주입하게 된다.

 

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

 

//자바기반 컴포넌트 스캔 범위 설정
import org.springframework.context.annotation.ComponentScan
....

@Configuration
@ComponentScan("com.example.demo")
public class AppConfig{
    ....
}
/*
    @ComponentScan 어노테이션을 부여하고 value나 basepackage 속성에
    컴포넌트 스캔을 수행할 패키지를 지정한다.
    이 예제의 경우 com.example.demo 패키지 이하의 범위에 있는 모든 클래스를 스캔하고
    스캔 대상이 되는 어노테이션이 부여된 클래스를 IoC 컨테이너에 자동으로 등록한다.
    이 속성을 생략하고 @CompoentScan  이렇게만 어노테이션을 부여할 경우
    이 클래스가 속해있는 패키지 이하의 범위만 스캔한다.
*/
<!-- XML 기반 스캔 범위 설정 -->
<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>

<!-- 이렇게 <context:component-scan> 요소의 base-package에 패키지를 지정하면 된다. -->

IoC 컨테이너에 등록되는 Bean의 이름은 기본적으로 클래스명의 첫 글자를 소문자로 바꾼 이름과 같다.

단, 첫 글자 이후에 대문자가 연속되는 경우에는 첫 글자를 소문자로 변환하지 않고 클래스명이 그대로 Bean이름으로 사용된다.

예를들어 UserRepositoryImpl 클래스라면 userRepositoryImpl 이라는 이름으로 사용되는 것이고

USerRepositoryImpl 클래스라면 두번째 글자 역시 대문자이기 때문에 첫글자도 소문자로 변환하지 않고 USerRepositoryImpl로 사용된다.

만약 bean 이름을 명시적으로 지정하고 싶다면 @Component("name") 이런식으로 @Component 어노테이션에 원하는 이름을 넣어주면 된다.

 

 

아래는 이런 Bean 설정 방식들을 지원하기 위한 구현 클래스의 예제다.

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
/*
    자바 기반 설정 방식으로 AnnotationConfigApplicationContext의 
    생성자에 @Configuration 어노테이션이 붙은 클래스를 인수로 전달한다.
*/
ApplicationContext context = new AnnotationConfigApplicationContext("com.example.app");
/*
    어노테이션 기반의 설정 방식으로 AnnotationConfigApplicationContext의 
    생성자에 패키지 명을 인수로 전달한다.
    그럼 지정된 패키지 이하의 경로에서 컴포넌트를 스캔한다.
*/
ApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/applicationContext.xml");
/*
    XML 기반의 설정 방식으로 ClassPathXmlApplicationContext의 생성자에 
    XML 파일을 인수로 전달한다.
    경로에 접두어(prefix)가 생략된 경우에는 classpath안에서 
    상대 경로로 설정 파일을 탐색한다.
*/
ApplicationContext context = new FileSystemXmlApplicationContext("./spring/applicationContext.xml");
/*
    XML 기반의 설정방식으로 FileSystemXmlApplicationContext의 
    생성자에 XML 파일을 인수로 전달한다.
    경로에 접두어가 생략된 경우에는 JVM의 작업 디렉토리 안에서 
    상대경로로 설정 파일을 탐색한다.
*/

이전 포스팅에서 정리했다시피 ApplicationContext는 단독 애플리케이션에서 스프링을 사용하거나 JUnit으로 만든 테스트 케이스 안에서 스프링을 구동할 때 사용하고, 웹 애플리케이션에서는 스프링 MVC를 활용하게 되는데 이때는 WebApplicationContext를 사용한다.

 

 

2. DI 방식(의존성 주입 방식)

 

의존성 주입은 총 세가지의 방법으로 사용할 수 있다.

1. 설정자 기반 의존성 주입 방식(Setter-based dependency injection)

2. 생성자 기반 의존성 주입 방식(Constructor-based dependency injection)

3. 필드 기반 의존성 주입 방식(Filed-based dependency injection)

 

 

설정자 기반 의존성 주입 방식(Setter-based dependency injection)

 

설정자 기반 의존성 주입 방식은 설정자 메소드(Setter Method)의 인수를 통해 의존성을 주입하는 방식이다.

줄여서 세터 인젝션(Setter injection)이라고도 부른다.

이 세터 인젝션은 설정자 메소드가 만들어져 있어야 사용할 수 있다.

public class UserServiceImpl implements UserService {
    
    private UserRepository userRepository;
    private PasswordEncoder passwordEncoder;
    
    //default Constructor. 생략이 가능하다.
    public UserServiceImpl(){
    
    }
    
    public void setUserRepository(UserRepository userRepository){
        this.userRepository = userRepository;
    }
    
    public void setPasswordEncoder(PasswordEncoder passwordEncoder){
        this.passwordEncoder = passwordEncoder;
    }
    ....
}

이렇게 설정자 메소드가 만들어졌다면 의존성 주입을 할 수 있다.

우선은 이 세터 인젝션을 자바 기반 설정 방식으로 표현한 예제다.

 

@Configuration
public class AppConfig{

    @Bean
    UserRepository userRepository(){
        return new UserRepositoryImpl();
    }
    
    @Bean
    PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    UserService userService(){
    
        UserServiceImpl userService = new UserServiceImpl();
    
        userService.setUserRepository(userRepository());
        userService.setPasswordEncoder(passwordEncoder());
    
        return userService;
    }
    
    ....
}

예제를 보면 설정자 메소드에 다른 컴포넌트의 참조 결과를 설정했을 뿐이다.

 

조금 다른 방식으로 @Bean 어노테이션을 붙인 메소드에 매개변수 형태로 의존 컴포넌트를 받게 한 후, 그 값을 설정자 메소드를 통해 주입시켜도 된다.

 

@Configuration
public class AppConfig{

    ....

    @Bean
    UserService userService(UserRepository userRepository, PasswordEncoder passwordEncoder){
    
        UserServiceImpl userService = new UserServiceImpl();
    
        userService.setUserRepository(userRepository);
        userService.setPasswordEncoder(passwordEncoder);
    
        return userService;
    }
    
    ....
}

 

이처럼 자바 기반 설정 방식으로 세터 인젝션을 하게 되면 마치 프로그램에서 인스턴스를 직접 생성하는 것 처럼 보이기 때문에 이 코드가 bean을 정의한 설정인지 체감이 안될 수 있다.

 

 

다음은 세터 인젝션을 XML 기반 설정방식으로 표현한 예제다.

<bean id=userService" class="com.example.demo.UserServiceImpl">
    <property name="userRepository" ref="userRepository"/>
    <property name="passwordEncoder" ref="passwordEncoder"/>
</bean>

XML 방식에서 세터 인젝션을 할 때는 주입 대상을 <property> 요소에 기술하는데 <property>  요소의 name 속성에 주입할 대상의 이름을 지정하면 된다.

 

 

마지막으로 세터 인젝션을 어노테이션 기반으로 표현한 예제다.

@Component
public class UserServiceImpl implements UserService {
    
    private UserRepository userRepository;
    private PasswordEncoder passwordEncoder;
    
    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Autowired
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
    
    ....
}

이 예제처럼 설정자 메소드에 @Autowired 어노테이션을 붙여주기만 하면 된다.

어노테이션 기반의 설정 방식을 이용하면 자바기반이나 XML 기반의 설정방식 처럼 별도의 설정 파일을 둘 필요가 없다.

 

 

 

생성자 기반 의존성 주입 방식(Consturctor-based Configuration)

 

생성자 기반 의존성 주입 방식은 생성자의 인수를 사용해 의존성을 주입하는 방식이다.

생성자(Constructor)를 그대로 따와 컨스트럭터 인젝션(Constructor Injection)이라고 부른다.

 

컨스트럭터 인젝션의 설정 방식은 Bean 설정 방식에서의 예제에서 확인할 수 있다.

 

//constructor injection의 자바기반 설정방식
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
....

@Configuration
public class AppConfig{
    
    ....
    @Bean
    UserService userService(UserRepository userRepository, PasswordEncoder passwordEncoder){
        return new UserServiceImpl(userRepository, passwordEncoder);
    }
    
    ...
}
<!-- constructor injection의 xml 기반 설정 방식 -->
<bean id="userService" class="com.example.demo.UserServiceImpl">
    <constructor-arg ref="userRepository"/>
    <construcotr-arg ref="passwordEncoder"/>
</bean>
//constructor injection의 어노테이션 기반 설정방식

@Component
public class UserserviceImpl implements UserService{
    
    @Autowired
    public userServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder){
        ....
    }
    ....
}

 

 

이렇게 자바 기반 설정 방식에서는 생성자에 의존 컴포넌트를 직접 설정하고,

XML 기반 설정 방식에서는 <constructor-arg> 요소에서 참조하는 컴포넌트를 설정한다.

그리고 어노테이션 기반 설정 방식에서는 생성자에 @Autowired를 부여한다.

 

xml 기반 설정방식에서 생성자가 여러개의 인자를 필요로 하는 경우 <consturctor-arg> 를 여러번 정의하면 되는데

순서에 주의해야 한다.

이때 다음과 같이 index 속성을 활용하면 생성자 인수의 순서를 명시적으로 지정해 가독성을 향상시키고 추가/삭제시에 발생할 수 있는 실수를 쉽게 발견한다는 장점이 있다.

 

<!-- constructor injection의 xml 기반 설정 방식 -->
<bean id="userService" class="com.example.demo.UserServiceImpl">
    <constructor-arg index="0" ref="userRepository"/>
    <construcotr-arg index="1" ref="passwordEncoder"/>
</bean>

다른 방법으로는 name 속성에 인수명을 지정할 수도 있다.

<!-- constructor injection의 xml 기반 설정 방식 -->
<bean id="userService" class="com.example.demo.UserServiceImpl">
    <constructor-arg name="userRepository" ref="userRepository"/>
    <construcotr-arg name="passwordEncoder" ref="passwordEncoder"/>
</bean>

name 속성을 활용하면 인수의 순서가 바뀌거나 추가될 때도 인덱스 순서를 매번 변경하지 않아도 되는 장점이 있다.

다만 인수명 정보는 소스코드가 컴파일 되는 과정에서 없어지기 때문에 컴파일 할 때 javac 명령과 함께 디버깅 정보를 전달할 수 있는 -g 옵션을 사용하거나, JDK 8 이후부터는 메소드 매개변수의 메타 정볼르 생성할 수 있는 -parameters 옵션을 사용해야 한다.

만약 이렇게 별도의 컴파일 옵션을 주는것이 번거롭다면 아래와 같이 @ConsturctorProperties 어노테이션을 달아주는 방법도 있다.

 

@ConstructorProperties({"userRepository", "passwordEncoder"})
public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {

}

컨스트럭터 인젝션을 사용하면 필드를 final로 선언해서 생성후에 변경되지 않게 만들 수 있다.

이렇게 필드를 변경하지 못하도록 엄격하게 제한을 거는것은 다른 의존성 주입 방식으로는 처리하지 못하고 오직 컨스트럭터 인젝션에서만 가능하다.

 

 

 

필드 기반 의존성 주입 방식(Field dependency Injection)

 

필드 기반 의존성 주입 방식은 생성자나 설정자 메소드를 쓰지 않고 IoC 컨테이너의 힘을 빌려 의존성을 주입하는 방식이다.

 

필드 인젝션을 할 때는 의존성을 주입하고 싶은 필드에 @Autowired 어노테이션을 달아주면 된다.

이처럼 필드 기반 의존성 주입 방식을 사용하면 생성자나 설정자 메소드를 굳이 만들 필요가 없어지기 떄문에 두 메소드의 작성을 생략해 소스코드가 비교적 간결해 보이는 장점이 있다.

 

웹 개발을 공부하다보면 많이 볼 수 있는 방식이다.

 

@Component
public class UserServiceImpl implements UserService {

    @Autowired
    UserRepository userRepository;
    
    @Autowired
    PasswordEncoder passwordEncoder;
    
    ....
}

 

필드 인젝션을 사용할 때는 한가지 주의할 점이 있다.

소스코드의 양을 줄이기 위해 생성자나 설정자 메소드를 생략하고 싶다면 반드시 IoC 컨테이너를 사용한다는 것을 전제해야 한다는 것이다.

IoC 컨테이너 없이 사용되는 독립형 라이브러리로 사용될 소스코드에서 필드 인젝션을 사용하는 것은 잘못된 판단이다.

 

 

+ Recent posts