Baeldung의 이 글을 정리 및 추가 정보를 적은 글입니다.

1. Overview

  • 예전 글에서 bean scope에 대해 잠깐 얘기한 적이 있는데, 이에 대해 좀 더 자세히 알아보도록 하자.

  • bean scope는 bean이 언제 생성되고, 몇 개 생성되고, 얼마나 유지되는지를 지정하는데 사용된다.

2. Singleton Scope

  • 기본 설정이다.

  • 딱 하나만 만든다. 그래서 해당 domain의 bean을 누가 접근하든 동일한 객체가 반환된다.

  • 이 때문에 캐싱을 보통 한다. 왜냐하면 그만큼 많이 접근을 할테니까... 그리고 이 bean에 적용된 변동사항은 모든 다른 bean들이 확인할 수 있다.

  • 예시로 Person이라는 bean을 만들어보도록 하자.

public class Person {
    private String name;

    // standard constructor, getters and setters
}
@Bean
@Scope("singleton")
public Person personSingleton() {
    return new Person();
}
  • 이 때, 저렇게 문자열을 사용하는 대신 상수를 제공하는 것도 가능하다. 그러면 IDE에서 더 잘 잡아준다.
@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)
  • 이제 위 구조를 위한 config를 설정해야 하는데 xml파일로 할 것이다. 이는 밑과 같다. 이를 scopes.xml이라고 하자. 여기서 xmlns부분은 Spring bean configuration때 기본으로 포함하는 영역이고 실질적으로 집중해야 하는 부분은 personSingleton이 있는 bean tag 부분이다.
<?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="personSingleton" class="org.baeldung.scopes.Person" scope="singleton"/>    
</beans>
  • 위의 config 파일과 Person에 관한 코드를 가지고 밑과 같은 식으로 테스트를 짜면 Person에 해당하는 bean이 하나만 나오는걸 확인하는게 가능하다. setName으로 이름을 바꾸더라도 다른 쪽에서 동일한 값을 확인하고 있기 때문. getBean에서 같은 reference를 반환하기 때문이다.
private static final String NAME = "John Smith";

@Test
public void givenSingletonScope_whenSetName_thenEqualNames() {
    ApplicationContext applicationContext = 
      new ClassPathXmlApplicationContext("scopes.xml");

    Person personSingletonA = (Person) applicationContext.getBean("personSingleton");
    Person personSingletonB = (Person) applicationContext.getBean("personSingleton");

    personSingletonA.setName(NAME);
    Assert.assertEquals(NAME, personSingletonB.getName());

    ((AbstractApplicationContext) applicationContext).close();
}
  • 이 bean의 소멸 시점은 위처럼 context가 close되거나, referesh 되는 경우다. 파괴되기 전에 할 행동을 지정하는 annotation인 @PreDestroy라는 것도 있는데 이건 지금 설명하진 않겠다.

3. Prototype Scope

  • IoC container에 해당 instance를 요청할 때, container이 새로운 instance를 생성하고 이를 반환하는 형태의 scope이다. 명시하는 방법은 이전과 비슷하다.
@Bean
@Scope("prototype")
public Person personPrototype() {
    return new Person();
}
  • 역시나 상수로도 지정 가능하다.
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
  • configuration에 사용할 xml config 파일은 이전과 비슷한데, bean 태그에 있는 내용을 밑으로 바꾸기만 하면 된다.
<bean id="personPrototype" class="org.baeldung.scopes.Person" scope="prototype"/>
  • 그러면 밑과 같이 테스트를 할 경우, A랑 B가 서로 다른 이름을 가지고 있으므로 동일한 reference, 즉 동일한 bean이 아님을 확인할 수 있고, prototype scope는 매번 새로운 bean을 만든다는 것을 확인이 가능하다.
private static final String NAME = "John Smith";
private static final String NAME_OTHER = "Anna Jones";

