SpringSecurity를 사용하던 중 Security 설정에서 상속받아 사용하던 WebSecurityConfigurerAdapter가 Deprecated되었다.

 

처음 Spring Security를 공부했을 때 WebSecurityConfigurerAdapter를 상속받아 설정하는걸 배웠었고 프로젝트에 구현할때도 그렇게 했었는데 어느날 갑자기 선이 쭉 생기면서 Deprecated 되었다는 것을 발견했다.

 

그래서 찾아보니 Spring Security 5.7.0부터 사용을 권장하지 않는다는 공식 문서를 찾게 되었다.

 

공식문서에는 더이상 사용을 하지 않는 다는 말과 함께 어떻게 수정해 사용하면 되는지에 대해 설명해뒀다.

 

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

 

Spring Security without the WebSecurityConfigurerAdapter

<p>In Spring Security 5.7.0-M2 we <a href="https://github.com/spring-projects/spring-security/issues/10822">deprecated</a> the <code>WebSecurityConfigurerAdapter</code>, as we encourage users to move towards a component-based security configuration.</p> <p

spring.io

 

 

WebSecurityConfigurerAdapter를 사용할때의 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**", "/js/**", "/css/**");
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/**")
                .access("hasRole('ROLE_USER')")
            .and()
                .formLogin()
                .successHandler(loginSuccessHandler())
            .and()
                .logout()
                .logoutSuccessUrl("/")
            .and()
                .exceptionHandling().accessDeniedPage("/");
    }
}

 

WebSecurityConfigurerAdapter를 사용하지 않을때의 설정

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/resources/**", "/js/**", "/css/**");
    }
    
    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/user/**")
                .access("hasRole('ROLE_USER')")
            .and()
                .formLogin()
                .successHandler(loginSuccessHandler())
            .and()
                .logout()
                .logoutSuccessHandler(logoutSuccessHandler())
            .and()
                .exceptionHandling().accessDeniedpage("/");
        
        
        return http.build();
    }
}

 

 

코드만 봤을때는 크게 차이는 없다. 코드상으로는 상속받는게 없으니 override하지 않고 Bean으로 등록해준다는 정도의 차이점만 발생한다고 볼 수 있다.

이 외 설정에 대해서도 공식문서에 잘 나와있기 때문에 공식문서를 잘 확인한다면 큰 문제는 없을것 같다.

 

 

몇번 테스트를 해보면서 그런적은 없는데 이 설정 방식에 대해 알아보던 중 이렇게 bean으로 등록했을 때 오류가 발생하는 경우도 있는것 같다.

 

found webSecurityConfigurerAdapter as well as securityFilterChain. please select just one.

이런 오류가 발생하는 경우가 있다는데 그럴때는 securityConfig 클래스에

@ConditionalOnDefaultWebSecurity

@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)

이 두 어노테이션을 추가해주고

securityFilterChain 메소드에 

@Order(SecurityProperties.BASIC_AUTH_ORDER)

어노테이션을 추가해주면 해결이 된다고 한다.

 

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 종료

 

 

Bean 설정 분할

 

IoC 컨테이너에서 관리하는 bean이 많아지면 많아질수록 설정 내용도 많아져서 관리하기가 어려워진다.

이럴때는 bean 설정 범위를 명확히 하고 가독성도 높이기 위해 목적에 맞게 분할하는 것이 좋다.

 

분할 방식에 대해서는 자바 기반 설정에서의 분할, XML 기반 설정에서의 분할로 나눠 정리한다.

어노테이션 기반 설정 방식에 대한 분할은 책에 나와있지도, 언급되지도 않았는데 어노테이션 기반 설정 방식의 경우 ComponentScan을 통해 bean을 컨테이너에 등록해 처리하는 방식이다보니 별다르게 분할을 해야할 이유가 없기 때문이지 않을까라고 생각한다.

 

 

1. 자바 기반 설정 방식에서의 분할

 

자바 기반 설정방식에서 설정된 내용(Configuration Class)을 분할할 때는 @Import 어노테이션(org.springframework.context.annotation.Import)을 사용한다.

예제는 userService, productService, adminService 라는 세개의 빈을 분할해서 테스트한 예제다.

 

//UserConfig Class
@Configuration
public class UserConfig {
    
    @Bean
    UserService userService() {
        return new UserServiceImpl();
    }
}


//ProductConfig Class
@Configuration
public class ProductConfig {

    @Bean
    @Scope("prototype")
    ProductService productService() {
        return new ProductServiceImpl();
    }
}


//AppConfig Class
@Configuration
@Import({UserConfig.class, ProductConfig.class})
public class AppConfig {

    @Bean
    AdminService adminService() {
        return new AdminServiceImpl();
    }
}


//test Class
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class testClass {

    @Test
    public void divTest() {
        
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        
        String[] beanArr = context.getBeanDefinitionNames();
        
        System.out.println("bean Arr");
        
        for(String beanName : beanArr)
            System.out.println(beanName);
            
        
        UserService userService1 = context.getBean(UserService.class);
        UserService userService2 = context.getBean(UserService.class);
        assertEquals(userService1, userService2);
        
        AdminService adminService1 = context.getBean(AdminService.class);
        AdminService adminService2 = context.getBean(AdminService.class);
        assertEquals(adminService1, adminService2);
        
        ProductService productService1 = context.getBean(ProductService.class);
        ProductService productService2 = context.getBean(ProductService.class);
        assertEquals(productService1, productService2);
    }
}


/* 테스트 결과
    
    beanArr
    processor 및 EventListener 
    ...
    appConfig
    com.example.UserConfig
    userService
    com.example.ProductConfig
    productService
    adminService
    
    productService1, productService2 검증에 대한 오류
*/

예제를 보면 productService와 userService를 각각 ProductConfig와 UserConfig에 분할해서 설정했다.

그리고 테스트에서 잘 들어오는지 확인해보기 위해 productService는 prototype으로 설정했고 AppConfig에는 adminService라는 bean을 하나 설정해주고 Import로 ProductConfig와 UserConfig를 설정해줬다.

 

이때 주의사항은 분할된 설정 클래스도 일반 설정 클래스처럼 @Configuration 어노테이션을 꼭 붙여줘야 한다.

 

테스트에서는 AnnotationConfigApplicationContext로 ApplicationContext를 만들어주고

해당 ApplicationContext에 속해있는 bean의 이름들을 getBeanDefinitnionNames()로 받아 출력했다.

그리고 각 bean들을 두개씩 가져와 테스트해보니 userService와 adminService는 정상적으로 singleton으로 생성된 것을 확인했고 productService역시 오류가 발생했으니 prototype으로 잘 처리되었다고 볼 수 있다.

 

테스트 결과를 보면 조금 특이한 점이 있는데 이전에 설정 방식에 대해 정리할때는 딱 bean의 이름만 나왔었지만

이번에는 분할한 설정 클래스 역시 같이 출력되었다.

그래서 context.getBean(UserConfig.class); 로 해당 bean을 가져와봤는데 아무 문제가 발생하지 않았다.

bean을 가져올 수 있다고 해서 거기서 UserService bean을 가져온다거나 하는건 안되는것 같고 UserConfig안에 메소드를 하나 만들어서 테스트했더니 해당 메소드를 호출할 수는 있었다.

근데 그 외에는 딱히 사용할 수 있는 뭔가는 보이지 않았다...

 

 

 

2. XML 기반 설정 방식에서의 분할

 

XML 기반 설정 방식에서 파일을 분할할 때는 <import> 요소를 사용한다.

예제는 자바 기반 설정 방식에서와 동일한 형태로 테스트한다.

 

<!-- product-config.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="productService" class="com.example.ProductServiceImpl" scope="prototype"/>

</beans>


<!-- user-config.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="com.example.UserServiceImpl"/>

</beans>


<!-- applicationContext.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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <import resource="User-config.xml"/>
    <import resource="Product-config.xml"/>

    <bean id="adminService" class="com.example.AdminServiceImpl"/>
</beans>
//test class
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class testClass {

    @Test
    public void divTest() {
        
        ApplicationContext context = new GenericXmlApplicationContext(
                               "file:src/main/webapp/WEB-INF/applicationContext.xml");
        
        String[] beanArr = context.getBeanDefinitionNames();
        
        System.out.println("bean Arr");
        
        for(String beanName : beanArr)
            System.out.println(beanName);
            
        
        UserService userService1 = context.getBean(UserService.class);
        UserService userService2 = context.getBean(UserService.class);
        assertEquals(userService1, userService2);
        
        AdminService adminService1 = context.getBean(AdminService.class);
        AdminService adminService2 = context.getBean(AdminService.class);
        assertEquals(adminService1, adminService2);
        
        ProductService productService1 = context.getBean(ProductService.class);
        ProductService productService2 = context.getBean(ProductService.class);
        assertEquals(productService1, productService2);
    }
}


/* 테스트 결과
    
    beanArr
    userService
    productService
    adminService
    
    productService1, productService2 검증에 대한 오류
*/

XML 기반 설정에서 주의할점은 import시에 경로다.

지금 예제에서는 applicationContext.xml과 같은 webapp/WEB-INF 위치에 *-config.xml 파일을 생성했기 때문에 저렇게 딱 파일명만 적어도 되지만 resource 폴더에 생성하는 경우 classpath: 를 사용한다거나 해서 경로를 잘 잡아줘야 한다.

 

분할된 bean 정의 파일은 일반적인 bean 정의 파일과 동일한 형식으로 작성하면 된다.

테스트 결과를 보면 자바 기반 설정 방식과는 조금 다른것을 볼 수 있는데 자바 기반 설정방식에서는 import 해준 클래스까지 모두 출력된 반면에 xml 기반 설정 방식에서는 생성하고자 했던 bean의 이름만 딱 나오는 것을 볼 수 있다.

 

사용하는데에 대한 차이는 없었지만 컨테이너에 등록되는 bean의 차이가 좀 있다는 점이 차이점이라면 차이점일 수 있겠다.

 

 

프로파일(profile)별 설정

 

스프링에서는 설정 파일을 특정 환경이나 목적에 맞게 선택적으로 사용할 수 있도록 그룹화 할 수 있으며, 이 기능을 프로파일(profile)이라고 한다.

예를들어, 애플리케이션이 실행될 환경마다 서로 다른 프로파일을 만든다면 개발 환경을 위한 development profile, 검증을 위한 staging profile, 실제 운영 환경을 위한 production profile등을 만들 수 있을 것이다.

 

프로파일 정의는 모든 자바, XML, 어노테이션 기반 설정 방식 모두 가능하다.

 

1. 자바기반 설정 방식에서의 profile 정의

 

자바기반 설정 방식에서 프로파일을 정의할 때는 @Profile 어노테이션(org.springframework.context.annotation.Profile)을 사용한다.

 

@Configuration
@Profile("development")
public class DevelopmentConfig {
    ....
}


@Configuration
@Profile("staging")
public class satgingConfig {
    ....
}


@Configuration
@Profile("production")
public class ProductionConfig {
    ....
}

이렇게 클래스 단위로 @Profile 어노테이션을 붙여 프로파일을 정의할 수 있다.

그리고 클래스 단위가 아닌 메소드 단위로도 정의가 가능하다.

 

@Configuration
pbulic class AppConfig {
    
    @Bean(name = "dataSource")
    @Profile("development")
    DataSource dataSourceForDevelopment() {
        ....
    }
    
    @Bean(name = "dataSource")
    @Profile("staging")
    DataSource dataSourceForStaging() {
        ....
    }
    
    @Bean(name = "dataSource")
    @Profile("production")
    DataSource dataSourceForProduction() {
        ....
    }
}

 

@Profile 어노테이션에서는 @Profile({"development", "staging"})과 같이 여러개의 프로파일을 지정하거나

@Profile("!production")과 같이 production 프로파일만을 제외한다는 부정형으로 표현할 수도 있다.

 

프로파일을 개발 환경별로 구분하는 방식은 개발이 어느정도 진행된 다음에 다른 개발 환경으로 쉽게 옮겨갈 수 있게 해주는 유용한 기법이다.

 

 

2. XML 기반 설정에서의 profile 정의

 

XML 기반 설정 방식에서는 <beans> 요소의 profile 속성을 활용한다.

<!-- 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd"
       profile="development">
       
    <!-- 이 안에서 정의한 내용은 지정한 프로파일(development)내에서만 유효하다. -->
</beans>


<!-- 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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <beans profile="development">
        <!-- 이 안에서 정의한 내용은 development 프로파일 내에서만 유효하다 -->
        <bean id="datasource" class="....">
            <!-- 생략 -->
        </bean>
    </beans>
    
    <beans profile="staging">
        <!-- 이 안에서 정의한 내용은 staging 프로파일 내에서만 유효하다 -->
        <bean id="datasource" class="....">
            <!-- 생략 -->
        </bean>
    </beans>
    
    <beans profile="production">
        <!-- 이 안에서 정의한 내용은 production 프로파일 내에서만 유효하다 -->
        <bean id="datasource" class="....">
            <!-- 생략 -->
        </bean>
    </beans>
</beans>

XML 기반 설정 방식에서는 예제중에 위에 있는 경우처럼 각 프로파일별로 XML 파일을 만들어 정의하는 방법과 하나의 XML 파일 안에 <beans> 요소를 통해 여러개의 프로파일을 정의하는 방법으로 사용할 수 있다.

그리고 여러 프로파일을 동시에 지정하고 싶다면 <beans profile="profile1, profile2, ....."> 과 같이 쉼표로 구분해서 나열하면 된다.

 

 

3. 어노테이션 기반 설정 방식에서의 profile 정의

 

어노테이션 기반 설정 방식에서는 컴포넌트에 @Profile 어노테이션을 부여하고 프로파일을 지정하면 된다.

@Component
@Profile("staging")
public class DummyUserRepository implements UserRepository {
    ....
}

 

만약 프로파일이 별도로 지정되지 않은 bean 설정이 있다면 이것들은 모든 프로파일에서 사용이 가능하다고 보면 된다.

 

 

프로파일 선택

 

실행시 어떤 프로파일을 선택해야 할지에 대한 정보는 시스템 프로퍼티를 통해 전달 할 수 있는데, 자바 애플리케이션을 실행할 때 명령행 옵션으로 spring.profiles.active라는 프로퍼티 값과 사용할 프로파일 이름을 지정하면 된다.

# 자바 명령행 옵션으로 프로파일을 지정하는 방법
-Dspring.profiles.active=production

 

만약 프로파일을 여러개 선택하고 싶으면 쉼표로 구분해서 나열할 수 있다.

 

자바 명령행 옵션으로 전달하는 방법 외에도 환경 변수를 이용할 수도 있는데 환경 변수명 SPRING_PROFILES_ACTIVE에 사용할 프로파일 이름을 지정하면 된다.

 

# 환경 변수로 프로파일을 지정하는 방법
export SPRING_PROFILES_ACTIVE=production

 

웹 애플리케이션에서는 웹 애플리케이션 설정 파일(Web Application Deployment Descriptor)인 web.xml에 다음과 같이 작성하면 된다.

<!-- web.xml -->
....
<context-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>production</param-value>
</context-param>
....

 

spring.profiles.active를 따로 지정하지 않았다면 기본값으로 spring.profiles.default에서 지정된 프로파일을 사용한다.

