본문 바로가기
Spring|Spring-boot

Standalone Application

by oncerun 2023. 1. 22.
반응형

스프링 부트의 특징 중 하나는 독립실행형 애플리케이션이라는 것이다.  

 

스프링 부트를 만드는 과정에서는 서블릿 컨테이너를 감추는 작업이 필요했다. 이러한 서블릿 컨테이너를 구현한 구현체들에 대한 별도의 설치나 설정을 하지 않고 개발을 할 수 있는 환경을 만들어야 함을 의미하는데,

 

스프링 부트의 main() 메서드에서는 실제 서블릿 컨테이너를 별도의 설치하지 않아도 main 메서드 실행 시 기본 구현체로 서블릿 컨테이너의 구현체인 톰캣이 기본 포트 8080으로 뜨는 것을 확인할 수 있다.

 

tomcat은 자바로 작성된 하나의 프로젝트이다. tomcat의 개발자들은 별도의 설치를 통해 사용하는 것 대신 embedded 된 tomcat을 사용할 수 있도록 제공하는 라이브러리가 존재한다.

 

이번에는 이러한 내장 톰켓라이브러리를 사용해 main 메서드 실행함에 있어 서블릿 컨테이너를 띄우는 과정을 살펴본다.

import org.apache.catalina.startup.Tomcat;

Tomcat 객체를 통해 서블릿 컨테이너를 생성하고 설정하고 시작하기 위해선 생각보다 많은 시간이 소요된다.

new TomcatServletWebServerFactory();

이를 위해 스프링 부트는 서블릿 컨테이너를 코드로 쉽게 시작해서 사용할 수 있도록 제공해주는 Helper Class가 존재한다.

public static void main(String[] args) {

    TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();

    WebServer webServer = serverFactory.getWebServer();

    webServer.start();

}

 

아직 서블릿이 없어서 요청 시 404에러가 발생하긴 하지만 톰캣이 정상적으로 작동한다. 

 

그렇기 때문에 서블릿을 추가해본다.

 

 

WebServer webServer = serverFactory.getWebServer(servletContext -> {
    
    
});

 

ServletContextInitializer라는 인터페이스는 서블릿 콘텍스트에 서블릿, 필터, 리스너를 등록하는 함수형 인터페이스이다. 

@FunctionalInterface
public interface ServletContextInitializer {

   /**
    * Configure the given {@link ServletContext} with any servlets, filters, listeners
    * context-params and attributes necessary for initialization.
    * @param servletContext the {@code ServletContext} to initialize
    * @throws ServletException if any call against the given {@code ServletContext}
    * throws a {@code ServletException}
    */
   void onStartup(ServletContext servletContext) throws ServletException;

}

 

이를 람다로 대체하고 서블릿을 추가해 보자.

 

서블릿 인터페이스를 전부 구현하는 것은 매우 불필요하다. 그래서 서블릿을 공부했을 때 아마 HttpServlet 클래스를 상속하여 필요한 부분만 오버라이드했던 것이 기억이 난다. 

 

WebServer webServer = serverFactory.getWebServer(servletContext -> {
    servletContext.addServlet("oncerun", new HttpServlet() {
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.setStatus(HttpStatus.OK.value());
            resp.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
            resp.getWriter().println("oncerun servlet");
        }
    }).addMapping("/oncerun");
});

 

 

서블릿 동작테스트를 진행해 보자.

 

 

일단 응답 데이터의 인코딩을 UTF-8로 변경하고 실제 쿼리 파라미터를 보내고 해당 파라미터를 응답으로 내려주는 서블릿으로 변경해 보자.

 

public static void main(String[] args) {

    TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();

    WebServer webServer = serverFactory.getWebServer(servletContext -> {
        servletContext.addServlet("oncerun", new HttpServlet() {
            @Override
            protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                String name = req.getParameter("name");

                resp.setStatus(HttpStatus.OK.value());
                resp.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
                resp.setCharacterEncoding("UTF-8");
                resp.getWriter().println(name);
            }
        }).addMapping("/oncerun");
    });

    webServer.start();

}

응답 테스트

curl 127.0.0.1:8080/oncerun?name=hi

 

오랜만에 서블릿 하니까 재밌다.. 

 

지금까지 main 메서드에서 실행하여 서블릿 컨테이너를 띄우고 서블릿을 만들어 요청을 처리하는 코드를 작성했다. 

 

덧붙여서 스프링 MVC에 대한 구조를 기억하고 FrontController의 기능까지 만든다면 Spring MVC의 dispatherServlet까지 구현해 볼 수 있다. 

 