@Test
public void givenPrototypeScope_whenSetNames_thenDifferentNames() {
    ApplicationContext applicationContext = 
      new ClassPathXmlApplicationContext("scopes.xml");

    Person personPrototypeA = (Person) applicationContext.getBean("personPrototype");
    Person personPrototypeB = (Person) applicationContext.getBean("personPrototype");

    personPrototypeA.setName(NAME);
    personPrototypeB.setName(NAME_OTHER);

    Assert.assertEquals(NAME, personPrototypeA.getName());
    Assert.assertEquals(NAME_OTHER, personPrototypeB.getName());

    ((AbstractApplicationContext) applicationContext).close();
}
  • 참고로 이 scope로 형성된 bean은 IoC container에서 관리하지 않는다. 생성 이후 IoC container의 관리에 벗어나게 되는건데... 이 때문에 수거 시점은 POJO들이 garbage collector에 의해 회수되는 시점이랑 동일하다. 보통 해당 reference를 보유한 곳이 더 이상 없을 때 수거가 된다.

  • '요청시'마다 bean을 생성한다는 점에서 유의해야하는 것이 있는데, singleton-scope bean이 생성자 기반으로 prototype-scope bean을 inject받으면 단일 runtime에서 여러번 prototype-scope bean을 획득하는게 불가능하다. 이를 원하면 method 기반 injection을 수행해야 한다.

4. Web Aware Scopes

  • 여기서부터 이전 글에서 언급하지 않은 scope들이다. web-aware application에서만 사용하며 앞의 2개에 비해 사용 빈도수가 좀 더 적다.

  • web-aware이라는게 뭔가 대단...해보이지만 사실 웹이라는 개념 자체를 인지하고 있는 application이라는 뜻으로 밑의 내용을 보면... 왜 저런 application에서 사용하는지 자연스럽게 이해할 수 있다. 웹과 관련된 이벤트 및 요소에 따라 bean을 생성하는 것이기 때문.

  • 밑과 같은 class의 bean을 만들려고 한다 해보자.

public class HelloMessageGenerator {
    private String message;
    
    // standard getter and setter
}

4.1 Request Scope

  • request scope를 가진 bean은 각 HTTP 요청 때마다 만들어진다. 이 scope를 사용하려면 밑과 같이 코딩해야 한다.
@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public HelloMessageGenerator requestScopedBean() {
    return new HelloMessageGenerator();
}
  • 밑의 코드도 위의 코드와 정확히 똑같은 역할을 해준다.
@Bean
@SessionScope
public HelloMessageGenerator sessionScopedBean() {
    return new HelloMessageGenerator();
}
  • 이제 밑과 같은 controller을 만들어보자.
@Controller
public class ScopesController {
    @Resource(name = "requestScopedBean")
    HelloMessageGenerator requestScopedBean;

    @RequestMapping("/scopes/request")
    public String getRequestScopeMessage(final Model model) {
        model.addAttribute("previousMessage", requestScopedBean.getMessage());
        requestScopedBean.setMessage("Good morning!");
        model.addAttribute("currentMessage", requestScopedBean.getMessage());
        return "scopesExample";
    }
}