웹 애플리케이션이라면 위와 같이 web.xml에 spring.profiles.default를 설정해서 기본 프로파일을 지정한 다음 프로파일을 바꾸고 싶을때만 자바 명령행 옵션으로 spring.profile.active를 지정해 기본 프로파일을 덮어쓰면 된다.

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 종료

 

 

IoC 컨테이너는 bean 간의 의존관계를 정리할 뿐만 아니라 생존 기간도 관리한다.

이 생존기간을 BeanScope라고 하며 개발자가 직접 bean의 스코프를 다루지 않아도 된다는 점은 IoC 컨테이너를 사용하는 큰 이유이기도 하다.

 

만약 IoC 컨테이너가 bean의 스코프를 관리하지 않고 의존관계만 관리해준다면 어떤 bean을 singleton으로 사용하고 싶을 때 개발자가 직접 singleton을 구현해야 한다.

 

좀 더 복잡하게 단순한 singleton이 아닌 Http Session이 살아있는 동안에만 존재하는 단 하나의 인스턴스가 필요한 경우에는?

HttpSession의 특정 속성에 내가 찾는 인스턴스가 설정되어있는지 확인 한 후에 없다면 새로 만들어서 설정하고, 있다면 그것을 재사용하도록 만들어줘야 한다.

 

문제는 HttpSession이 파괴될 때는 HttpSessionListener를 사용해 세션의 특정 속성에 포함된 인스턴스의 파괴 후 처리를 개발자가 직접 구현해야 한다.

이러한 코드가 많으면 많을수록 애플리케이션 전체를 이해하기 어려워지고 기능을 예측하기 힘들어진다.

이런 복잡한 기능을 IoC 컨테이너가 bean 스코프를 관리하도록 맡겨서 처리할 수 있다.

 

IoC 컨테이너가 관리하는 bean은 기본적으로 singleton으로 만들어진다.

어떠한 bean의 singleton 인스턴스가 필요하다면 context.getBean()으로 가져오면 된다.

 

스프링에서 사용가능한 스코프의 종류는 다음과 같다.

스코프 설명
singleton IoC 컨테이너를 기동할 때 bean 인스턴스 하나가 만들어지고 난 이후부터는 그 인스턴스를 공유하는 방식이다.
기본 스코프이기 때문에 별도로 스코프를 지정하지 않았다면 singleton으로 간주한다.
prototype IoC 컨테이너에 bean을 요청할 때마다 새로운 bean 인스턴스가 만들어진다.
Multi Thread 환경에서 오작동이 발생하지 않아야 하는 (thread-safe) bean이라면 singleton 스코프가 아닌 prototype을 사용해야 한다.
request HTTP 요청이 들어올 때 마다 새로운 bean 인스턴스가 만들어진다.
웹 애플리케이션을 만들때만 사용할 수 있다.
session HTTP 세션이 만들어질 때 마다 새로운 Bean 인스턴스가 만들어진다.
웹 애플리케이션을 만들때만 사용할 수 있다.
global Session 포틀릿(portlet) 환경에서 글로벌 HTTP 세션이 만들어질 때 마다 새로운 Bean 인스턴스가 만들어진다.
포틀릿을 사용한 웹 애플리케이션을 만들때만 사용할 수 있다.
application ServletContext가 만들어질 때 마다 bean 인스턴스가 만들어진다.
웹 애플리케이션을 만들때만 사용할 수 있다.
custom 스코프 이름을 직접 정할 수 있고 정의한 규칙에 따라 bean 인스턴스를 만들 수 있다.

 

그리고 스프링 4.1부터 websocket 스코프가 더 추가되었다.

singleton은 IoC 컨테이너가 기동할 때 bean 인스턴스가 하나 만들어지고 이후부터는 계속 이 인스턴스를 공유하는 방식이다.

그리고 다른 스코프들 중 custom을 제외한 나머지 스코프들은 해당 조건이 만족할 때마다 새로운 bean 인스턴스가 만들어진다.

 

 

 

1. 스코프 설정

IoC 컨테이너에 등록된 bean은 기본 bean scope가 singleton이라고 했다.

즉, IoC 컨테이너에서 bean을 가져오려 할 때 같은것이 없다면 새로 만들고, 같은것이 있을 때는 이미 만들어진 것을 공유한다.

그리고 IoC컨테이너가 파괴될 때 그 안에 있는 bean 역시 파괴된다.

 

만약 이런 기본 스코프가 아닌 다른 스코프로 bean을 사용하고 싶다면 bean을 정의하는 단계에서 스코프를 명시해야 한다.

이 설정은 자바, xml, annotation 기반 설정 모두에서 가능하다.

 

자바 기반 설정 방식에서는 @Bean 어노테이션이 붙은 메소드에 @Scope 어노테이션을 추가해 스코프를 명시한다.

테스트 예제는 기본 스코프가 singleton으로 생성되는지 확인하고 그 다음에 prototype으로 다시 한번 테스트를 한다.

 

//singleton Scope Bean
@Configuration
public class AppConfig{

    @Bean
    UserService userService(){
        return new UserServiceImpl();
    }
}


//singleton Scope Bean Test

@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{

    @Test
    public void singletonScopeJavaTest() {
    
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        
        UserService userService1 = context.getBean(UserService.class);
        UserService userService2 = context.getBean(UserService.class);
        
        assertEquals(userService1, userService2);
        //테스트 통과
    }
   
}



//prototype Scope Bean
@Configuration
public class AppConfig{

    @Bean
    @Scope("prototype")
    UserService userService(){
        return new UserServiceImpl();
    }
}


//prototype Scope Bean Test

@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{

    @Test
    public void prototypeScopeJavaTest() {
    
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        
        UserService userService1 = context.getBean(UserService.class);
        UserService userService2 = context.getBean(UserService.class);
        
        assertEquals(userService1, userService2);
        //테스트 실패
    }
   
}

이렇게 두번의 테스트를 실행해보면 singleton 테스트에서는 정상적으로 테스트가 통과되지만 prototype 테스트에서는 통과하지 못하는 결과를 보임으로서

singleton 테스트는 두 인스턴스가 동일하고, prototype 테스트는 두 인스턴스가 다르다는 것을 확인할 수 있다.

 

같은 설정을 xml 기반으로 하게 되면 <bean> 오소의 scope 속성에서 스코프를 지정할 수 있고

어노테이션 기반 설정에서는 스캔대상이 되는 클래스에 @Scope 어노테이션으로 명시해 지정할 수 있다.

 

<!-- xml 기반 설정으로 scope 설정 -->

<!-- applicationContext.xml -->
<!-- singleton Scope Bean -->

<?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="userService" class="com.example.UserService"/>
    
</beans>



<!-- applicationContext.xml -->
<!-- prototype Scope Bean -->

<?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="userService" class="com.example.UserService" scope="prototype"/>
    
</beans>
//어노테이션 기반으로 scope 설정

//singleton Scope Bean
@Component
public class UserServiceImpl implements UserService{

}


//prototype Scope Bean
@Component
@Scope("prototype")
public class UserServiceImpl implements UserService{

}
//xml 기반 설정 테스트

//xml singleton Scope Bean Test

@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{

    @Test
    public void singletonScopeXmlTest() {
    
        ApplicationContext context = new GenericXmlApplicationContext(
        	"file:src/main/webapp/WEB-INF/applicationContext.xml"
        );
        
        UserService userService1 = context.getBean(UserService.class);
        UserService userService2 = context.getBean(UserService.class);
        
        assertEquals(userService1, userService2);
        //테스트 통과
    }
   
}

//xml prototype Scope Bean Test

@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{

    @Test
    public void prototypeScopeXmlTest() {
    
        ApplicationContext context = new GenericXmlApplicationContext(
        	"file:src/main/webapp/WEB-INF/applicationContext.xml"
        );
        
        UserService userService1 = context.getBean(UserService.class);
        UserService userService2 = context.getBean(UserService.class);
        
        assertEquals(userService1, userService2);
        //테스트 실패
    }
   
}



//Annotation 기반 설정 테스트

//Annotation singleton Scope Bean Test

@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{

    @Test
    public void singletonScopeAnnotationTest() {
    
        ApplicationContext context = new AnnotationConfigApplicationContext("com.example");
        
        UserService userService1 = context.getBean(UserService.class);
        UserService userService2 = context.getBean(UserService.class);
        
        assertEquals(userService1, userService2);
        //테스트 통과
    }
   
}

//Annotation prototype Scope Bean Test

