1. Containerless 개발준비
앞서 말한 바와 같이, 컨테이너 설치와 배포 등의 작업을 하지 않고 컨테이너를 동작시키는 방법을 코드를 구현할 수 있다. 스프링 부트가 사용하는 것으로 보이는 아래 두 라인을 제거하고 빈 main 메서드만 남긴다.
기존 코드
@SpringBootApplication
public class HellobootSelfApplication {
public static void main(String[] args) {
SpringApplication.run(HellobootSelfApplication.class, args);
}
}
스프링부트에서 제공하는 어노테이션과 메소드 내용을 지운 후 준비
public class HellobootSelfApplication {
public static void main(String[] args) {
}
}
2. 서블릿 컨테이너 띄우기
스프링 부트 프로젝트를 만들 때 추가한 web모듈로 인해 아래와 같은 내장형 톰캣 라이브러리가 추가된다.
다시 한번 이야기하자면, 서블릿은 자바의 표준 기술이고 이 표준 기술을 구현한 제품들이 많이 존재한다. 그중 가장 대표적인 서블릿 컨테이너가 Tomcat이다.
Tomcat은 자바로 만들어진 프로그램으로, 'embed tomcat'(임베디드 톰캣)이라는 라이브러리를 제공해준다. Tomcat을 정상적으로 실행하고, 서블릿을 띄우기 위해 준비하는 과정이 많지만 스프링 부트는 이와 같은 웹 서버의 복잡한 생성과정과 설정들을 미리 결정하고 생성해 주는 도우미 클래스가 있다.
내장형 Tomcat의 초기화 작업과 간편한 설정을 지원하도록 스프링 부트가 제공하는 TomcatServletWebServerFactory를 사용하면 톰캣 웹 서버(서블릿 컨테이너)를 실행하는 코드를 만들 수 있다.
public class HellobootSelfApplication {
public static void main(String[] args) {
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer();
webServer.start();
}
}
ServletwebServerFactory를 생성해준 후, 구현체로 TomcatServletWebServerFactory를 선택해 준다. serverFactory에서 서버를 만든 후 start를 실행하면 서블릿 컨테이너(톰캣) 띄우기에 성공한다.
3. 서블릿 등록
코드에서 서블릿을 등록하려면 ServletContext가 필요하다.
public class HellobootSelfApplication {
public static void main(String[] args) {
ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("hello", new HttpServlet() {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(200);
resp.setHeader("Content-Type", "text/plain");
resp.getWriter().println("Hello Servlet");
}
}).addMapping("/hello");
});
webServer.start(); // Tomcat 서블릿 컨테이너 동작
}
}
우선 main메소드 안의 두 번째 코드부터 살펴보면,
serverFactory.getWebServer()의 파라미터로 servletContext를 넣어줄 수 있다. 이 과정에서 ServletContextInitializer를 구현한 오브젝트를 전달하여 서블릿 등록과 같은 초기화를 할 수 있다. 이때 ServletContextInitializer는 @FunctionalInterface이므로 람다식으로 전환이 가능하다.
서블릿은 HttpServlet클래스를 상속해서 필요한 메서드를 오버라이딩 하는 방식으로 만들 수 있다. 그리고 서블릿을 등록할 때는 서블릿 이름과 서블릿 오브젝트를 이용하고, 서블릿 등록정보에는 매핑할 URL정보를 지정해야 한다.
servletContext.addServlet("hello", new HttpServlet() {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(200);
resp.setHeader("Content-Type", "text/plain");
resp.getWriter().println("Hello Servlet");
}
}).addMapping("/hello");
서블릿에서는 HttpServletRequest를 이용해서 요청 정보를 가져오고, HttpServletResponse를 이용햇해서 응답을 만드는 작업을 수행한다. 3가지 요소(상태코드, 헤더, 바디)를 이용해서 웹 요청을 생성한다.
4. 프론트 컨트롤러
여러 요청을 처리하는데 반복적으로 등장하게 되는 공통 작업을 하나의 오브젝트에서 일괄적으로 처리하게 만드는 방식을 프런트 컨트롤러 패턴이라고 한다. 서블릿을 프런트 컨트롤러로 만들려면 모든 요청, 혹은 일정 패턴을 가진 요청을 하나의 서블릿이 담당하도록 매핑해 준다.
프런트 컨트롤러가 모든 URL을 다 처리할 수 있도록 서블릿 바인딩을 변경한다.
}).addMapping("/*");
서블릿 내에서 HTTP 요청정보를 이용해서 각 요청을 분리한다. 만약 처리할 수 있는 HTTP 요청 정보가 없다면 상태코드를 404로 설정한다.
if (req.getRequestURI().equals("/hello") && req.getMethod().equals(HttpMethod.GET.name())) { ...
}
else if (req.getRequestURI().equals("/user")) {
... }
else { resp.setStatus(HttpStatus.NOT_FOUND.value());
}
5. Hello 컨트롤러 매핑과 바인딩
프런트 컨트롤러가 요청을 분석해서 처리할 요청을 구분한 뒤에 이를 처리할 핸들러(컨트롤러 메서드)로 요청을 전달한다. 핸들러가 처리하고 돌려준 리턴 값을 해석해서 웹 요청을 생성한다.
프론트 컨트롤러가 HTTP 요청을 처리할 핸들러를 결정하고 연동하는 작업을 매핑이라고 한다. 또, 핸들러에게 웹 요청 정보를 추출하고 의미 있는 오브젝트에 담아서 전달하는 작업을 바인딩이라고 한다. 프런트 컨트롤러의 두 가지 중요한 기능은 매핑과 바인딩이다.
if (req.getRequestURI().equals("/hello") && req.getMethod().equals(HttpMethod.GET.name())) {
String name = req.getParameter("name");
String ret = helloController.hello(name);
resp.setStatus(HttpStatus.OK.value());
resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().println(ret);
}
매핑과 바인딩은 세밀한 규칙을 부여하면 매번 코드를 작성하지 않고도 공통 코드를 이용해서 이를 처리할 수 있도록 만들 수 있다.