proxy

  • 위에 보면 좀 낯선 개념이 나오는데, 바로 proxyMode다. 이에 대해 설명하기 전에 proxy에 대해 설명해야 한다.

  • 위에 말했지만 request scope bean은 HTTP request때마다 생성된다고 했다. 애플리케이션이 막 시작 되어 IoC가 config 파일로 구성을 시작하고 있을 때 HTTP reqeust는 존재할 수가 없다. 그러면 저 bean도 존재를 하지 않는 것이다.

  • 문제는 어쨌든 저걸 활용하는 도메인 측에서는 관련 bean을 주입 받아야 한다. 아니, 그러면 HTTP request가 올 때마다 주입해주면 되는거 아니냐고? 그렇...게 해결되면 좋을텐데 singleton scope를 가지는 bean이 request scope bean을 주입받아야 한다고 해보자. 그러면 주입 기회가 config 구성 시점 밖에 없다. 뭐 config 구성한 후 시점에서 바로 사용하진 않고 request가 올 때만 본격적으로 사용하겠지만 구조적으로 request가 올 때 주입받는 환경을 갖추는게 불가능하다는 것이다. 즉 request 때마다 request scope bean을 주입 받는 구조가 아닌, application 시작 때 request scope bean을 주입 받도록 코딩이 되어야한다.

  • 근데 말했다시피 저 bean은 HTTP 요청에서만 instantiate된다. 이 모순을 해결하기 위해 proxy라는 것을 Spring에서 만든다. 네트워크 공부를 하다보면 proxy server이라고 client와 server 사이의 중계기 역할을 해 server이 client가 아닌 proxy와 항상 대화하고 있는것 같은 시스템을 만드는 것을 배운 적이 있을 것이다. 지금도 이 용도와 비슷하다.

  • 아까처럼 singleton scope bean이 request scope bean을 주입받을 때 대신 proxy를 주입하는 것이다. 그리고 실제 HTTP request 때 bean이 생성되면 이 proxy랑 연결지어서 활용하는 class 측에서 접근할 수 있도록 유도하는 것이다. singleton scope bean은 항상 proxy를 호출하게 되지만, 그때마다 proxy에서 알아서 현 request에 대한 bean에 접근할 수 있도록 해주는거다...

  • 이 때 proxyMode에는 이 request scope bean이 실제로 나타내는 class를 전달해줘야 한다. 혹은 인터페이스를 전달해줘도 되는데, 이 경우 scoped bean 자체가 해당 interface를 implement해야 하고, 또 접근하는 측도 그 interface를 활용해야 정상 동작을 한다. 인터페이스 기반 proxy는 JDK dynamic proxy를 활용한다는 점 참고...

  • 이거랑 관련된 documentation

  • 앞에 말한 이유로 request, 그리고 뒤의 session scope bean은 무조건 proxy를 설정해줘야 한다.

  • 위의 경우 ScopedProxyMode.TARGET_CLASS를 사용하고 있는데, 이 bean을 주입받은 곳은 bean을 만드는데 사용한 class를 기반으로 proxy를 만들라는 것이다.

@Controller

  • 그럼 처음 두 코드는 그렇다치고... 세번째에 보면 낯선 annotation과 class들이 많이 나온다. 하나하나 알아보도록 하자.

  • 먼저 @Controller. 관련 documentation. 여기서 사용한 servlet 기반으로 링크했다., javadocs

  • 사실 얘는 그냥 MVC의 controller에 해당하는 bean을 나타낼 때 쓰인다. @ComponentScan에 의해 bean으로 탐지가 가능한데, 이유는 javadoc을 보면 알 수 있듯이 @Component를 포함하고 있기 때문이다.

@Resource

  • documentation

  • 이 annotation은 유의할 부분이 있는데, 이 문법은 javax(현재는 jakarta)에서 정의하는 것이다. Spring이 Java EE (jakarta)를 baseline으로 삼았기 때문에 이 annotation도 사용하는 것이 가능.

  • 그래서 뭐하는거냐면... 사실 @Autowired와 매우 유사하다. 그냥 저 field에 주입을 하는 것이다. @Autowired와의 차이점은 @Autowired는 Spring에서, 이건 JEE가 뿌리라는 것 정도와 @Autowired는 type을, @Resource는 이름을 기본으로 해서 찾는다는 것 정도.

@RequestMapping

Model

  • javadocs

  • MVC model의 model에 해당하는 영역이다. 뭐... 별로 할 말은 없다.

  • 그러면 위 코드는 뭘 하는 것인가? 먼저 addAttributemodel에 대해 호출하는데... 그냥 저 이름의 attribute가 저 값을 가지도록 해서 추가하는 것이다. 이 때 getMessage를 호출하고 있군요. 저 값이 initialize가 되지 않는 값이라면 처음에는 getMessage 값이 null 값을 가진다.

  • 그 다음에 setMessage를 통해 requestScopedBean의 값을 설정해주고 있다. 그 다음에 다시 getMessage를 호출해서 currentMessage라는 이름의 attribute로 model에 집어넣고 최후엔 문자열 하나를 반환한다.

  • 그러면 추 후에 다시 request가 오면 어떻게 될까... 뭐 requestScopedBean은 다시 message값이 null로 되어 있다. request마다 bean이 새로 생성되어서 proxy에 attatch 되기 때문.