@RunWith(SpringJUnitClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class TestClass{

    @Test
    public void prototypeScopeAnnotationTest() {
    
        ApplicationContext context = new AnnotationConfigApplicationContext("com.example");
        
        UserService userService1 = context.getBean(UserService.class);
        UserService userService2 = context.getBean(UserService.class);
        
        assertEquals(userService1, userService2);
        //테스트 실패
    }
   
}

테스트 결과로 어느 설정이건 스코프 설정을 아무것도 하지 않았을때는 default로 singleton 스코프가 되고,

prototype으로 정의했을 때는 제대로 다른 인스턴스가 생성되는 것을 확인할 수 있다.

 

스프링 MVC와 같이 스프링 프레임워크를 기반으로 한 웹 애플리케이션을 사용할 때는 굳이 reqeust나 session 스코프 등의 웹 애플리케이션 전용 스코프는 설정하지 않아도 된다.

하지만 Servlet Filter와 같이 스프링 프레임워크를 활용하지 않는 영역에서 웹 애플리케이션 전용 스코프의 bean을 사용하고 싶다면 web.xml에 설정을 추가해주면 된다.

JSF 같은 다른 웹 애플리케이션 프레임워크에서도 스프링과 연계할 때 이 방법을 활용한다.

<!-- web.xml -->

<listener>
    <listener-class>org.springframework.web.context.reqeust.RequestContextListener</listener-class>
</listener>

 

 

2. 다른 스코프의 Bean 주입

스코프는 bean의 생존 기간을 의미하기 때문에 각 bean의 스코프가 다르다는 것은 각각의 수명이 다르다는 것이다.

예를들어 singleton 스코프가 prototype 스코프보다 더 오래산다.

웹 애플리케이션 환경이라면 request < session < singleton 순서로 오른쪽으로 갈 수록 더 오래 산다.

 

IoC 컨테이너에서는 bean 간의 의존관계가 형성되는데, 

만약 하나의 bean이 또 다른 bean에 의존하고 있다면 IoC 컨테이너에 의해 주입된 bean은 자신의 스코프와 상관없이 주입받는 bean의 스코프를 따르게 된다.

 

prototype 스코프의 bean을 singleton 스코프의 bean에 주입시켰을 때

prototype 스코프의 bean은 자신을 주입받은 singleton 스코프의 bean이 살아있는 한은 IoC 컨테이너에서 다시 만들 필요가 없기 때문에 결과적으로는 singleton 스코프의 bean과 같은 수명을 갖게 된다.

 

좀 더 구체적인 예시로 PasswordEncoder는 Multi Thread 환경에서 안전하지 않기 때문에 반드시 요청을 받을 때 마다 새로 생성하는 prototype 스코프로 동작해야 한다.

만약 singleton으로 처리되어 여러 스레드가 동시에 이 bean을 이용하게 된다면 오동작을 일으킬 수 있다.

 

//passwordEncoder Bean
@Bean
@Scope("prototype")
PasswordEncoder passwordEncoder() {
    //Multi Thread 환경에서 안전하지 않으므로 singleton으로 사용하면 안된다.
    return new ThreadUnsafePasswordEncoder();
}


//passwordEncoder bean을 주입받는 UserService Bean
@Component
public class UserServiceImpl implements UserService {
    
    @Autowired
    PasswordEncoder passwordEncoder;
    
    public void register(User user, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
        //생략
    }
}

 

위 예제를 보면 passwordEncoder Bean은 prototype으로 정의되어 있고 UserServiceImpl은 별도의 스코프 정의가 없기 때문에 singleton 스코프를 갖게 된다.

그리고 UserServiceImpl에서는 passwordEncoder를 주입받아 register 메소드에서 사용하고 있다.

 

그럼 passwordEncoder는 prototype으로 정의되었지만 singleton인 UserServiceImpl에 주입되어 있기 때문에 singleton의 수명을 갖게 된다.

그럼 singleton의 수명을 갖게 되었으니 passwordEncoder에서 prototype으로 정의했더라도 singleton처럼 매번 새로운 인스턴스를 생성하지 않고 이미 만들어둔 인스턴스를 사용하게 된다.

즉, prototype으로 동작하는 것이 아닌 singleton으로 동작하게 된다.

 

그럼 register 메소드를 두번 실행하면 같은 passwordEncoder 인스턴스가 두번 사용되는 것이므로

prototype으로 정의해둔 의미가 없다.

 

이런 경우에는 Lookup Method Injection(룩업 메소드 인젝션)으로 해결하거나 Scoped Proxy(스코프트 프록시)로 해결하거나 Custom Scope(커스텀 스코프)로 해결하는 방법이 있다.

 

 

● Lookup Method Injection(룩업 메소드 인젝션)으로 해결

이 문제를 해결하는 가장 좋은 방법은 passwordEncoder를 주입받지 않는 것이다.

그 대신 필요할때마다 IoC 컨테이너에서 bean을 찾아오면 된다.

 

@Component
public class UserServiceImpl implements UserService {

    @Autowired
    ApplicationContext context; //ApplicationContext 자동 주입
    
    public void register(User user, String rawPassword) {
        //passwordEncoder 메소드를 호출한 반환값을 할당
        PasswordEncoder passwordEncoder = passwordEncoder();
        
        String encodedPassword = passwordEncoder.encode(rawPassword);
        //생략
    }
    
    PasswordEncoder passwordEncoder() {
        return this.context.getBean(PasswordEncoder.class);
        /*
          ApplicationContext를 통해 IoC 컨테이너에 등록된 bean을 직접 찾아서 가져온다.
          이때 꺼내오는 bean은 원래 의도한 스코프대로 설정되어 나온다.
          즉, passwordEncoder는 prototype 스코프로 설정되어 있는 상태고
          register에서 호출될때마다 새로 생성되기 때문에 매번 prototype 스코프 상태의 bean을 사용할 수 있다.
        */
    }
}

 

가장 쉬운 방법이고 이 코드가 동작하는데는 큰 문제가 없다.

의존관계에 있는 bean끼리의 낮은 결합도를 유지하기 위해 IoC 컨테이너를 사용했다.

하지만 한가지 흠이 있다면 IoC 컨테이너를 사용하는 과정에서 IoC 컨테이너에 의존적인 클래스와 API가 소스코드 상에 노출된다는 점이다.

bean 간의 의존관계는 해결했지만 IoC 컨테이너와의 의존관계가 소스코드에 남았으니 사실상 바람직하지 못한 방법이고 피해야 하는 방법이다.

 

IoC 컨테이너와 관련된 코드를 소스코드에 남기지 않고 bean을 찾아오게 만드는 방법으로 룩업 메소드 인젝션을 사용한다.

IoC 컨테이너에는 passwordEncoder 메소드 같은 코드를 bytecode 형태로 만드는 기능이 있다.

IoC 컨테이너가 bean을 룩업(Lookup)하는 메소드를 만든 다음에 그 메소드를 의존할 bean에게 주입하면 되는데 이 기능을 룩업 메소드 인젝션이라고 하는 것이다.

 

이 기능을 사용하려면 IoC 컨테이너에게 룩업을 대행하게 하고 싶은 메소드에게 @Lookup(org.springframework.beans.factory.annotation.Lookup) 어노테이션을 붙여주면 된다.

그러면 이 bean이 IoC 컨테이너에 등록되는 시점에 IoC 컨테이너에서 bean을 찾는 실제 코드가 @Lookup 어노테이션이 붙은 메소드 자리에 주입된다.

 

@Component
public class UserServiceImpl implements UserService {
    
    public void register(User user, String rawPassword) {
        
        PasswordEncoder passwordEncoder = passwordEncoder();
        String encodedPassword = passwordEncoder.encode(rawPassword);
    }
    
    @Lookup
    PasswordEncoder passwordEncoder() {
        return null;
        //return 값은 null이어도 상관이 없다.
    }
}

이 동작원리를 살펴보자면 IoC 컨테이너느 UserServiceImpl의 서브 클래스를 동적으로 만든다.

이때 IoC 컨테이너는 기존의 PasswordEncoder 메소드를 IoC 컨테이너가 직접 만든 룩업 메소드로 오버라이드한다.

따라서 @Lookup을 붙인 메소드는 private이나 final을 지정하면 안된다.

매개변수 역시 지정하면 안되는데 IoC 컨테이너가 해당 메소드를 오버라이드 할 때 방해가 되기 때문이다.

 

@Lookup 어노테이션의 value 속성에는 bean의 이름을 지정할 수 있다.

별도의 value를 지정하지 않았다면 그때는 메소드의 반환값 타입을 보고 룩업 대상 bean을 찾게 된다.

 

xml 기반 설정 방식에서는 <lookup-method> 요소를 통해 룩업 메소드 인젝션을 사용할 수 있다.

<bean id="passwordEncoder" class="com.example.ThreadUnsafePasswordEncoder" scope="prototype"/>

<bean id="userService" class="com.example.UserServiceImpl">
    <lookup-method name="passwordEncoder" bean="passwordEncoder"/>
    <!-- 생략 -->
</bean>

이처럼 룩업 메소드 인젝션은 서로 다른 스코프의 bean을 조합하면서 생기는 문제를 해결할 뿐만 아니라 소스코드에서 직접 IoC 컨테이너를 사용하는 것을 방지하는 용도로도 활용할 수 있다.

룩업 메소드 인젝션은 자바 기반 설정방식에서는 사용할 수 없다.

 

● Scoped Proxy(스코프트 프록시)로 해결

 

이 방법은 기존의 bean을 Proxy(프록시)로 감싼 후, 이 프록시를 다른 bean에 주입하고, 주입받은 bean에서 이 프록시의 메소드를 호출하면 프록시 내부적으로 IoC 컨테이너에서 bean을 lookup(룩업)하고 룩업된 bean의 메소드를 실행하는 방식이다.

 

스코프트 프록시 실행 순서는 아래와 같다.

passwordEncoder를 proxy로 감싼다 -> proxy를 UserServiceImpl에 주입한다 -> UserServiceImpl에서 proxy의 메소드를 호출한다 -> proxy가 내부적으로 DI 컨테이너에서 passwordEncoder를 룩업해 메소드를 실행한다.

 

이 방법은 보통 request나 session 같이 수명이 짧은 bean을 singleton 스코프같은 상대적으로 수명이 긴 bean에 주입할 때 많이 사용한다.

 

스코프트 프록시를 활성화 할때는 @Scope 어노테이션에 proxyMode 속성에 프록시를 만드는 방법을 지정하면 된다.

 

@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
PasswordEncoder passwordEncoder() {
    return new ThreadUnSafePasswordEncoder();
}


@Component
public class UserServiceImpl implements UserService {
    @Autowired
    PasswordEncoder passwordEncoder;
    
    public void register(User user, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
    }
}

이렇게 되면 스코프트 프록시가 활성화 된 상태이기 때문에 passwordEncoder 필드에는 PasswordEncoder의 프록시가 주입되고 encode 메소드가 호출될때마다 request 스코프의 PasswordEncoder 인스턴스가 만들어진다.

 

스코프트 프록시를 사용하려면 proxyMode 속성에 다음 중 하나를 지정해야 한다.

  • ScopedProxyMode.INTERFACES
    • JDK의 동적 프록시(java.lang.reflect.Proxy)를 사용해 인터페이스 기반의 프록시를 만든다.
  • ScopedProxyMode.TARGET_CLASS
    • 스프링 프레임워크에 내장되어 있는 CGLIB을 사용해 서브클래스 기반의 프록시를 만든다.

스코프트 프록시 방식은 proxyMode에 따라 인터페이스나 서브클래스 기반으로 프록시를 만든다.

 

아래 예제는 생성되는 프록시의 예제로 동적으로 생성되는 만큼 반드시 이렇게 만들어진다는 보장은 없고 단지 차이를 보기위한 예시정도이다.

 

//인터페이스 기반 프록시
public class PasswordEncoderProxy implements PasswordEncoder {
    
    @Autowired
    ApplicationContext context;
    
    @Override
    public String encode(String rawPassword) {
        
        PasswordEncoder passwordEncoder = context.getBean("passwordEncoder", PasswordEncoder.class);
        
        return passwordEncoder.encode(rawPassword);
    }
}


//서브클래스 기반 프록시
public class PasswordEncoderProxy extends ThreadUnsafePasswordEncoder {
    
    @Autowired
    ApplicationContext context;
    
    @Override
    public String encode(String rawPassword) {
        
        PasswordEncoder passwordEncoder = context.getBean("passwordEncoder", PasswordEncoder.class);
        
        return passwordEncoder.encode(rawPassword);
    }
}

만약 스코프트 프록시를 적용할 대상 bean이 인터페이스를 갖고 있지 않은 경우 서브클래스 기반 프록시를 사용해야 한다.

그리고 서브클래스 기반의 프록시는 메소드를 오버라이드 해야 하기 때문에 메소드나 클래스에 final을 붙일 수 없다.

 

XML 기반 설정 방식으로 스코프트 프록시를 표현할때는 <aop:scoped-proxy> 요소를 사용한다.

그리고 <beans> 요소는 aop 요소를 사용하기 위한 네임스페이스(xmlns)와 스키마(xsi) 정보가 추가되어 있어야 한다.

 

<?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:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">
                           <!-- aop 기능을 사용하기 위해 aop 관련 네임스페이스와 스키마 정보를 추가 -->

    <bean id="passwordEncoder"
          class="com.example.ThreadUnsafePasswordEncoder"
          scope="request">
        <aop:scoped-proxy proxy-target-class="false"/>
        <!-- 스코프트 프록시를 적용할 bean의 <bean> 요소 아래에 <aop:scoped-proxy> 요소를 정의한다.
             proxy-target-class 속성을 false로 지정하면 인터페이스를 기반으로 한 프록시가 만들어지고
             true로 지정하면 서브클래스 기반 프록시가 만들어진다. -->
    </bean>
    
    <bean id="userService" class="com.example.UserServiceImpl">
        <property name="passwordEncoder" ref="passwordEncoder"/>
        <!-- 생략 -->
    </bean>
</beans>

 

 

어노테이션 기반 설정 방식으로 스코프트 프록시를 표현할 때는 스캔 대상 클래스에 붙인 @Scope 어노테이션에 proxyMode 속성을 추가하면 된다.

 

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public class ThreadUnsafePasswordEncoder implements PasswordEncoder {
    //생략
}

 

스프링 공식문서에서는 request, session, globalSession 스코프에서 스코프트 프록시를 사용하고 prototype 스코프에 대해서는 룩업 메소드 인젝션을 사용하도록 안내하고 있다.

 

프로토타입 스코프에서 스코프트 프록시를 사용하지 못하는 것은 아니지만,

주입된 필드에서 프록시 안에 있는 메소드를 한번 더 호출하기 때문에 매번 새로운 인스턴스가 만들어질때마다

각 프록시의 메소드가 반복해서 호출되므로 효율성 측면에서 바람직하지 않다는 점을 감안해야 한다.

 

 

● Custom Scope로 해결

 

스프링 프레임워크에서는 기존에 미리 만들어져 제공되는 스코프 외에도 사용자가 직접 정의한 Custom Scope(커스텀 스코프)를 만들 수 있다.

 

커스텀 스코프를 만들려면 Scope 인터피에스(org.springframework.beans.factory.config.Scope)를 구현하고 CustomScopeConfiguration 클래스(org.springframework.beans.factory.config.CustomScopeConfiguration)에 자신이 만든 스코프를 스코프 명과 함께 설정하면 된다.

 

아래는 Scope 인터페이스를 직접 구현하는 대신 스프링 프레임워크에서 제공하는 샘플 구현체를 사용한 예다.

샘플로 제공되는 SimpleThreadScope 클래스(org.springframework.context.support.SimpleTreadScope)를 커스텀 스코프라고 생각하고 자바 기반 설정 방식으로 어떻게 설정하는지를 보면 된다.

 

@Bean
static CustomScopeConfigurer customScopeConfigurer() {
    CustomScopeConfigurer configurer = new CustomScopeConfigurer();
    configurer.addScope("thread", new SimpleThreadScope());
    
    return configurer;
}

여기까지 하고 나면 thread 단위로 스코프를 주고 싶은 bean에 @Scope("thread") 어노테이션만 붙이면 된다.

그럼 IoC 컨테이너에 해당 bean을 요청할 때마다 thread 단위로 인스턴스가 만들어진다.

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. Autowiring

2. ComponentScan

 

 

1. Autowiring

 

Autowiring(오토와이어링)은 자바 기반 설정 방식에서 @Bean 메소드를 사용하거나 XML 기반 방식에서 <bean> 요소를 사용하는 것처럼 명시적으로 Bean을 정의하지 않고도 IoC 컨테이너에 Bean을 자동으로 주입하는 방식이다.

오토와이어링에는 type을 사용한 방식(Autowiring by type)과 이름을 사용한 방식(Autowiring by name)이 있다.

그리고 이렇게 하나만 가져오는 방법 외에 Collection이나 Map 타입으로 오토와이어링하는 방법이 있다.

 

 

타입으로 오토와이어링하기

 

흔히 스프링 웹 개발 공부를 하면서 볼 수 있는 @Autowired 어노테이션은 타입으로 오토와이어링을 하는 방식이다.

타입으로 오토와이어링을 하는 방식은 Setter Injection, Constructor Injection, Field Injection의 세가지 의존성 주입 방식에서 모두 사용할 수 있다.

타입으로 오토와이어링을 할 때는 기본적으로 의존성 주입이 반드시 성공한다고 가정한다.

그래서 주입할 타입에 해당하는 Bean을 IoC 컨테이너에서 찾지 못한다면 NoSuchBeanDefinitionException이 발생한다.

 

이런 필수조건을 완화하고 싶다면 @Autowired(required = "false")로 설정하면 된다.

그럼 해당 타입의 Bean을 찾지 못하더라도 예외가 발생하지 않고 의존성 주입은 실패했기 때문에 해당 필드의 값은 null이 된다.

 

오토와이어링의 조건을 완화해 필드 인젝션을 하는 방법으로 두가지가 있다.

하나는 방금 언급한 required = "false"를 설정하는것이고 다른 하나는 Optional을 사용해 처리할 수 있는데 이건 스프링 4부터 사용이 가능하다.

 

//required = "false"로 조건 완화
@Autowired(required = "false")
PasswordEncoder passwordEncoder;


//Optional로 조건 완화
@Autowired
Optional<PasswordEncoder> passwordEncoder;
    
public void createUser(User user, String rawPassword) {
        
    String encodedPassword = passwordEncoder.map(x -> x.encode(rawPassword))
                                    .orElse(rawPassword);
}

 

타입으로 오토와이어링 할때 IoC컨테이너에 같은 타입의 Bean이 여러개 있다면 그 중에서 어떤 것을 사용해야 할 지 알 수가 없다.

그래서 이런 경우에는 NoUniqueBeanDefinitionException이 발생한다.

보이는 의미 그대로 Bean 정의가 유일하지 않은 예외이다.

이처럼 같은 타입의 Bean이 여러개 정의된 경우에는 @Qualifier 어노테이션을 추가하면서 Bean 이름을 지정하면 같은 타입의 Bean 중에서 원하는 Bean만 선택할 수 있다.

 

//같은 타입의 Bean 여러개를 자바 기반 방식으로 정의
@Configuration
@ComponentScan
public class AppConfig {

    @Bean
    PasswordEncoder sha256PasswordEncoder() {
        return new Sah256PasswordEncoder();
    }
    
    @Bean
    PasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}


//PasswordEncoder를 오토와이어링
@Compoent
public class UserServiceImpl implements UserService {
    
    @Autowired
    @Qualifier("sha256PasswordEncoder")
    PasswordEncoder passwordEncoder;
    
    ....
}

 

이렇게 동일한 PasswordEncoder 타입이고 이름이 다른 두개의 Bean이 존재할 때

그냥 @Autowired 어노테이션 하나만으로 처리하면 NoUniqueBeanDefintionExcpetion이 발생하는 것이다.

둘다 동일하게 PasswordEncoder 타입이기 때문에 어떤것을 가져와야 할지 알 수 없기 때문이다.

그래서 @Qualifier 어노테이션에 원하는 Bean의 이름(sha256PasswordEncoder)을 추가로 명시하면 그에 해당하는 Bean을 선택할 수 있게 된다.

 

이걸 해결하는 또 다른 방법이 하나 더 있는데 만약 100번 정도의 오토와이어링에서 90번은 bcryptPasswordEncoder를 사용하는데 10번만 sha256PasswordEncoder를 가져와야 한다고 가정했을 때 100번 모두 @Qualifier 어노테이션을 전부 다 부여해서 처리하기에는 작성할 때 오타가 날 수도 있고 작성해야 하는 코드 양도 너무 많아진다.

 

그래서 우선적으로 선택할 수 있도록 하는 어노테이션이 존재한다.

 

//같은 타입의 Bean 여러개를 자바 기반 방식으로 정의
@Configuration
@ComponentScan
public class AppConfig {

    @Bean
    PasswordEncoder sha256PasswordEncoder() {
        return new Sah256PasswordEncoder();
    }
    
    @Bean
    @Primary
    PasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}


//PasswordEncoder타입 중 bcryptPasswordEncoder를 오토와이어링
@Compoent
public class UserServiceImpl implements UserService {
    
    @Autowired
    PasswordEncoder passwordEncoder;
    
    ....
}



//PasswordEncoder타입 중 sha256PasswordEncoder를 오토와이어링
@Compoent
public class UserServiceImpl implements UserService {
    
    @Autowired
    @Qualifier("sha256PasswordEncoder")
    PasswordEncoder passwordEncoder;
    
    ....
}

이렇게 우선적으로 가져오도록 할 Bean에 @Primary 어노테이션을 부여해주게 되면 @Qualifier가 붙어있지 않은 오토와이어링에서 먼저 선택해 가져오게 된다.

그럼 남은 10개의 오토와이어링에서만 예제 제일 아래처럼 @Qualifier로 sha256PasswordEncoder를 가져오도록 하면 100번의 오토와이어링을 좀 더 편하게 처리할 수 있게 된다.

 

하지만 이렇게 @Qualifier로 수식하는 Bean의 이름에 구현 클래스의 이름이 포함된다거나 구현과 관련된 정보가 포함되어 있다면 명명 방법이 바람직하다고 볼 수는 없다.

즉, 코드에서 '나는 지금 여기에 sha256PasswordEncoder를 오토와이어링 했어요!' 라고 알리는것은 좋지 않다는 것이다.

결합도를 낮추기 위해 DI 방식을 채택했는데 Bean을 사용할 때 특정 구현체가 사용될 것으로 의식한 이름을 지정해버리면 굳이 DI를 사용하는 의미가 없어지기 때문이다.

굳~이 이렇게 사용해야 한다면 DI 를 사용하지 않는것이 더 나을 수도 있다.

 

이런 경우네는 Bean의 이름으로 구현체의 이름을 쓰는 대신 역할이나 사용목적, 혹은 용도로 이름을 쓰는 것이 좋다.

sha256은 bcrypt에 비해 '경량'이기 때문에 책에서 예제는 'lightweight'라는 이름으로 지어 작성했다.

 

@Configuration
@ComponentScan
public class AppConfig {

    @Bean(name = "lightweight")
    PasswordEncoder sha256PasswordEncoder() {
        return new Sha256PasswordEncoder();
    }
    
    @Bean
    @Primary
    PasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}



@Autowired
@Qualifier("lightweight")
PasswordEncoder passwordEncoder;

이렇게 @Bean에서 name 속성에 이름을 명시함으로써 직접적인 구현체의 이름을 쓴다거나 클래스에 대한 정보가 포함되는 것을 방지할 수 있다.

 

이런 Bean의 역할이나 용도는 문자열 형태의 이름이 아닌 타입(어노테이션)으로 표현할 수도 있다.

 

//LightWeight 어노테이션 생성
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LightWeight {

}


//Bean 에 적용
@Configuration
@ComponentScan
public class AppConfig {

    @Bean
    @LightWeight
    PasswordEncoder sha256PasswordEncoder() {
        return new Sha256PasswordEncoder();
    }
    
    @Bean
    @Primary
    PasswordEncoder bcryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}


//오토와이어링에서 사용
@Autowired
@LightWeight
PasswordEncoder passwordEncoder;

이렇게 어노테이션을 생성해 Bean을 정의하고 필드 인젝션시에도 사용할 수 있다.

이처럼 직접 어노테이션을 정의하는 방식은 문자열로 된 Bean 이름을 지정하는 방식과 달리 오타가 발생하게 되었을 때 IDE에서 잡아내 주기 때문에 편하게 확인할 수 있다.

그리고 여러개의 유사한 Bean을 정의해야 한다면 최적의 방법이다.

물론 이렇게 처리하더라도 해당 구현에 대한 정보를 담고 있는 @Sha256 이러한 명명은 피해야 한다.

 

 

 

이름으로 오토와이어링하기

 

Bean의 이름이 필드명이나 프로퍼티명과 일치할 경우에 Bean 이름으로 필드 인젝션을 하는 방법도 있다.

이 방법에서는 JSR 250 사양을 지원하는 @Resource 어노테이션(javax.annotation.Resource)을 활용한다.

 

@Component
public class UserServiceImpl implements UserService {
    
    @Resource(name = "sha256PasswordEncoder")
    PasswordEncoder passwordEncoder;
    
    ....
}

이 예제는 @Qualifier 대신 @Resource가 들어간 것이다.

그리고 이전에 타입으로 오토와이어링 할 때 봤던 예제 중에서 @Bean의 name 속성이나 @Lightweight 어노테이션을 사용하지 않은 경우이다.

하지만 @Resource 어노테이션의 name 속성에 Bean의 이름을 명시함으로써 sha256으로 선택이 되는것이다.

 

//필드 이름과 일치하는경우
@Component
public class UserServiceImpl implements UserService {
    
    @Resource
    PasswordEncoder sha256PasswordEncoder;
    
    ....
}


//프로퍼티 이름과 일치해 세터인젝션을 하는 경우
@Component
public class UserServiceImpl implements UserService {
    
    private PasswordEncoder passwordEncoder;
    
    @Resource
    public void setSha256PasswordEncoder(PasswordEncoder passwordEncoder) {
        
        this.passwordEncoder = passwordEncoder;
    }
    
    ....
}

@Resource 어노테이션은 이렇게 두가지 경우로 사용할 수 있다.

이 두가지의 경우에 해당하지 않는다면 이름으로 오토와이어링을 하는것이 아닌 타입으로 오토와이어링을 해야 한다.

@Resource 방식은 조금 복잡한 편이라 동작방식을 제대로 이해한 후에 사용하는 것이 좋으며 컨스트럭터 인젝션에서는 @Resource 어노테이션을 사용하지 못한다.

 

 

 

Collection이나 Map 타입으로 오토와이어링

 

스프링에서는 위에서 봤던 방법들처럼 하나로 정의된 Bean을 가져오는 방법 외에도 같은 인터페이스를 구현한 Bean을 Collection이나 Map에 담아 가져오는 방법도 제공한다.

 

//IF 인터페이스를 구현한 여러개의 Bean 정의

public interface IF<T> {

}



@Component
public class IntIF1 implements IF<Integer> {

}


@Component
public class IntIF2 implements IF<Integer> {

}


@Component
public class StringIF implements IF<String> {

}

이렇게 하나의 인터페이스를 구현한 Bean이 여러개 존재하는 경우 오토와이어링을 아래와 같은 형태로 할 수 있다.

 

@Autowired
List<IF> ifList;

@Autowired
Map<String, IF> ifMap;

이렇게 오토와이어링 하게 되면 ifList에는 IntIF1, IntIF2, StringIf Bean이 List 형태로 주입된다.

그리고 ifMap에는 '빈 이름 = 빈' 형태로 주입된다.

'intIF1 = IntIF1, intIF2 = IntIF2, stringIF = StringIF'

 

 

IF 인터페이스를 보면 T 타입으로 제네릭 타입이다.

@Autowired
List<IF<Integer>> ifList;

@Autowired
Map<String, IF<Integer>> ifMap;

 

그래서 이렇게 IF의 Integer 타입으로 받게 되면 주입될 Bean의 파라미터가 Integer로 한정되기 때문에

IntIF1, IntIF2처럼 Integer 타입의 Bean만 주입되게 된다.

 

그럼 만약에 Bean을 정의할 때 처음부터 List나 Map 형태로 정의한다면??

@Bean
List<IF> ifList(){
    return Arrays.asList(new IntIF1(), IntIF2(), StringIF());
}

@Bean
Map<String, IF> ifMap(){
    map.put("intIF1", new IntIF1());
    map.put("intIF2", new IntIF2());
    map.put("StringIF", new StringIF());
    
    return map;
}
//@Autowired 어노테이션을 통한 필드 인젝션(인젝션 불가)
@Autowired
@Qualifier("ifList")
List<IF> ifList;

@Autowired
@Qualifier("ifMap")
Map<String, IF> ifMap;


//@Resource 어노테이션을 통한 필드 인젝션(인젝션 가능)
@Resource
List<IF> ifList;

@Resource
Map<String, IF> ifMap;

Bean을 정의할 때 List나 Map 형태로 정의하는 것에 대한 문제는 없다.

다만 인젝션 시에 @Autowired를 통해서는 주입이 불가능하고 @Resource 어노테이션을 통해서만 주입이 가능하다는 제한이 있다.

 

 

 

2. ComponentScan

 

이전 포스팅인 Bean 설정방식, DI 방식에서 ComponentScan이 언급되었었다.

컴포넌트 스캔은 Class Loader를 스캔하면서 특정 클래스를 찾은 다음 IoC 컨테이너에 등록하는 방법을 말한다.

 

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

그 특정 어노테이션의 종류는 아래와 같다.

  1. @Component (org.springframework.stereotype.Component)
  2. @Controller (org.springframework.stereotype.Controller)
  3. @Service (org.springframework.stereotype.Service)
  4. @Repository (org.springframework.stereotype.Repository)
  5. @Configuration (org.springframework.context.annotation.Configuration)
  6. @RestController (org.springframework.web.bind.annotation.RestController)
  7. @ControllerAdvice (org.springframework.web.bind.annotation.ControllerAdvice)
  8. @ManagedBean (javax.annotation.ManagedBean)
  9. @Named (javax.inject.Named)

컴포넌트 스캔을 하기 위해서는 자바기반 설정방식에서는 @ComponentScan을,

XML 방식에서는 <context:component-scan>을 사용한다.

컴포넌트 스캔을 할 때는 ClassLoader에서 위와 같은 어노테이션을 찾아야 하기 때문에 탐색 범위가 넓고 처리하는 시간도 오래걸린다.

이 시간은 결국 처리시간을 증가시키는 원인이 되기 때문에 탐색범위를 넓게 설정하는 것은 성능면에서 좋지 않다.

가능한 한 광범위한 범위설정을 피해야 하기 때문에 통상 애플리케이션의 최상위나 한단계 아래의 패키지 까지를 스캔 대상으로 잡는것이 적절하다.

 

//범위가 광범위한경우(설정이 부적절)
@ComponentScan(basePackages = "com")

@ComponentScan(basePackages = "com.example")


//범위가 적절한 경우(설정이 적절)
@ComponentScan(basePackages = "com.example.demo")

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

 

이때 속성은 basePackages 대신 value 속성을 써도 된다.

어느쪽을 사용해도 스캔하는데는 문제가 없으나 이 속성을 생략하는 경우 @ComponentScan이 설정된 클래스가 속한 패키지부터 하위 패키지만 스캔한다는 것에 유의해야 한다.

 

이 컴포넌트 스캔 대상이 되는 어노테이션 중 가장 많이 활용되는 네가지는 다음과 같다.

1. @Controller

  MVC 패턴에서의 C. 즉, 컨트롤러 역할을 하는 컴포넌트에 붙이는 어노테이션.

  클라이언트에서 오는 요청을 받고 비즈니스 로직의 처리 결과를 응답으로 돌려보내는 기능을 한다.

  이때 실제 비즈니스 로직은 @Service가 붙은 컴포넌트에서 처리하도록 위임한다.

 

2. Service

  비즈니스 로직(service)을 처리하는 컴포넌트에 붙이는 어노테이션.

  컨트롤러에서 받은 입력 데이터를 활용해 비즈니스 로직을 실행하는 기능을 한다.

  이때 영속적으로 보관해야 하는 데이터가 있다면 @Repository가 붙은 컴포넌트에서 처리하도록 위임한다.

 

3. @Repository

  영속적인 데이터 처리를 수행하는 컴포넌트에 붙이는 어노테이션.

  ORM 관련 라이브러리를 활용해 데이터의 CRUD를 처리하는 기능을 한다.

 

4. @Component

  위의 세 경우에 해당하지 않는 컴포넌트(유틸리티 클래스나 기타 지원 클래스 등)에 붙이는 어노테이션.

 

 

이 컴포넌트 스캔 대상들 외에도 추가로 다른 컴포넌트를 더 포함하고 싶다면 필터를 적용해 스캔 범위를 커스터마이징 할 수 있다.

 

스프링에서는 아래와 같은 필터를 제공한다.

  1. 어노테이션을 활용한 필터(ANNOTATION)
  2. 할당 가능한 타입을 활용한 필터(ASSIGNABLE_TYPE)
  3. 정규 표현식 패턴을 활용한 필터(REGEX)
  4. AspecjJ 패턴을 활용한 필터(ASPECTJ)

 

아래는 필터 예제다.

//할당 가능한 타입으로 필터링(인터페이스)
public interface DomainService {

}


//할당 가능한 타입으로 필터링(자바 기반 설정 방식)
@ComponentScan(basePackages = "com.example.demo"
               , includeFilters = {
                           @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE
                                                 , classes = {DomainService.class})
                                  })
                                  