단순하게 모든 요청을 하나의 서블릿에서 처리하게 되며 해당 서블릿에서 요청과 응답의 중복 코드를 처리할 수 있도록 하고, 해당 요청을 처리할 수 있는 handler를 찾도록 만들고 해당 handler에게 요청을 위임하는 코드를 작성하면 된다. 

 

하지만 이는 그렇게 간단하지 않다. 생각보다 더 많은 부분을 고려해야 하기 때문이다. 

 

예를 들면 요청의 Payload를 분석하여 파라미터로 넘기는 과정을 공통적으로 처리할 수 있는 구조를 만들어주어야 한다.

 

응답에 대해 일관되게 처리할 수 있는 추상화된 구조가 필요하다.

 

에러에 대해 일관되게 처리해야 한다.

웹 애플리케이션을 개발할 때마다 꼭 필요할 것은 기능을 매번 작성하는 것은 생산성 저하라는 큰 문제가 있다. 

 

하지만 우리는 그렇게 하지 않는다. 왜냐하면 스프링이 해당 기능을 미리 만들어서 수많은 변형에 대한 구현체와 유연한 구조를 가진 코드 구조를 제공하기 때문이다.

 

현재까지 별도의 서블릿 컨테이너의 설치 없이도 독립적으로 실행할 수 있는 환경을 만들었지만 아직 스프링의 기능은 사용하지 않았다. 

 

Spring MVC의 Architecture

이제 독립 실행 애플리케이션 말고 독립 실행형 스프링 애플리케이션을 사용해 보자. 

 

이는 이제 스프링 컨테이너를 대표하는 인터페이스 이름이 존재한다.

 

ApplicationContext의 인터페이스의 주석을 확인해 보자.

Central interface to provide configuration for an application. This is read-only while the application is running, but may be reloaded if the implementation supports this.
An ApplicationContext provides:
Bean factory methods for accessing application components. Inherited from ListableBeanFactory.
The ability to load file resources in a generic fashion. Inherited from the org.springframework.core.io.ResourceLoader interface.
The ability to publish events to registered listeners. Inherited from the ApplicationEventPublisher interface.
The ability to resolve messages, supporting internationalization. Inherited from the MessageSource interface.
Inheritance from a parent context. Definitions in a descendant context will always take priority. This means, for example, that a single parent context can be used by an entire web application, while each servlet has its own child context that is independent of that of any other servlet.
In addition to standard org.springframework.beans.factory.BeanFactory lifecycle capabilities, ApplicationContext implementations detect and invoke ApplicationContextAware beans as well as ResourceLoaderAware, ApplicationEventPublisherAware and MessageSourceAware beans.
See Also:
ConfigurableApplicationContext, org.springframework.beans.factory.BeanFactory, org.springframework.core.io.ResourceLoader
Author:
Rod Johnson, Juergen Hoeller

 

Application은 애플리케이션에 대한 구성을 제공하는 중앙 인터페이스입니다. 

 

ApplicationContext는 다음을 제공한다.

 

1. Bean이라고 하는 애플리케이션 컴포넌트에 접근하는 Factorymethod를 지원한다.

2. file 리소스를 로드하는 기능이 있으며 이는  ResourceLoader를 상속한다.

3. 이벤트를 구독하고 발행하는 기능이 존재한다. 이는 ApplicationEventPublisher 인터페이스를 상속한다.

4. 국제화를 지원하기 위한 메시지 기능이 존재한다. 이는 MessageSource 인터페이스에서 상속된다.

5. 부모 콘텍스트에 대한 상속 부분이 있다. 서블릿을 활용하여 웹 애플리케이션을 만들게 되면 각 서블릿은 다른 서블릿을 상속받은 하위 콘텍스트와 독립적인 자체의 하위 콘텍스트를 가지지만 ApplicationContext는 웹 애플리케이션의 전반적으로 단 하나의 부모 콘텍스트만 가지기 때문에 전체 웹 컴포넌트에서 사용할 수 있다. 

 

5번 특징에 대해 조금 더 알아보자.

 

스프링 콘텍스트는 애플리케이션의 Bean을 관리하기 위한 영역이다. 그렇기에 하나의 싱글 콘텍스트가 존재하며, 이로 인해 여러 컴포넌트(Bean)들이 하나의 영향을 받게 됩니다. 

그러나 서블릿 환경에서는 각 서블릿마다 개별적인 콘텍스트를 가지고 있기 때문에, 각 서블릿마다 독립적인 컴포넌트들을 관리할 수 있습니다. 

 