4.2 Session Scope

  • 네트워크 연결 세션마다 하나씩 만들어진다. 이 scope를 사용하려면 밑과 같이 코딩해야 한다.
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public HelloMessageGenerator sessionScopedBean() {
    return new HelloMessageGenerator();
}
  • 밑의 코드도 위와 정확히 똑같은 역할을 한다.
@Bean
@SessionScope
public HelloMessageGenerator sessionScopedBean() {
    return new HelloMessageGenerator();
}
  • 앞과 유사한 controller을 만들어봤다. 밑의 경우 sessionScopedBean이 처음 연결이 끊기고 다음 연결이 올 때까지는 계속 유지되어서, 같은 연결에 대한 request에 대해서 message는 매번 보존이 된다.
@Controller
public class ScopesController {
    @Resource(name = "sessionScopedBean")
    HelloMessageGenerator sessionScopedBean;

    @RequestMapping("/scopes/session")
    public String getSessionScopeMessage(final Model model) {
        model.addAttribute("previousMessage", sessionScopedBean.getMessage());
        sessionScopedBean.setMessage("Good afternoon!");
        model.addAttribute("currentMessage", sessionScopedBean.getMessage());
        return "scopesExample";
    }
}

4.3 Application Scope

  • ServletContext가 살아있는 동안 유지되는 bean을 만든다. 이 scope를 사용하려면 밑과 같이 코딩해야 한다.
@Bean
@Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public HelloMessageGenerator applicationScopedBean() {
    return new HelloMessageGenerator();
}
  • 밑의 코드도 위와 정확히 똑같은 역할을 한다.
@Bean
@ApplicationScope
public HelloMessageGenerator applicationScopedBean() {
    return new HelloMessageGenerator();
}
  • 앞과 유사한 controller이다. 여기서 applicationScopedBean의 instance는 같은 ServletContext에 대해서 실행되고 있다면 여러 reqeust, 여러 session, 심지어는 그 ServletContext를 공유하는 여러 servlet application이 접근할때도 매번 동일하다.ServletContext을 기준으로 lifecycle이 정해지고, ServletContext 하나하나에 대해 bean이 하나씩 만들어지는 것이다. 그게 첫 문장에서 얘기한 내용.

  • singleton scope이 이 녀석과 다른 점은, ServletContext가 아닌 ApplicationContext에 대해서 lifecycle및 instance 개수가 정해진다는 것이다.

servlet

  • 그런데 위 얘기에서 ServletContext라는 것이 뭔지 잘 모르는 사람도 있을 것이다. context...니까 servlet이랑 관련이 있을것 같은데 그러면 servlet이 무엇일까?

  • 이건 JSP랑 관련된 개념으로 이 글을 보면 도움이 많이 된다. 다 읽으면 무슨 용도인지 감이 올 것이다. JSP 부분은 이 글을 읽는데 꼭 이해할 필요 없다는 점 참고. (알면 좋지만.)

참고로 톰캣(Tomcat)도 많이 들어봤을텐데, 이 녀석이 위 글에서 말하는 Servlet Container에 해당한다.

  • 그런데 잠깐만... 이거 Java EE spec의 일부다. 그리고 Spring은 이것의 많은 부분을 지켰다고 한다. 그러면... Spring에서 저 조건을 만족하는 'servlet'에 해당하는 녀석이 있는건가? 어... 완전 대응되진 않고 그것보다 좀 더 high-level의 개념을 제공해준다. 그게 바로 DispatcherServlet이다.