//정규표현식 패턴으로 필터링(자바 기반 설정 방식)
@ComponentScan(basePackages = "com.example.demo"
               , includeFilters = {
                           @ComponentScan.Filter(type = FilterType.REGEX
                                                 , pattern = {".+DomainService$"})
                                  })
<!-- 할당 가능한 타입으로 필터링(XML 기반 설정 방식) -->
<context:component-scan base-package="com.example.demo">
    <context:include-filter type="assignable" expression="com.example.demo.domain.DomainService"/>
</context:component-scan>


<!-- 정규 표현식 패턴으로 필터링(XML 기반 설정 방식) -->
<context:component-scan base-package="com.example.demo">
    <context:include-filter type="regex" expression=".+DomainService$"/>
</context:component-scan>

여기서 주의해야 할 점은 필터를 적용해서 컴포넌트 스캔을 할 때 앞서 살펴본 어노테이션이 붙은 스캔 대상도 함께 탐색 범위에 포함된다는 것이다.

만약 기본 설정에서 어노테이션이 붙은 스캔 대상을 무시하고, 순수하게 필터를 적용해서 탐색되는 컴포넌트만 사용하고 싶다면 아래와 같이 useDefaultFilters 속성을 false로 설정하면 된다.

 

그리고 기본 스캔 대상에 필터를 적용해 특정 컴포넌트를 추가하는 것과 반대로 빼는 방법도 있는데 이때는 excludeFilter 속성을 사용하면 된다.

 

//기본 스캔 대상을 제외하고 필터로만 스캔(자바 기반 설정 방식)
@ComponentScan(basePackages = "com.example.demo"
               , useDefaultFilters = false
               , includeFilters = {
                           @ComponentScan.Filter(type = FilterType.REGEX
                                                 , pattern = {".+DomainService$"})
               })



//기본 스캔 대상과 특정 컴포넌트(@Exclude 어노테이션이 붙은 컴포넌트)를 제외하고 필터로만 스캔
@ComponentScan(basePackages = "com.example.demo"
               , useDefaultFilters = false
               , includeFilters = {
                           @ComponentScan.Filter(type = FilterType.REGEX
                                                 , pattern = {".+DomainService$"})
               })
               , excludeFilters = {
                           @ComponentScan.Filter(type = FilterType.ANNOTATION
                                                 , pattern = {"Exclude.class"})
               })
<!-- 기본 스캔 대상을 제외하고 필터로만 스캔(XML 기반 설정 방식) -->
<context:component-scan base-package="com.example.demo" use-default-filters="false">
    <context:include-filter type="regex" expression=".+DomainService$"/>
</context:component-scan>


<!-- 기본 스캔 대상과 특정 컴포넌트(@Exclude 어노테이션이 붙은 컴포넌트)를 제외하고 필터로만 스캔 -->
<context:component-scan base-package="com.example.demo" use-default-filters="false">
    <context:include-filter type="regex" expression=".+DomainService$"/>
    <context:exclude-filter type="annotation" expression="com.example.demo.Exclude"/>
</context:component-scan>

 

만약 포함(include)하는 필터와 제외(exclude)하는 필터 모두에 해당하는 컴포넌트가 있는 경우 제외하는 필터가 포함하는 필터보다 우선순위가 높기 때문에 해당 컴포넌트는 스캔 대상에서 제외되고 결과적으로 IoC 컨테이너에도 등록되지 않는다.

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

 

 

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. WebApplicationContext 계층구조

