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 단위로 인스턴스가 만들어진다.

+ Recent posts