DispatcherServlet

  • 얘는 Java EE의 Servlet이랑 호환이 되지만 성능이 그냥 JavaEE에서 요구하는 servlet보다 좀 더 좋다.

  • 앞의 글에서 프론트 컨트롤러 패턴을 봤을텐데, 이 패턴에서 활용하는게 DispatcherServlet이다. 거기의 Front Controller이 DispatcherServlet이다.

  • 앞의 글에서 나온 내용이지만 Spring MVC 말고 그냥 Java EE 기반의 servlet을 활용할 때는 Servlet Container에게 각 Servlet에 대한 정보를 전달해야 그 container이 관리를 할 수 있기에 Web.xml이라는 파일을 일일이 만들어야 했다. 그리고 여러 위치 (여러 URL)에 대한 요청을 처리하는 Servlet을 일일이 만들어야 했다. 이게 조합되니까 xml 문법이 개빡도는것까지 해서 지옥의 유지관리 사태가 발생하게 되었고, 그래서 Spring에서 등장한게 프론트 컨트롤러 패턴이다. 앞의 글의 그림을 보면 좀 더 이해가 잘 될것이다.

사실 DispatcherServletweb.xml을 집어넣어야하는 것은 똑같다만 앞의 RequestMapping을 통해 여러 URL에 대한 servlet을 만들 필요가 없다는 것이 장점이다. 그리고 DispatcherServletweb.xml자체도 spring boot에서 이를 annotation이나 configuration 등으로 쉽게 해결해줘서 그 부담을 덜어줬다. 이는 언젠가 알아보도록 하자.

이때 DispatcherServlet을 넣을 Servlet Container의 후보군은 꽤 다양하다. 아까 나온 톰캣은 물론 Jetty, Undertow 등 Servlet API를 지원하는 녀석들은 다 가능. 다만 Spring Boot의 경우 이 선택 및 설정마저 귀찮기 때문에 기본적으로 Application에다가 Tomcat을 임베드시켰다. 이 임베드 시키는 servlet container도 configuration을 건드려 바꿀 수 있다는 점 참고.

  • 결론은 DispatcherServlet도 Java EE Servlet API의 일종이다. 다만 앞의 프론트 컨트롤러 패턴을 기반으로 좀 더 최적화되어 있다는 것 정도.

Modularity with DispatcherServlet

  • 여기서 좀 응용이 하나 가능한데, 바로 하나의 거대 web application에서 특정 기능들을 담당하는 servlet을 여러개 만들어가지고 modularity를 형성하는 것이다.

  • 이 얘기를 하는 이유는

    • ServletContext, WebApplicationContext, ApplicationContext의 관계를 좀 더 잘 이해할 수 있도록 하기 위해
    • ...그냥 말나온김에 modularity가 잘 된 웹 애플리케이션을 Spring에서 어떻게 만드는지 알려주기 위해.

WebApplicationContext, ApplicationContext, ServletContext

  • 아마 당황스러울 것이다. Servletcontext는 둘째치고 저것들은 갑자기 뭔데 여기서 튀어나오는 것인가. 하나하나 알아보도록 하자. 일단 밑의 xml 파일을 한번 봐보도록 하자. (출처)