2. Web Application의 context 구성방법

3. RootApplicationcontext, ServletApplicationContext 등록

 

 


이번 포스팅은 웹 애플리케이션 안에 WebApplicationContext 타입의 IoC 컨테이너를 두는 방법을 정리한다.

 

자바 서버에는 하나 이상의 웹 모듈을 배치해서 사용할 수 있다.

스프링을 사용하면 보통 독립적으로 배치 가능한 웹 모듈(WAR) 형태로 애플리케이션을 배포하고 하나의 웹 애플리케이션은 여러개의 servlet을 가질 수 있다.

 

자바 서버 기술이 막 나온 시기에는 URL 하나당 하나의 servlet을 만들어 등록하고 각각 독립적인 기능을 담당하게 했지만

최근에는 많은 웹 요청을 한번에 받을 수 있는 대표 servlet을 등록해두고, 공통적인 선행 작업을 수행하게 한 후에,

각 요청의 기능을 담당하는 Handler라는 클래스를 호출하는 방식으로 개발하는 경우가 일반적이다.

이와같이 몇개의 servlet이 중앙 집중식으로 모든 요청을 다 받아 처리하는 방식을 Front Controller Pattern이라고 한다.

스프링도 프론트 컨트롤러 패턴을 사용하기 때문에 스프링 웹 애플리케이션에 사용되는 servlet의 숫자는 하나이거나 많아야 두세개 정도이다.

 

웹 애플리케이션 안에서 동작하는 IoC 컨테이너는 두가지 방법으로 만들어진다.

하나는 스프링 애플리케이션의 요청을 처리하는 servlet 안에서 만들어지는 것이고,

다른 하나는 웹 애플리케이션 레벨이서 만들어지는 것이다.

일반적으로는 이 두가지 방식을 모두 사용해 컨테이너를 만들기 때문에 두개의 컨테이너 즉, WebApplicationContext 오브젝트가 만들어진다.

스프링 애플리케이션의 진입 창구 역할을 하는 프론트 컨트롤러 servlet이 한개 이상 등록된다면, 그만큼 전체 컨테이너 개수는 더 늘어난다.

 

 

1. WebApplicationContext 계층구조

 

웹 애플리케이션 레벨에 등록되는 컨테이너는 보통 RootWebApplicationContext라고 불린다.

이 컨텍스트는 servlet 레벨에 등록되는 컨테이너들의 부모 컨테이너가 되고 일반적으로 전체 계층구조 내에서 가장 최 상단에 위치한 Root Context가 되기 때문이다.

웹 애플리케이션에는 스프링 애플리케이션의 프론트 컨트롤러 역할을 하는 servlet이 한개 이상 등록될 수 있는데 이 servlet에는 각각 독립적으로 ApplicationContext가 만들어진다.

이런 경우 각 서블릿이 공유하게 되는 공통적인 Bean들이 있고, 이런 Bean들을 웹 애플리케이션 레벨의 컨텍스트에 등록하면 된다.

그러면 공통되는 Bean들이 servlet 별로 중복돼서 생성되는것을 방지할 수 있다.

 

출처 : 토비의 스프링 3.1

 

 

위 그림에서 Servlet A 와 Servlet B는 각각 자신의 전용 ApplicationContext를 갖고 있다.

동시에 두 컨텍스트가 공유해서 사용하는 Bean을 담아놓을 수 있는 컨텍스트가 존재하는데 이 컨텍스트는 각 서블릿에 존재하는 컨텍스트의 부모 컨텍스트로 만들어줘서 최상단에 위치하는 컨텍스트인 Root Context가 된다.

이렇게 구성하게 되면 Servlet A와 Servlet B의 컨텍스트는 서로 독립적인 Bean을 생성해서 동작하고 공통적인 Bean은 부모 컨텍스트인 RootApplicationContext가 만든 것을 공유해서 사용할 수 있다.

스프링에서 ApplicationContext 계층구조가 사용되는 가장 대표적인 경우다.

 

하나의 servlet이 웹 애플리케이션에 들어오는 모든 애플리케이션 요청을 처리할 수 있는 프론트 컨트롤러 역할을 하는데 굳이 두개 이상으로 나눠서 servlet을 구성하고 요청을 분산해야 하는가에 대해 의문이 생길 수 있다.

이렇게 두개의 servlet을 두고 사용하는 경우가 많지는 않은데 특별한 이유로 기존에 만들어진 servlet 레벨의 ApplicationContext와 그 설정을 그대로 유지하면서 새로운 기능이나 별도의 웹 기술을 추가하고 싶은 경우에 사용된다.

일반적으로는 스프링의 ApplicationContext를 가지면서 프론트 컨트롤러 역할을 하는 servlet은 하나만 만들어서 사용한다.

 

그럼 이렇게 프론트 컨트롤러의 역할을 하는 servlet을 하나만 만들어서 사용할건데 계층구조를 만드는 이유는??

전체 애플리케이션에서 웹 기술에 의존적인 부분과 그렇지 않은 부분을 구분하기 위해서다.

스프링을 사용한다고 해서 스프링이 제공하는 웹 기술만을 사용해야 하는 것은 아니다.

데이터 액세스 계층이나 서비스 계층은 스프링 기술을 사용하고 스프링 Bean으로 만들지만 웹을 담당하는 프레젠테이션 계층은 스프링 외의 기술을 사용하는 경우도 있기 때문이다.

예를들어 웹 서비스 엔진이나 Ajax 프레임워크에서 받은 요청을 스프링으로 전달해서 처리해야 하는 경우 사용할 스프링 Bean은 대부분 서비스 계층이나 데이터 액세스 계층에 속해있다.

이런 경우 때문에 스프링 Servlet을 사용하는 스프링의 웹 기술 외의 다른 웹 기술을 고려중이라면 계층형태로 구분하는 것이 바람직하다.

 

스프링에서는 웹 애플리케이션마다 하나씩 존재하는 Servlet Context를 통해 RootApplicationContext에 접근할 수 있는 방법을 제공한다.

이를 통해 Ajax 엔진 같은 곳에서 RootApplicationContext에 접근할 수 있다.

WebApplicationContextUtils.getWebApplicationContext(ServletContext sc)

 

 

이런 스프링의 간단한 유틸리티 메소드를 이용하면 스프링 밖의 어디서라도 웹 애플리케이션의 RootApplicationContext를 얻을 수 있다.

그리고 getBean() 메소드를 사용하면 RootContext의 어떤 Bean이든 가져와 쓸 수 있다.

ServletContext는 웹 애플리케이션마다 하나씩 만들어지는 것으로, Servlet의 runtime 환경정보를 담고있다.

HttpServletRequest나 HttpSession 오브젝트를 갖고 있다면 간단히 ServletContext를 가져올 수 있는데 스프링과 연동돼서 사용할 수 있는 서드파티 웹 프레임워크는 이 방법을 사용해 스프링 Bean을 가져와 사용한다.

ServletContext에 접근할 수 있는 JSP나 일반 Servlet에서도 가능하다.

 

프레젠테이션 계층을 분리해서 계층구조로 ApplicationContext를 구성해두면 언제든 간단히 웹 기술을 확장하거나 변경, 조합해서 사용할 수 있으므로 당장에는 스프링 servlet 하나만 존재한다고 해도 계층 구조로 만들어두는것이 권장된다.

 

계층구조가 만들어지기 때문에 계층구조에서의 주의사항을 항상 염두에 두어야 한다.

계층구조에서는 부모 컨텍스트와 자식 컨텍스트가 동일한 이름의 Bean을 갖고 있다면 자식 컨텍스트의 Bean이 우선된다.

예를들어 hello라는 Bean이 ChildContext와 ParentContext에 동일하게 hello라는 이름으로 존재한다고 하고 hello Bean을 요청했을 때 ChildContext에 존재하는지 먼저 확인한 뒤에 존재하지 않으면 ParentContext에서 찾기 때문에 ChildContext에 있는 Bean을 우선적으로 가져오게 된다.

또한 자바에서 상속받았을 때 처럼 자식 컨텍스트의 Bean은 부모 컨텍스트의 Bean을 참조할 수 있지만 반대로 부모 컨텍스트가 자식 컨텍스트의 Bean을 참조하는것은 불가능하다.

 

웹 애플리케이션에서 계층구조는 ServletContext와 RootApplicationContext 두가지의 구조가 만들어지고 ServletContext가 자식 컨텍스트, RootApplicationContext가 부모 컨텍스트다.

그럼 Servlet Context의 Bean은 RootApplicationContext의 Bean을 참조할 수 있지만 반대는 불가능하고,

두 컨텍스트에 동일한 이름의 Bean이 존재한다면 RootApplicationContext보다 ServletContext에 존재하는 Bean이 더 우선되어 RootApplciationContext의 Bean은 무시될 수 있다.

하나의 컨텍스트에 정의된 AOp 설정은 다른 컨텍스트의 Bean에는 영향을 미치지 않는다는 점도 주의해야 한다.

 

스프링 애플리케이션의 XML 설정 파일은 보통 계층이나 성격에 따라 여러개의 파일로 분리해서 작성하면 편리하다.

이런 경우 각 설정파일마다 하나씩 컨텍스트를 만들고 계층구조로 묶는 방법도 가능하겠지만, 반대로 하나의 컨텍스트가 여러개의 설정 파일을 사용하도록 할 수도 있다.

같은 컨텍스트가 사용할 Bean 설정이라면 굳이 파일을 여러개 쪼개서 작성하지 않고 하나에 작성하는 것이 더 낫지 않을까 하는 생각도 할 수 있지만 등록되는 빈의 개수가 많은 경우 파일 하나에 설정정보의 양이 너무 많아지게 되고 작성하거나 관리하는데 불편해지기 때문에 계층별로 구분해두거나 자주 바뀌는 설정과 고정된 설정을 구분하는게 좋다.

 

 

2. Web Application의 context 구성방법

웹 애플리케이션의 ApplicationContext를 구성하는 방법으로는 다음 세가지를 고려해볼 수 있다.

 

1. ServletContext와 RootApplicationContext 계층구조

 

가장 많이 사용되는 기본적인 구성방법이다.

스프링 웹 기술을 사용하는 경우 웹 관련 Bean들은 Servlet의 컨텍스트에 두고 나머지는 RootApplicationContext에 등록한다.

Root 컨텍스트는 모든 Servlet 레벨 컨텍스트의 부모 컨텍스트가 된다.

스프링 웹 외에도 기타 웹 프레임워크나 HTTP 요청을 통해 동작하는 각종 서비스를 함께 사용할 수도 있다.

 

 

2. RootApplicationContext 단일구조

 

스프링 웹 기술을 사용하지 않고 서드파티 웹 프레임워크나 서비스엔진만을 사용해서 프레젠테이션 계층을 만든다면 스프링 Servlet을 둘 이유가 없다.

따라서 servlet의 ApplciationContext도 사용하지 않게 된다.

이때는 RootApplicationContext만 등록해주면 된다.

 

 

3. ServletContext 단일구조

 

스프링 웹 기술을 사용하면서 스프링 외의 프레임워크나 서비스 엔진에서 스프링의 Bean을 이용할 생각이 아니라면 RootApplicationContext를 생락할 수도 있다.

대신 Servlet에서 만들어지는 컨텍스트에 모든 Bean을 다 등록하면 된다.

계층구조를 사용하면서 발생할 수 있는 혼란을 근본적으로 피하고 단순한 설정을 선호한다면 이 방법을 선택할 수 있다.

이때는 servlet안에 만들어지는 ApplicationContext가 부모 컨텍스트를 갖지 않기 때문에 스스로 Root 컨텍스트가 된다.

이렇게 만들어지는 Servlet context는 컨텍스트 계층 관점에서 보자면 Root 컨텍스트이지만 웹 애플리케이션 레벨에 두는 공유 가능한 Root 컨텍스트와는 구별된다.

 

 

이 방법들 중 첫번째 방법은 컨텍스트 계층구조를 만드는 방법이고 나머지 두 방법은 단일구조로 컨텍스트를 하나만 사용하는 방법이다.

첫번째와 세번째 방법은 스프링 웹 기능을 사용하는 경우이고, 두번째 방법은 스프링 웹 기술을 사용하지 않을 때 적용 가능한 방법이다.

 

 

3. RootApplicationcontext, ServletApplicationContext 등록

 

RootApplicationContext를 먼저 정리하고 그 다음 ServletApplicationContext를 정리한다.

 

웹 애플리케이션 레벨에 만들어지는 Root Web ApplicationContext를 등록하는 가장 간단한 방법은 servlet의 이벤트리스너(EventListener)를 이용하는 것이다.

스프링은 웹 애플리케이션의 시작과 종료시에 발생하는 이벤트를 처리하는 리스너인 ServletContextListener를 이용한다.

ServletContextListener 인터페이스를 구현한 리스너는 웹 애플리케이션 전체에 적용 가능한 DB 연결 기능이나 logging 같은 서비스를 만드는데 유용하게 쓰인다.

이를 이용해 웹 애플리케이션이 시작될 때 RootApplicationContext를 만들어 초기화하괴, 웹 애플리케이션이 종료될 때 컨텍스트를 함께 종료하는 기능을 가진 리스너를 만들수도 있다.

스프링은 이러한 기능을 가진 리스너인 ContextLoaderListener를 제공한다.

사용하는 방법은 아래처럼 web.xml 파일 안에 리스너 선언을 넣어주기만 하면 된다.

 

<listener>
	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

 

ContextLoaderListener는 웹 애플리케이션이 시작할 때 자동으로 RootApplicationContext를 만들고 초기화해준다.

리스너에 별다른 파라미터를 지정하지 않으면 default로 설정된 아래의 값이 적용된다.

  • ApplicationContext 클래스 : XmlWebApplicationContext
  • XML 설정 파일 위치 : /WEB-INF/applicationContext.xml

RootApplicationContext는 웹 애플리케이션의 WEB-INF 폴더 내에 있는 applicationContext.xml 파일을 default 설정 파일로 사용한다.

Context Class와 설정파일 위치는 Servlet Context 파라미터를 선언해서 변경할 수 있다.

ContextLoaderListener가 이용할 파라미터를 <context-param> 항목 안에 넣어주면 default 설정 대신 파라미터로 지정한 내용이 적용된다.

 

● contextConfigLocation

default XML 설정 파일 위치는 파라미터를 선언해주는 것으로 바꿀 수 있다.

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
    	/WEB-INF/daoContext.xml
        /WEB-INF/applicationContext.xml
        <!-- 하나 이상의 XML 설정 파일을 사용할 경우
        	여러줄에 걸쳐 넣어주거나 공백으로 분리하면 된다. -->
    </param-value>
</context-param>

이렇게 contextConfigLocation context 파라미터를 넣어주면 default 설정파일 위치인 /WEB-INF/applicationContext.xml은 무시되고 파라미터로 제공된 설정파일을 사용하게 된다.

 