이렇게 분산된 환경을 가지는 서블릿 환경은 서블릿마다 환경을 개별적으로 설정할 수 있지만 서블릿 간 공유를 하지 못한다는 특징이 있습니다. 

 

공유를 하지 못한다면 다음과 같은 문제가 발생할 수 있습니다.

서블릿 A에서 생성한 객체를 서블릿 B에서 사용할 수 없습니다. 

이를 공유하기 위해 공유 서블릿을 만들고 콘텍스트 외부에서 생성해서 주입하는 방식을 해야 합니다.

결국 각각의 환경을 갖기 때문에 더 많은 리소스와 공통화를 위해 많은 노력이 들어가는데 ApplicationContext는 싱글 콘텍스트를 가지게 함으로 써 더 많은 장점을 가질 수 있도록 설계한 것 같습니다.

 

이제 간단하게 코드로 ApplicationContext를 사용해 보자.

기존 서블릿은 객체를 생성해서 서블릿을 등록하는 과정을 거쳤습니다. 하지만 스프링은 메타 데이터를 통해 객체를 생성하도록 합니다. 

public static void main(String[] args) {

     GenericApplicationContext applicationContext = new GenericApplicationContext();
        applicationContext.registerBean(HelloController.class);
        applicationContext.refresh();


}

@RestController
public class HelloController {
}

 

1. registerBean을 통해 애플리케이션 콘텍스트에 의해 관리될 Bean을 등록합니다.

2. 실제 객체를 생성하는 과정이 필요한데 이를 위해 refresh() 메서드를 호출할 수 있습니다.

 

서블릿 컨테이너에서 Bean 객체를 가져와 요청에 대한 처리를 하도록 구성할 수 있습니다.

 

Bean 객체의 라이프 사이클은 ApplicationContext가 관리하기 때문에 객체를 가져오는 것도 ApplicationContext에게 위임해야 합니다.

WebServer webServer = serverFactory.getWebServer(servletContext -> {
    servletContext.addServlet("oncerun", new HttpServlet() {
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

            /**
             * 1. 요청에 따라 적절한 handler 를 가져와 요청을 위임한다. 
             */
            HelloController helloController = applicationContext.getBean(HelloController.class);
            String res = helloController.invoke();
            /** 2. 응답을 받아서 클라이언트에게 응답한다.
             * */
            
        }
    }).addMapping("/*");
});

 

정리해 보자.

 

기존 Spring의 Web Application이 어떻게 동작했는지 확인해 보자.

 

1.WAS에 의해 web.xml 로딩

2.ContextLoaderListener Class를 통해 ApplicationContext 생성

3. root-context.xml 로딩

4. Spring Container가 생성되며 등록된 Bean을 초기화한다.

5. dispatcherServlet이 FrontController 역할을 수행하여 적절하게 요청과 응답을 처리한다.

6. dispatcherServlet도 서블릿이기에 별도의 설정파일도 존재했다. servlet-context.xml

 

 

이제 스프링 부트의 핵심 목표를 보자.

 

독립실행형의 애플리케이션을 지향한다.

 

따라서 비기능적인 기술에 대해 제공한다. 마치 WAS의 별도 설치 없이 코드레벨에서 WAS를 실행하고 ApplicationContext에 대한 설정을 코드레벨로 하여 즉시 실행할 수 있도록 제공한다.

 

코드 생성이나 XML 설정을 지양한다.

 

현재까지 XML 파일을 작성하지 않았다. 이러한 구성은 코드 레벨에서  필요하다면 직접 주입하고 그렇지 않다면 스프링 부트가 강한 주장을 하는 기술 조합을 사용하도록 한다. 물론 원한다면 커스터마이징을 손쉽게 할 수 있다.

 

지금까지 스프링 부트로 Main() 함수에서 시작하여 독립적으로 실행할 수 있는 하나의 웹 애플리케이션의 일부분을 구현하는 것을 확인했다. 

 

여기까지 살펴보았을 때 한 가지 특징을 알 수 있었는데 스프링과 스프링 부트는 다른 프레임워크라는 것이다.

그 이유는 프레임워크라면 고유의 목표와 철학, 그리고 어느 정도의 강제하는 개발방식이 있다. 

 

스프링과 스프링 부트는 이러한 특징에 있어 같다고 말할 수 없는 것 같다. 

 

여기까지 간단하게 독립실행형이 뭔지 조금 알아보았다. 다음에는 실제 간단한 API 서버로 동작하도록 개발하면서 스프링 부트가 무엇을 자동구성했는지 알아볼 것이다.

 

 

 

반응형

댓글