<web-app>

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

	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/app-context.xml</param-value>
	</context-param>

	<servlet>
		<servlet-name>app</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value></param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>app</servlet-name>
		<url-pattern>/app/*</url-pattern>
	</servlet-mapping>

</web-app>
  • WebApplicationContext : 각 Servlet 고유의 context다. 위에 보면 servlet tag에 init-param 태그가 있는 것을 볼 수 있고 contextConfigLocation이라고 config 파일 위치를 넣어야 하는 곳이 보일거다. 저기에 WebApplicationContext에 해당하는 config 파일을 넣는다.
  • ApplicationContext (root context) : Spring Application 전체에 대한 context다. 위에 보면 context-param tag에 contextConfigLocation이라고 config 파일 위치를 넣어야 하는 곳이 보일거다. 저기에 ApplicationContext에 해당하는 config 파일을 넣는다.

위 둘의 관계를 이해하고 싶으면 이 stackoverflow 글도 도움이 된다.

  • ServletContext(javadocs) : 앞의 2개는 spring에서 정의되어 있지만 이건 Java EE 상에서 정의된거다. 이 때문에 옛날 servlet API 구조와 관련된 기능을 제공하는데, servlet과 servlet container 사이의 통신과 관련된 method들을 제공한다. 그리고 이것도 doc에 따르면 application당 하나만 존재한다.

  • 마지막이 Java EE의 영역이기 때문에 Java EE의 기능과 관련된 context들을 많이 보유하지 Spring용 context를 보유하진 않는다. Spring이 이 영역을 건드리지 않는다는게 아니다. Spring도 자기 시스템을 운용하기 위해 해당 context를 건드리긴하나, 둘이 어쨌든 동작을 담당하는 layer이 다르다는 것을 의미한다.

  • 여튼 이걸 보면 알 수 있는 것이

    • 단일 web application에 여러개의 servlet(DispatcherServlet)을 생성하고 **각각 다른 configuration 파일을 넣는 것이 가능하다.
    • 이를 활용해 각 servlet이 다른 영역의 기능을 담당하도록 설정하는 것이 가능하다.
    • 그래도 어쨌든 같은 애플리케이션이고 하나의 servlet container에 들어가있으니 ServletContext를 통해 공통된 context를 가지는 것이 가능하다. (그리고 공통된 ApplicationContext도 가질 것이다. 다만 둘의 용도는 위에 봤듯 약간 다르다.)
    • 그리고 scope가 Application인 경우, ServletContext를 따라 bean의 life cycle이 결정되니 같은 ServletContext 하의 다른 servlet은 동일한 Application scope의 bean instance를 보는게 가능하다.
  • 밑은 관련 controller에 대한 코드이다. 거기서 보는 bean은 ServletContext가 유지되고 동일한 이상 계속 동일할것이다. 또 위에 보면 proxyMode를 설정했는데, 어쨌든 singleton scope의 bean이랑은 lifecycle 차이가 있기 때문에 그렇다.

@Controller
public class ScopesController {
    @Resource(name = "applicationScopedBean")
    HelloMessageGenerator applicationScopedBean;

    @RequestMapping("/scopes/application")
    public String getApplicationScopeMessage(final Model model) {
        model.addAttribute("previousMessage", applicationScopedBean.getMessage());
        applicationScopedBean.setMessage("Good afternoon!");
        model.addAttribute("currentMessage", applicationScopedBean.getMessage());
        return "scopesExample";
    }
}

4.4 WebSocket Scope

  • 뭐 예상대로 밑과 같이 만들면 되지만... 이 녀석을 이해하려면 이번엔 WebSocket에 대해서 알아야 한다.
@Bean
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public HelloMessageGenerator websocketScopedBean() {
    return new HelloMessageGenerator();
}

WebSocket

  • 그런데 사실 별거 없다.(?) TCP 연결 위에 있는 또 다른 프로토콜인데 bidirectional하고 full-duplex라는 점이 가장 큰 특징이다.

  • 기존의 TCP 연결은 client가 server에 poll을 하고, 그러면 server이 이를 인지하고 답변하는 형식이다. 그러나 websocket protocol을 사용하면 client가 특정 channel 요청을 queue하고, 이를 나중에 server이 channel에 접근해 확보, 적절한 reply를 나중에 channel에 queue한다. 그리고 client는 또 본인 여유가 있을 때 이를 확보하는 것이다.

  • 자세한건 위키백과 참고

  • 그러면 위 bean은 이 WebSocket session이 생기면 생성되고, 그게 있는 동안에 계속 유지되는 bean이라고 생각하면 된다. 저 WebSocket session이 끝나면 그 bean도 같이 사라진다. Lifecycle이 본인이 대응된 WebSocket session이랑 연결된거고, 이를 inject받은 측은 WebSocket connection이 유지되는 동안 계속 그 bean을 보는 것이지요.

  • 이 또한 singleton scope의 lifecycle이랑 차이가 있어서 위처럼 proxyMode를 설정해줘야 한다.

profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글