위 예제에 /WEB-INF/applicationContext.xml 도 있는데 왜 무시된다는건가? 라고 생각했었는데 이 설정이 없으면 default 설정파일 위치인 /WEB-INF/applicationContext.xml만을 설정 파일로 사용하는 것이고, 위 예제 처럼 contextConfigLocation 컨텍스트 파라미터를 넣어주면 /WEB-INF/daoContext.xml과 /WEB-INF/applicationContext.xml을 설정 파일로 사용하게 된다.

즉, 위 예제에서 applicationContext.xml을 빼고 다른 xml 설정 파일 경로를 작성한다면 applicationContext.xml 말고 그 파일을 설정파일로 사용하게 된다는 의미다.

 

설정 파일의 위치는 resource loader가 사용하는 접두어를 사용해 표현할 수도 있으며 접두어를 붙이지 않으면 웹 애플리케이션의 servlet resource path로부터 파일을 찾는다.

그래서 보통 /WEB-INF/로 시작하게 된다.

servlet resource path 대신 classpath로부터 설정파일을 찾게 할 수도 있는데 다음과 같이 classpath: 를 붙여주면 된다.

<param-value>classpath:daoContext.xml</param-value>

 

ANT 스타일의 경로표시 방법을 이용하면 한번에 여러개의 파일을 지정할 수도 있다.

  • /WEB-INF/*Context.xml : WEB-INF 밑의 Context.xml로 끝나는 모든 파일을 지정한다.
  • /WEB-INF/**/*Context.xml : WEB-INF 밑의 모든 서브폴더에서 Context.xml로 끝나는 모든 파일을 지정한다.

애플리케이션의 규모가 커져서 등록해야 할 Bean이 많아지면 Bean 설정을 여러개의 파일로 쪼개서 관리하는게 편리할 수 있다.

계층별로 구분하거나 기능 모듈별로 파일을 분리해서 만드는 방법도 좋다.

설정파일이 여러개 만들어졌다고 해서 컨텍스트도 여러개가 만들어지는건 아니고 하나의 Root 컨텍스트가 여러 파일의 Bean 설정 메타정보를 통합해서 사용할 뿐이다.

 

 

● contextClass

ContextLoaderListener가 자동으로 생성하는 컨텍스트의 클래스는 기본적으로 XmlWebApplicationContext다.

이를 다른 ApplicationContext 구현 클래스로 변경하고 싶으면 contextClass 파라미터를 이용해 지정해주면 된다.

여기에 사용될 컨텍스트는 반드시 WebApplicationContext 인터페이스를 구현해야 한다.

XmlWebApplicationContext외에 스프링이 제공하는 대체 가능한 컨텍스트 클래스는 AnnotationConfigWebApplicationContext다.

이 클래스는 XML 설정 대신 소스코드 내의 annotation 선언과 특별하게 만들어진 자바 코드를 설정 메타정보로 활용하는 것이다.

이 annotation 설정에 대한것은 이후 포스팅에서 따로 정리한다.

 

아래 예제는 AnnotationConfigWebApplicationContext를 사용해 RootApplicationContext를 생성하도록 지정한 것이다.

<context-param>
    <param-name>contextClass</param-name>
    <param-value>
    	org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
</context-param>

AnnotationConfigWebApplicationContext를 contextClass로 사용할 때는 contextConfigLocation 파라미터를 반드시 선언해줘야 한다.

이때는 XML 파일 위치가 아니라 설정 메타정보를 담고 있는 클래스 또는 Bean Scanning package를 지정할 수 있다.

 

 

이번에는 Servlet ApplicationContext 등록에 대해 정리한다.

스프링의 웹 기능을 지원하는 Front Controller Servlet은 DispatcherServlet이다.

DispatcherServlet은 이름에서 알 수 있듯이 web.xml에 등록해서 사용할 수 있는 평범한 servlet이다.

servlet 이름을 다르게 지정해주면 하나의 웹 애플리케이션에 여러개의 DispatcherServlet을 등록할 수도 있다.

각 DispatcherServlet은 servlet이 초기화 될 때 자신만의 컨텍스트를 생성하고 초기화 한다.

동시에 웹 애플리케이션 레벨에 등록된 RootApplicationContext를 찾아서 이를 자신의 부모 컨텍스트로 사용한다.

 

 

아래 예제는 Servlet ApplicationContext를 등록하는 방법이다.

<!-- web.xml -->
<servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>
    	org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

 

DispatcherServlet을 등록할 때 신경써야 할 사항은 다음 두가지다.

● <servlet-name>

DispatcherServlet에 의해 만들어지는 ApplicationContext는 모두 독립적인 네임스페이스를 갖게 된다.

이 네임스페이스는 servlet단위로 만들어지는 컨텍스트를 구분하는 키가 된다.

네임스페이스는<servlet-name> 으로 지정한 servlet 이름에 -servlet을 붙여서 만들면 되는데

예를들어 spring이라는 이름의 servlet의 네임스페이스를 만들면 spring-servlet이 된다.

네임스페이스가 중요한 이유는 DispatcherServlet이 사용할 default XML 설정 파일의 위치를 네임스페이스를 이용해 만들기 때문이다.

Servlet Context가 사용할 default 설정파일은 '/WEB-INF/' + servlet name space + '.xml' 과 같은 규칙으로 만들어진다.

따라서 <servlet-name>을 spring이라고 했다면 default 설정파일 위치는 /WEB-INF/spring-servlet.xml이 된다.

이렇게 servlet 이름을 이용해 네임스페이스를 만드는 이유는 여러개의 DispatcherServlet이 등록되더라도 각각 구분할 수 있고, 자신만의 default 설정파일을 가질 수 있도록 하기 위해서다.

 

● <load-on-startup>

<load-on-startup>은 servlet 컨테이너가 등록된 servlet을 언제 만들고 초기화 할지, 또 그 순서는 어떻게 되는지를 지정하는 정수값이다.

이 항목을 아예 생략하거나 음의 정수로 넣으면 해당 servlet은 servlet 컨테이너가 임의로 정한 시점에서 만들어지고 초기화된다.

반대로 0 이상의 값을 넣으면 웹 애플리케이션이 시작되는 시점에서 servlet을 로딩하고 초기화한다.

또한 여러개의 servlet이 등록되어 있다면 작은 수를 가진 servlet이 우선적으로 만들어진다.

DispatcherServlet은 servlet의 초기화 작업 중에 스프링 컨텍스트를 생성한다.

컨텍스트의 설정이나 환경에 문제가 있다면 컨텍스트 생성 시 대부분 확인이 가능하기 때문에 웹 애플리케이션이 시작되고 가능한 한 빨리 ServletContext의 초기화가 진행되는 것이 바람직하다.

그래야만 컨텍스트와 Bean의 초기화 작업을 통해 문제를 빨리 파악할 수 있기 때문이다.

<load-on-startup>의 값은 보통 1을 넣어준다.

 

RootApplicationContext는 서비스 계층과 데이터 액세스 계층의 빈을 모두 포함하고 있고, 그 외에도 각종 기반 서비스와 기술 설정을 갖고 있다.

따라서 설정파일을 여러개로 구분해두고 default 설정파일 위치 대신 <context-param>으로 지정된 설정 파일 위치를 사용하는 경우가 많다.

그에 반해 servlet이 사용하는 설정파일은 굳이 여러개로 구분해서 분리할 필요가 없는 경우가 대부분이다.

따라서 특별한 경우가 아니라면 servlet 이름 + '-servlet.xml'이라는 ServletContext의 default 설정 파일 이름을 따르는 것이 간편하다.

DispatcherServlet의 컨텍스트에 대한 default 설정을 변경하고 싶다면 RootApplicationContext와 마찬가지로 contextConfigLocation과 contextclass를 지정해줄 수 있다.

ServletContext의 파라미터 선언 방법은 RootContext와 거의 비슷하다.

파라미터의 선언에 <context-param> 대신 <servlet> 안에 있는 <init-param> 을 이용한다는 점만 다르다.

 

Servlet 설정 파일을 default를 쓰지 않고 여러개로 분리해야 할 때가 있는데 RootApplicationContext를 사용하지 않고 모든 계층의 Bean을 ServletContext 안에 등록하는 단일 ServletContext 구성방법을 사용하는 경우다.

웹 계층 외의 Bean을 정의한 applicationContext.xml과 웹 계층을 위한 spring-servlet.xml을 ServletContext가 모두 사용하게 한다면 아래와 같이 선언하면 된다.

이렇게 하면 리스너를 통한 RootContext의 등록은 생략할 수 있다.

<servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-value>contextConfigLocation</param-value>
        <param-value>
            /WEB-INF/applicationContext.xml
            /WEB-INF/spring-servlet.xml
        </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

RootContext 설정과 마찬가지로 <init-param>에 contextClass 파라미터를 정의해서 context class도 변경할 수 있다.

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. IoC Container의 사용방법

2. IoC 컨테이너의 종류

 

1. IoC Container의 사용방법

IoC 컨테이너의 종류를 확인하기 전에 IoC 컨테이너를 사용하는 애플리케이션을 만드는 방법과 동작원리에 대해 정리한다.

가장 간단하게 IoC 컨테이너를 만드는 방법은 ApplicationContext 구현 클래스의 인스턴스를 만드는 것이다.

그리고 이 컨테이너를 본격적인 IoC 컨테이너로 동작시키기 위해 POJO 클래스와 설정 메타정보가 필요하다.

 

토비의 스프링 3.1에 나온 예제로 클래스를 생성한다.

지정된 사람에게 인사를 하는 Hello 클래스와 

메세지를 받아 이를 출력하는 Printer 인터페이스,

이를 구현한 StringPrinter 클래스와 다른 방법으로 구현한 ConsolePrinter 클래스를 만들어준다.

 

출처 : 토비의 스프링 3.1

 

//Hello Class
public class Hello{
	
    String name;
    Printer printer;
    
    public String sayHello(){
    	return "Hello " + name;
        //property로 DI 받은 이름을 이용해 만드는 간단한 인사문구
    }
    
    public void print(){
    	this.printer.print(sayHello());
        /*
        	DI에 의해 의존 오브젝트로 제공받은 Printer 타입의 Object에게 출력작업을 위임.
            구체적으로 어떤 방식으로 출력하는지는 상관하지 않는다.
            또한 어떤 방식으로 출력하도록 변경해도 Hello 클래스의 코드는 수정할 필요가 없다.
        */
    }
    
    public void setName(String name){
    	this.name = name;
    }
    
    public void setPrinter(Printer printer){
    	this.printer = printer;
    }
}


//Printer Interface
public interface Printer{
	
    void printer(String message);

}


//StringPrinter class
public class StringPrinter implements Printer{
	
    private StringBuffer buffer = new StringBuffer();
    
    @Override
    public void print(String message){
    	this.buffer.append(message);
        //Printer 인터페이스의 메소드 구현.
        //buffer에 메세지를 추가해준다.
    }
    
    public String toString(){
    	return this.buffer.toString();
        //buffer에 추가해둔 메세지를 String으로 가져온다.
    }
}


//ConsolePrinter Class
public class ConsolePrinter implements Printer{
	
    @Override
    public void print(String message){
    	System.out.println(message);
    }
}

 

Hello 클래스는 Printer 인터페이스에만 의존하고있으며 구현클래스인 StringPrinter, ConolePrinter와 Printer인터페이스를 사이에 두고 느슨하게 연결되어있다.

서로 구체적으로 알 필요도 없고 관계를 맺을 수 있도록 필요한 최소한의 인터페이스 정보만 공유하면 된다.

실제로 Hello클래스는 runtime시에 어떤 구체적인 클래스의 오브젝트를 사용하게 될지 알지도 못하고 관심도 없다.

그래서 Printer 인터페이스의 구현 클래스를 변경하더라도 Hello클래스의 코드 변경은 필요 없고 단지 런타임시에 오브젝트를 연결해주는 IoC 컨테이너의 도움만 있으면 된다.

 

이렇게 각자 기능에 충실하게 독립적으로 설계된 POJO 클래스를 만들고 결합도가 낮은 유연한 관계를 가질 수 있도록 인터페이스를 이용해 연결해주는 것 까지가 IoC 컨테이너가 사용할 POJO를 준비하는 단계다.

 

다음은 설정 메타정보가 필요하다.

POJO클래스들 중에 애플리케이션에서 사용할 것을 선정하고 이를 IoC 컨테이너가 제어할 수 있도록 적절한 메타정보를 만들어 제공하는 작업이다.

 

IoC 컨테이너의 가장 기초적인 역할은 오브젝트를 생성하고 이를 관리하는 것인데 컨테이너가 관리하는 오브젝트를 Bean이라고 부른다.

IoC 컨테이너가 필요로 하는 설정 메타정보는 이 Bean을 어떻게 만들고 어떻게 동작하게 할 것인가에 대한 정보다.

 

스프링의 메타정보는 XML 파일만을 지칭하는 것이 아니다.

주로 XML에 담긴 내용을 읽어서 활용하는것은 사실이지만, 스프링의 설정 메타정보는 BeanDefinition 인터페이스로 표현되는 순수한 추상 정보다.

스프링 IoC 컨테이너, 즉 ApplicationContext는 이 BeanDefinition으로 만들어진 메타정보를 담은 오브젝트를 사용해 IoC와 DI 작업을 수행한다.

어떤 파일이던 상관없이 BeanDefinition으로 정의되는 스프링의 설정 메타정보 내용을 표현한 것이 있다면 무엇이든 사용가능하고 BeanDefinition 오브젝트로 변환해주는 BeanDefinitionReader가 있으면 된다.

BeanDefinitionReader는 인터페이스이고 이를 구현한 리더를 만들어주면 어떤 형식으로든 설정 메타정보를 작성할 수 있다.

 

BeanDefinition 인터페이스로 정의되는 IoC 컨테이너가 사용하는 Bean 메타정보는 대략 다음과 같다.

  • Bean 아이디, 이름, 별칭 : Bean 오브젝트로 구분할 수 잇는 식별자
  • 클래스 또는 클래스 이름 : Bean으로 만들 POJO 클래스 또는 서비스 클래스 정보
  • Scope : singleton, prototype과 같은 빈의 생성 방식과 존재 범위
  • property 값 또는 reference : DI에 사용할 property 이름과 value 또는 참조하는 Bean의 이름
  • constructor 파라미터(constuctor-arg) value 또는 참조 : DI에 사용할 constructor 파라미터 이름과 value또는 참조할 Bean의 이름
  • lazy-loading 여부, 우선 Bean 여부, Autowiring 여부, 부모 Bean 정도, BeanFactory 이름 등

스프링 IoC 컨테이너는 각 Bean에 대한 정보를 담은 설정 메타정보를 읽어들인 후에 이를 참고해 Bean 오브젝트를 생성하고 property나 constructor를 통해 의존 오브젝트를 주입해주는 DI 작업을 수행한다.

이 작업을 통해 만들어지고 DI로 연결되는 오브젝트들이 모여 하나의 애플리케이션을 구성하고 동작하게 된다.

 

일반적으로 설정 메타정보는 XML 파일이나 Annotation과 같은 외부 리소스를 전용 리더가 읽어서 BeanDefinition타입의 오브젝트로 만들어 사용한다.

원한다면 직접 코드에서 BeanDefinition 메타정보를 생성할 수도 있다.

 

 

아래 예제는 Hello 클래스를 IoC 컨테이너에 Bean으로 등록하는 예제다.

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.jupiter.api.Assertions.assertNotNull;

@RunWith(SpringJUnit4Runner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class testClass{
	
    @Test
    public void IoCTest(){
    	//IoC 컨테이너 생성. 생성과 동시에 컨테이너로 동작.
        StaticApplicationContext context = new StaticApplicationContext();
        
        //Hello 클래스를 hello1이라는 이름의 singleton Bean으로 컨테이너에 등록.
        context.registerSingleton("hello1", Hello.class);
        
        //IoC 컨테이너가 등록한 Bean을 생성했는지 확인하기 위해 Bean을 요청하고 Null이 아닌지 확인.
        Hello hello1 = context.getBean("hello1", Hello.class);
        assertNotNull(hello1);
    }
}

 

Bean 메타정보의 항목들은 대부분 default값이 있다.

singleton으로 관리되는 Bean 오브젝트를 등록할 때 반드시 제공해줘야 하는 정보는 Bean의 이름과 POJO클래스 뿐이다.

 

IoC컨테이너가 관리하는 Bean은 오브젝트 단위지 클래스 단위가 아니다.

보통은 클래스당 하나의 오브젝트를 만들기는 하지만 경우에 따라 하나의 클래스를 여러개의 Bean으로 등록하기도 한다.

예를들어 데이터베이스를 여러개 사용해야 한다면 dataSource Bean을 여러개 등록하고 각각 다른 데이터베이스 설정을 지정해서 사용하는 경우가 있다.

 

위 예제처럼 default 메타정보를 사용해 Bean을 등록해주는 방법과는 조금 다른 방법으로 직접 BeanDefinition 오브젝트를 만들어 Bean에 대한 설정정보를 넣어주고 IoC 컨테이너에 등록하는 방법도 있다.

 

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.jupiter.api.Assertions.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/applicationContext.xml"})
public class testClass{

    @Test
    public void IoCTest(){
        //hello1 Bean 생성 후 컨테이너에 등록.
        StaticApplicationContext context = new StaticApplicationContext();
        context.registerSingleton("hello1", Hello.class);

        Hello hello1 = context.getBean("hello1", Hello.class);

        //hello2 Bean 생성 후 컨테이너에 등록
        //Bean 메타정보를 담은 오브젝트 생성. Bean Class는 Hello로 지정.
        //<bean class="com.example.Hello" /> 에 해당하는 메타정보
        BeanDefinition helloDef = new RootBeanDefinition(Hello.class);
        
        //Bean의 name property에 들어갈 값을 지정.
        //<property name="name" value="Spring" />에 해당.
        helloDef.getPropertyValues().addPropertyValue("name", "Spring");

        //앞에서 생성한 Bean 메타 정보를 hello2라는 이름을 가진 Bean으로 등록.
        //<bean id="hello2" ..... /> 에 해당
        context.registerBeanDefinition("hello2", helloDef);

        Hello hello2 = context.getBean("hello2", Hello.class);


        //hello2.sayHello()의 출력 결과 확인
        assertEquals(hello2.sayHello(), "Hello Spring");
        //hello1과 hello2가 서로다른 오브젝트인지 확인.
        assertNotEquals(hello1, hello2);

        //IoC 컨테이너에 등록된 Bean 설정 메타정보.
        //Bean의 이름, 등록된 Bean의 개수 등등의 정보를 확인가능.
        for(String a : context.getBeanFactory().getBeanDefinitionNames())
            System.out.println(a);

        assertEquals(context.getBeanFactory().getBeanDefinitionCount(), 2);
    }
}

 

 

IoC 컨테이너는 Bean 설정 메타정보를 담은 BeanDefinition을 이용해 오브젝트를 생성하고 DI 작업을 진행한 뒤에 Bean으로 사용할 수 있도록 등록해준다.

이때 BeanDefinition의 class, property, Bean 아이디 등의 정보가 활용된다.

 

위 예제를 통해 Bean은 오브젝트 단위로 등록되고 만들어지기 때문에 같은 클래스 타입이더라도 서로 다른 Bean 오브젝트를 생성할 수 있다는 것을 확인할 수 있다.

그리고 예제 하단처럼 Bean 설정 메타정보를 확인가능하다.

 

예제 주석에 XML 파일 설정시에 어디에 해당하는 코드인지 적어뒀는데 이 설정을 applicationContext.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" />

    <bean id="hello1" class="com.example.Hello">
    </bean>

    <bean id="hello2" class="com.example.Hello">
        <property name="name" value="Spring">
    </bean>
</beans>

 

그리고 이게 제대로 설정이 된것인지 확인하는 방법은 아래처럼 확인한다.

 

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.jupiter.api.Assertions.*;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/applicationContext.xml"})
public class testClass{
    @Autowired
    private ApplicationContext context;

    @Test
    public void IoCTest(){
        String[] beans = context.getBeanDefinitionNames();

        for(String res : beans)
            System.out.println(res);

        Hello hello2 = context.getBean("hello2", Hello.class);

        assertEquals(hello2.sayHello(), "Hello Spring");
    }
}

 

Hello 클래스와 StringPrinter 클래스는 Printer 인터페이스를 사이에 두고 느슨하고 간접적인 관계를 맺고 있다.

이 두 클래스의 오브젝트간의 관계는 설정 메타정보를 참고해서 runtime시에 IoC 컨테이너가 주입해준다.

 

그럼 위 예제에서 Hello 클래스의 sayHello() 메소드를 호출해 "Hello " + name을 리턴 받았다면 이번에는 Hello 타입과 StringPrinter 타입의 빈을 hello와 printer라는 Bean 이름으로 생성한 뒤 printer Bean이 hello Bean에게 DI 되도록 한다.

 

import com.example.Hello;
import com.example.StringPrinter;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.jupiter.api.Assertions.*;


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/applicationContext.xml"})
public class testClass {

    @Test
    public void IoCTest(){
        StaticApplicationContext context = new StaticApplicationContext();

        //StringPrinter 타입의 printer라는 이름을 가진 bean 등록.
        context.registerBeanDefinition("printer", new RootBeanDefinition(StringPrinter.class));

        //Hello 클래스로 지정된 Bean 메타정보를 담은 오브젝트 생성.
        BeanDefinition helloDef = new RootBeanDefinition(Hello.class);
        //단순 값을 갖는 프로퍼티 등록.
        helloDef.getPropertyValues().addPropertyValue("name", "Spring");
        //아이디가 printer인 Bean에 대한 Reference를 property로 등록
        helloDef.getPropertyValues().addPropertyValue("printer", new RuntimeBeanReference("printer"));
        //앞서 생성한 Bean 메타정보를 hello라는 이름의 Bean으로 등록.
        context.registerBeanDefinition("hello", helloDef);

        Hello hello = context.getBean("hello", Hello.class);
        hello.print();

        assertEquals(context.getBean("printer").toString(), "Hello Spring");
    }
}

Hello 클래스의 print() 메소드는 DI 된 Printer 타입의 오브젝트(StringPrinter)에게 요청해서 사용해야 하기 때문에 결과를 스트림으로 저장해두는 printer Bean을 통해 확인하게 된다.

 

이것 역시 applicationContext.xml에 작성하고 테스트하면 아래처럼 하면 된다.

//applicationContext.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" />

    <bean id="hello1" class="com.example.Hello">
    </bean>

    <bean id="hello2" class="com.example.Hello">
        <property name="name" value="Spring">
        <property name="printer" ref="printer"/>
    </bean>

    <bean id="printer" class="com.example.StringPrinter">
    </bean>
</beans>


//test class
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"file:src/main/webapp/WEB-INF/applicationContext.xml"})
public class testClass{

    @Autowired
    private ApplicationContext context;

    @Test
    public void IoCTest(){
        Hello hell2 = (Hello) context.getBean("hello2");
        hello2.print();

        assertEquals(context.getBean("printer").toString(), "Hello Spring");
    }
}

이렇게 애플리케이션을 구성하는 Bean 오브젝트를 생성하는 것이 IoC 컨테이너의 핵심 기능이다.

IoC 컨테이너는 일단 Bean 오브젝트가 생성되고 관계가 만들어지면 그 뒤로는 거의 관여하지 않는다.

기본적으로 singleton Bean은 ApplicationContext 초기화 작업중에 모두 만들어진다.

 

 

마지막으로 IoC 컨테이너에서 Bean을 가져오는 방법은 아래처럼 몇가지가 존재한다.

Hello hello = context.getBean(Hello.class);
Hello hello = context.getBean("hello1", Hello.class);
Hello hello = (Hello) context.getBean("hello1");

첫번째 방법은 가져오려는 Bean의 타입을 지정하는 방법이다.

지정한 타입에 해당하는 Bean이 IoC 컨테이너에 오직 하나만 존재할 때 사용하며 스프링 3.0.0부터 사용할 수 있다.

 

두번째 방법은 가져오려는 Bean의 이름(hello1)과 타입(Hello.class)을 지정하는 방법이다.

지정한 타입에 해당하는 Bean이 IoC 컨테이너에 여러개 있을 때 이름으로 구분하기 위해 사용한다.

스프링 2.5.6까지는 반환값이 Object 타입이라서 원하는 Bean의 타입으로 형변환 해야 하지만 3.0.0부터는 형변환을 하지 않아도 된다.

 

마지막 방법은 가져오려는 Bean의 이름(hello1)을 지정하는 방법이다.

반환값이 Object 타입이라서 원하는 빈의 타입으로 형변환을 해야 하며 스프링 1.0.0부터 사용할 수 있다.

 

2. IoC Container의 종류

 

ApplicationContext 인터페이스를 바르게 구현했다면 어떤 클래스든 스프링의 IoC 컨테이너로 사용할 수 있다.

스프링에는 다양한 용도로 쓸 수 있는 ApplicationContext 구현 클래스가 존재하기 때문에 개발자가 직접 구현할 일은 거의 없다.

근데 스프링 애플리케이션에서 직접 코드를 통해 ApplicationContext 오브젝트를 생성하는 경우는 거의 없다.

대부분 간단한 설정을 통해 ApplicationContext가 자동으로 만들어지는 방법을 사용하기 때문이다.

 

책을 두개를 보면서 같이 정리하다보니 이 구현클래스 설명에 대한 차이가 있었다.

일단 스프링 철저 입문에서는 아래 네가지를 설명했다.

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의 작업 디렉토리 안에서 상대경로로 설정 파일을 탐색한다.
*/

 

책으로 정리하기 전에 처음 IoC/DI에 대해 접하고 공부할 때 많이 보던 구현클래스들이었다.

 

토비의 스프링 3.1에서는 위 예제들에서 사용했던 StaticApplicationContext, GenericApplicationContext, GenericXmlApplicationContext, WebApplicationContext 이렇게 네가지를 설명했다.

 

IDE를 통해 ApplicationContext.class를 들어가 확인해보면 상속받고 있거나 구현하고 있는 클래스가 22개가 나온다.

당연히 이 두 책에 나온 모든 ApplicationContext를 볼 수 있었고 그 외에도 엄청 많았다.

이게 각 클래스들이 다 ApplicationContext가 최상위에 있는건 맞는데 상속에 상속에 상속 이런식으로 얽혀있는게 많아서 다 보려면 아마 따로 하나하나 다 찾아보면서 봐야할 것 같기 때문에......................

일단은 토비의 스프링에 나와있는 네가지만 설명을 정리한다.

 

1. StaticApplicationContext

StaticApplicationContext는 코드를 통해 Bean 메타정보를 등록하기 위해 사용한다.

스프링의 기능에 대한 예제를 만들 때를 제외하면 실제로는 사용되지 않는다고 한다.

그럼에도 예제에서 사용한 이유는 '스프링 IoC 컨테이너는 파일 포맷이나 리소스 종류에 독립적이며 오브젝트로 표현되는 순수한 메타정보를 사용한다' 라는 것을 보여주기 위함이었다고 한다.

그리고 또 한가지 이유는 스프링 웹에서 예제 테스트로 검증해보고 싶을 때 유용하게 쓸 수 있기 때문이다.

웹 관련 기능 테스트시에는 StaticWebApplicationContext를 사용한다.

StaticApplicationContext는 실전에 사용하지는 않고 테스트 목적으로 Bean을 등록하고 컨테이너가 어떻게 동작하는지 확인하고 싶을 때 사용하면 된다고 한다.

 

2. GenericApplicationContext

GenericApplicationContext는 가장 일반적인 애플리케이션 컨텍스트의 구현 클래스다.

실전에서 사용될 수 있는 모든 기능을 갖추고 있고 컨테이너의 주요 기능을 DI를 통해 확장할 수 있도록 설계되어 있다.

GenericApplicationContext는 StaticApplicationContext와는 달리 XML 파일과 같은 외부의 리소스에 있는 Bean 설정 메타정보를 Reader를 통해 읽어들여 메타정보로 전환해 사용한다.

특정 포맷의 Bean 설정 메타정보를 읽어서 이를 ApplicationContext가 사용할 수 있는 BeanDefinition 정보로 변환하는 기능을 가진 오브젝트는 BeanDefinitnionReader 인터페이스를 구현해서 만들고, Bean 설정 정보 리더 라고 불린다.

XML로 작성된 Bean 설정 정보를 읽어서 컨테이너에게 전달하는 대표적인 Bean 설정정보 리더는 XmlBeanDefinitionReader다.

 

예제 하나를 통해 이 Reader를 GenericApplicationContext가 이용해도록 해 hello Bean과 printer Bean을 등록하고 사용하도록 하는 것을 확인한다.

 

//XML로 만든 Bean 설정 메타정보
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="hello" class="com.example.Hello">
        <property name="name" value="Spring"/>
        <property name="printer" value="printer"/>
    </bean>

    <bean id="printer" class="com.example.StringPrinter"/>
</beans>


//GenericApplicationContext 사용방법 에제
@Test
public void genericApplicationContext(){
    GenericApplicationContext context = new GenericApplicationContext();
    XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context);

    reader.loadBeanDefinitions(
            "applicationContext2.xml"
    );
    //XmlBeanDefinitionReader는 기본적으로 classpath로 정의된 리소스로부터 파일을 읽는다.

    ac.refresh();
    //모든 메타정보가 등록이 완료되었으니 어플리케이션 컨테이너를 초기화 하는 명령.

    Hello hello = context.getBean("hello", Hello.class);
    hello.print();

    assertEquals(context.getBean("printer").toString(), "Hello Spring");
    //검증 내용은 StaticApplicationContext 테스트와 동일하다.
}

 

XmlBeanDefinitionReader는 스프링의 resource loader를 이용해 XML 내용을 읽어온다.

리소스 대신 String을 넘기면 기본적으로 classpath 리소스로 인식한다.

그래서 위 예제에서 applicationContext2.xml의 위치는 src/main/resources/applicationContext2.xml이 된다.

그렇다고 classpath: 를 붙이면 안되는건 아니고 생략했을 때 default로 classpath로 인식한다는 것이다.

그리고 classpath 외에도 file: , http: 같은 접두어를 이용해 구체적인 리소스 타입을 지정할 수 있다.

위에서 얘기했듯이 꼭 xml 파일이 아니더라도 IoC 컨테이너가 사용할 수 있는 BeanDefiniton 오브젝트로 변환할 수만 있따면 설정 메타정보는 어떤 포맷으로 만들어져도 상관없다.

 

GenericApplicationContext는 BeanDefinitionReader를 여러개 사용해서 여러 리소스로부터 설정 메타정보를 읽어들이게도 할 수 있다.

모든 설정 메타정보를 가져온 후에 refresh() 메소드를 한번 호출해서 ApplicationContext가 필요한 초기화 작업을 수행하게 해주면 된다.

스프링을 사용하면서 GenericApplicationContext를 직접 만들어서 사용할 일은 거의 없다.

스프링 컨테이너 자체를 확장해 새로운 프레임워크를 만들거나 스프링을 사용하는 독립형 애플리케이션을 만들지 않는 한 직접 만들어서 이용할 필요는 없다.

하지만 코드로 직접 만들어서 초기화하거나 하는 경우가 없을 분이지 알게모르게 자주 사용된다.

JUnit 테스트는 테스트 내에서 사용할 수 있도록 ApplicationContext를 자동으로 만들어주는데 이때 생성되는 context가 GenericApplicationContext다.

 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml")
public class UserServiceTest{
    @Autowired
    ApplicationContext applicationContext;
    
    ....
}

 

테스트가 실행되면서 GenericApplicationContext가 생성되고 @ContextConfiguration에 지정한 XML 파일로 초기화 돼서 테스트내에서 사용할 수 있도록 준비된다.

 

 

3. GenericXmlApplicationContext

코드에서 GenericApplicationContext를 사용하는 경우에 xmlBeanDefinitionReader를 만들어 설정 메타정보를 읽어야 한다고 했다.

GenericXmlApplicationContext는 이 두 클래스를 결합한 것으로 XmlBeanDefinitionReader를 내장하고 있기 때문에 xml파일을 읽어들이고 refresh()를 통해 초기화 하는 것까지 한 줄로 끝낼 수 있다.

XML파일로 설정을 만들고 ApplicationContext에서 XML을 읽어 사용하는 코드를 시험삼아 만들어볼 필요가 있따면 사용하기에 적당하다.

XML 파일정보는 리소스로더가 읽을 수 있는 형식으로 GenericXmlApplicationContext 생성자에 넣어주면 되며 하나 이상의 파일을 지정할 수도 있다.

 

GenericApplicationContext context = new GenericXmlApplicationContext(
		"file:src/main/webapp/WEB-INF/applicationContext2.xml");
        
Hello hello = context.getBean("hello", Hello.class);

 

4. WebApplicationContext

스프링 애플리케이션에서 가장 많이 사용되는 ApplicationContext다.

ApplicationContext를 확장한 인터페이스이므로 정확하게는 WebApplicationContext를 구현한 클래스를 사용하는 셈이다.

이름 그대로 웹 환경에서 사용할 때 필요한 기능이 추가된 ApplicationContext인데 스프링 애플리케이션은 대부분 servlet 기반의 독립 웹 애플리케이션(war)으로 만들어지기 떄문이다.

WebApplicationContext중에서도 가장 많이 사용되는건 XmlWebApplicationContext다.

XML 이외의 설정정보 리소스도 사용할 수 있는데

Annotation을 이요한 설정리소스만 사용한다면 AnnotationConfigWebApplicationContext를 사용하면 되고 

default는 XmlWebApplicationContext다.

 

WebApplicationContext를 보기 전에 IoC 컨테이너를 적용했을 때 애플리케이션을 기동시키는 방법에 대해 살펴볼 필요가 있다.

IoC Container는 Bean 설정 메타정보를 이용해 Bean 오브젝트를 만들고 DI 작업을 수행한다.

Bean 오브젝트의 메소드를 호출함으로써 애플리케이션을 동작시켜야 한다.

계속 봐왔던 예제처럼 getBean() 메소드를 사용해 Bean 오브젝트를 가져와야 하고 한번 가져온 뒤에는 다시 getBean()으로 가져올 필요는 없다.

그럼 Bean 오브젝트끼리 DI로 서로 연결되어있으므로 의존관계를 타고 필요한 오브젝트가 호출되면서 애플리케이션이 동작하게 된다.

IoC 컨테이너의 역할은 이렇게 초기에 Bean 오브젝트를 생성하고 DI 한 후에 최초로 애플리케이션을 기동할 Bean하나를 제공해주는것 까지다.

 

그런데 웹 애플리케이션은 동작하는 방식이 근본적으로 다르다.

독립 자바 프로그램은 VM에게 main 메소드를 가진 클래스를 시작시켜 달라고 요청할 수 있지만

웹에서는 main 메소드를 호출할 방법이 없고 사용자도 여럿이며 동시에 웹 애플리케이션을 사용한다.

그래서 웹 환경에서는 main 메소드 대신 ServletContainer가 브라우저로부터 오는 HTTP 요청을 받아 해당 요청에 매핑되어 있는 servlet을 실행해주는 방식으로 동작한다.

servlet이 main메소드와 같은 역할을 하는 셈이다.

 

웹 애플리케이션에서 스프링 애플리케이션을 기동하기 위해서는 main 메소드 역할을 하는 servlet을 만들어두고 미리 ApplicationContext를 생성해둔 다음, 요청이 servlet으로 들어올때마다 getBean()으로 필요한 Bean을 가져와 정해진 메소드를 실행해주면 된다.

아래 그림이 웹 환경에서 스프링 Bean으로 이뤄진 애플리케이션이 동작하는 구조다.

 

출처: 토비의 스프링 3.1

 

 

서버 환경이기 때문에 필요한 ServletContainer와 client로부터의 원격 호출을 감안하고 본다면 main메소드 안에서 ApplicationContext를 만들고 설정 메타정보를 읽어들여서 초기화 한 후에 애플리케이션의 기동 책임을 맡는 POJO Bean 오브젝트를 요청해 메소드를 실행하는것과 다를바가 없다.

main 메소드나 테스트 메소드에서 했던 작업을 WebApplication과 그에 소속된 Servlet이 대신 해줄 뿐이다.

 

ServletContainer는 브라우저와 같은 client로부터 들어오는 요청을 받아 servlet을 동작시켜주는 역할을 한다고 했다.

servlet은 웹 애플리케이션이 시작될 때 미리 만들어둔 WebApplicationContext에게 Bean 오브젝트로 구성된 애플리케이션의 기동역할을 해줄 Bean을 요청해 받아둔다.

그리고 미리 지정된 메소드를 호출함으로써 스프링 컨테이너가 DI 방식으로 구성해둔 애플리케이션의 기능이 시작되는 것이다.

 

스프링은 이런 웹 환경에서 ApplicationContext를 생성하고 설정 메타정보로 초기화해주고 클라이언트로 들어오는 요청마다 적절한 Bean을 찾아 이를 실행해주는 기능을 갖고 있는 DispatcherServlet 이라는 이름의 serlvet을 제공한다.

이 servlet을 web.xml에 등록하는 것만으로 웹 환경에서 스프링 컨테이너가 만들어지고 애플리케이션을 실행하는데 필요한 대부분의 준비는 끝난다.

 

WebApplicationContext의 특징은 자신이 만들어지고 동작하는 환경인 웹 모듈에 대한 벙보에 접근할 수 있다는 것이다.

이를 이용해 웹 환경으로부터 필요한 정보를 가져오거나 웹 환경에 스프링 컨테이너 자신을 노출할 수 있는데 이렇게 노출되면 같은 웹 모듈에 들어있는 스프링 Bean이 아닌 일반 오브젝트와 연동될 수 있다.

'Spring' 카테고리의 다른 글

IoC와 DI(4. Bean 설정 방식, DI 방식)  (0) 2022.08.24
IoC와 DI(3. WebApplication의 IoC Container 구성)  (0) 2022.08.22
IoC와 DI(1. IoC Container란)  (0) 2022.08.18
JPA 양방향 연관관계(mappedBy)  (0) 2022.03.18
JPA Entity Listener 2  (0) 2022.03.17

Spring IoC와 DI에 대해 정리.

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

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

 

Reference

 

[Spring] Spring IoC와 DI란?

spring-study에서 스터디를 진행하고 있습니다. IoC란? IoC란 Inversion of Control의 줄임말이며, 제어의 역전이라고 한다. 스프링 애플리케이션에서는 오브젝트(빈)의 생성과 의존 관계 설정, 사용, 제거

steady-coding.tistory.com

 

[Spring] IoC 컨테이너 (Inversion of Control) 란?

IoC (Inversion of Control)? IoC를 네이버 영어사전에서 번역해보면 제어 반전을 뜻하고 있습니다. IoC(제어 반전)이란, 객체의 생성, 생명주기의 관리까지 모든 객체에 대한 제어권이 바뀌었다는 것을

dev-coco.tistory.com

 

IoC 컨테이너와 DI(Dependency Injection)

Spring Framework IoC 컨테이너와 DI(Dependency Injection) 학습 목표 IoC(Inversion of Control)의 이해 DI(Dependency Injection)의 이해 Spring DI 컨테이너에 대한 이해 1.IoC(Inversion of Control)의 이해..

dog-developers.tistory.com

 

 

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 종료

 

 

 

IoC 컨테이너(Inversion of Control Container)

 

스프링에서는 오브젝트의 생성과 관계 설정, 사용, 제거 등의 작업을 애플리케이션 코드 대신 독립된 컨테이너가 담당한다.

이를 컨테이너가 코드 대신 오브젝트에 대한 제어권을 갖고 있다고 해서 IoC(Inversion of Control, 제어의 역전)라고 부른다.

 

객체의 생성, 생명주기의 관리까지 모든 객체에 대한 제어권이 바뀌었다는 것을 의미하고 컴포넌트 의존관계 설정(Component dependency resolution), 설정(Configuration) 및 생명주기(Lifecycle)을 해결하기 위한 디자인 패턴이다.

 

스프링에서 객체를 생성하고 관리하고 책임지고 의존성을 관리해주는 컨테이너를 IoC 컨테이너, DI 컨테이너, 스프링 컨테이너라고 부른다.

 

컨테이너는 인스턴스 생성부터 소멸까지의 인스턴스 생명주기 관리를 개발자 대신 해준다.

그럼 개발자는 객체관리 주체가 프레임워크(컨테이너)가 되기 때문에 로직에 집중할 수 있다는 장점이 있다.

  • IoC 컨테이너는 객체의 생성을 책임지고, 의존성을 관리한다.
  • POJO의 생성, 초기화, 서비스, 소멸에 대한 권한을 가진다.
  • 개발자들이 직접 POJO를 생성할 수 있지만 컨테이너에게 맡긴다.
  • 개발자는 비즈니스 로직에만 집중할 수 있다.
  • 객체 생성 코드가 없으므로 의존하는 컴포넌트간의 결합도를 낮춰 테스트가 용이하다.

 

그리고 IoC 하면 따라오는것이 DI(Dependency Injection, 의존성 주입)이다.

DI는 IoC 디자인 패턴중 하나다.

IoC는 DL(Dependency Lookup)과 DI(Dependency Injection)으로 나눌 수 있는데

DL은 저장소에 저장되어 있는 Bean에 접근하기 위해 컨테이너가 제공하는 API를 이용해 Bean을 LookUp하는 것이고

DI는 각 클래스간의 의존관계를 Bean 설정(Bean Definition) 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것이다.

이렇게 DL과 DI로 나눠지지만 DI와 보통 묶어서 정리하는 이유는 DL을 사용할 경우 컨테이너 종속이 증가하기 때문에 DI를 많이 사용하기 때문이라고 한다.

 

스프링에서는 IoC를 담당하는 컨테이너를 BeanFactory, ApplicationContext라고 부리기도 한다.

오브젝트의 생성과 오브젝트 사이의 런타임 관계를 설정하는 DI 관점으로 볼 때는 컨테이너를 BeanFactory라고 하고, DI를 위한 BeanFactory에 Enterprise Application을 개발하는데 필요한 여러가지 컨테이너 기능을 추가한것을 ApplicationContext라고 부른다.

 

 

BeanFactory

  • BeanFactory 계열의 인터페이스만 구현한 클래스는 단순히 컨테이너에서 객체를 생성하고 DI를 처리하는 기능만 제공한다.
  • Bean을 등록, 생성, 조회, 반환 관리를 한다.
  • 팩토리 디자인 패턴을 구현한 것으로 BeanFactory는 빈을 생성하고 분배하는 책임을 지는 클래스다.
  • Bean을 조회할 수 있는 getBean() 메소드가 정의되어 있다.
  • 보통은 BeanFactory를 바로 사용하지 않고, 이를 확장한 ApplicationContext를 사용한다.

 

ApplicationContext

  • Bean을 등록, 생성, 조회, 반환 관리하는 기능을 갖고 있으며 이 기능은 BeanFactory와 같다.
  • 스프링의 각종 부가 기능을 추가로 제공한다.
    • 국제화가 지원되는 텍스트 메세지를 관리해준다.
    • 이미지 같은 파일 자원을 로드할 수 있는 포괄적인 방법을 제공해준다.
    • 리스너로 등록된 빈에게 이벤트 발생을 알려준다.

 

출처 : https://dog-developers.tistory.com/12

즉, ApplicationContext는 그 자체로 IoC와 DI를 위한 BeanFactory이면서 그 이상의 기능을 가졌다고 볼 수 있다.

스프링의 IoC 컨테이너는 일반적으로 ApplicationContext를 말한다.

 

 

스프링의 BeanFactory와 ApplicationContext는 각각의 기능을 대표하는 BeanFactory와 ApplicationContext라는 두개의 인터페이스로 정의되어 있다.

그리고 ApplicationContext는 BeanFactory 인터페이스를 상속한 서브 인터페이스다.

 

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, 
                                            HierarchicalBeanFactory, MessageSource, 
                                            ApplicationEventPublisher, ResourcePatternResolver{

}

 

토비의 스프링 3.1 예제에서는 EnvironmentCapable 인터페이스는 상속받지 않는 예제다.

5.2.3.RELEASE 기준 위와 같이 확인할 수 있는데 spring documentation에서 확인해보니 3.1부터 나온 인터페이스라고 한다.

아마 책 작성시에는 안들어가있다가 추후 추가된것이 아닌가 싶다.

이 인터페이스는 검사를 수행하는데 주로 사용되는 인터페이스라고 되어있다.

 

ListableBeanFactory와 HierarchicalBeanFactory는 BeanFactory 인터페이스의 서브 인터페이스다.

따라서 ApplicationContext는 BeanFactory 인터페이스를 상속받고 있다고 볼 수 있는 것이다.

 

IoC 컨테이너는 ApplicationContext 인터페이스를 구현한 클래스의 오브젝트를 의미한다.

스프링 애플리케이션은 최소한 하나 이상의 IoC 컨테이너, 즉 ApplicationContext 오브젝트를 갖고 있다.

 

 

'Spring' 카테고리의 다른 글

IoC와 DI(3. WebApplication의 IoC Container 구성)  (0) 2022.08.22
IoC와 DI(2. IoC Container의 종류와 사용방법)  (0) 2022.08.18
JPA 양방향 연관관계(mappedBy)  (0) 2022.03.18
JPA Entity Listener 2  (0) 2022.03.17
JPA Entity Listener 1  (0) 2022.03.16

+ Recent posts