Containerless 개발 준비
내장형 톰캣을 실행하지 않도록 HellobootApplication.java에서 일부 코드를 삭제한다.
package tobyspring.helloboot;
public class HellobootApplication {
public static void main(String[] args) {
}
}
서블릿 컨테이너 띄우기
톰캣은 설치를 해서 사용할 수도 있지만 내장형 톰캣도 따로 있어 설치하지 않고 임베디드 형식으로 사용할 수 있게 개발되었다. 스프링 부트는 https://start.spring.io/ 에서 프로젝트를 생성하면 프로젝트에 자동으로 임베디드 톰캣 라이브러리가 포함된다. 그럼 프로젝트에서 내장형 톰캣을 불러와 사용해보자.
톰캣 라이브러리를 직접 가져오는게 아니라 스프링에서 제공해주는 TomcatServletWebServerFactory 클래스를 사용한다. 톰캣 라이브러리를 직접 가져오게 되면 복잡한 생성, 수많은 설정들을 해줘야하는 번거로움이 있는데 스프링에서 제공하는 클래스를 사용하면 보다 쉽게 톰캣을 사용할 수 있다.
package tobyspring.helloboot;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServer;
public class HellobootApplication {
public static void main(String[] args) {
TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer();
webServer.start();
}
}
위 코드에서 TomcatServletWebServerFactory로 WebServer 객체를 만든다. WebServer 객체는 단순히 '톰캣'만을 위한 클래스는 아니며 톰캣 같은 서블릿 컨테이너를 구동시켜주는 프로그램들을 위한 클래스이다. 즉, 추상화가 되어있는 클래스다. 만약 톰캣 외 Jetty 서버를 사용하고 싶다면 TomcatServletServerFactory 객체를 JettyServletServerFactory로 변경 후 WebServer 클래스를 사용하면 된다.
서블릿 등록
getWebServer() 메서드 안에 익명 클래스를 생성한다.
WebServer webServer = serverFactory.getWebServer(new ServletContextInitializer() {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
}
});
ServletContextInitializer()는 Functional Interface이기 때문에 람다식으로 간단히 작성할 수 있다.
WebServer webServer = serverFactory.getWebServer(servletContext -> {
});
addServlet()을 이용해 서블릿을 등록한다. HttpServlet의 여러 메서드 중 service()를 오버라이딩한다.
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("hello", new HttpServlet() {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.service(req, resp);
}
});
});
이제 HTTP 응답에 필수 요소인 header, status code, body를 작성하고, 요청이 들어오면 서블릿에서 어느 서비스로 보내줘야하는 mapping 관련 정보도 작성해준다.
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");
});
서버 재실행 후 테스트
서블릿 요청 처리
위에서 작성한 코드는 값들을 문자열로 넣어주었다. 이런식으로 개발자가 직접 작성하면 오타의 위험성이 높기 때문에 스프링에서 제공하는 enum 값을 사용하면 더 안전하게 코드를 작성할 수 있다.
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(HttpStatus.OK.value());
resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().println("Hello Servlet");
}
또한 HelloController에서 작성했던 것 처럼 요청 메세지에서 파라미터를 가져와 출력해보자.
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
resp.setStatus(HttpStatus.OK.value());
resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().println("Hello " + name);
}
요청 메세지 객체인 req에서 getParmeter() 메서드를 사용하면 파라미터 값을 가져올 수 있다.
서버 재실행 후 테스트
프론트 컨트롤러 - 전환
프론트 컨트롤러는 제일 앞단에서 모든 요청을 받아들이는 웹 컴포넌트이다. frontController에서 먼저 요청을 받은 후, 알맞은 서블릿과 매핑되도록 코드를 작성하면 유지보수 및 관리가 더욱 쉬워진다는 장점이 있다. 또한 공통적인 부분을 한 번에 처리할 수 있어 편리하다.
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("frontController", new HttpServlet() {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
if(req.getRequestURI().equals("/hello") && req.getMethod().equals(HttpMethod.GET.name())) {
String name = req.getParameter("name");
resp.setStatus(HttpStatus.OK.value());
resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);
resp.getWriter().println("Hello " + name);
}
else if(req.getRequestURI().equals("/user")) {
//
}
else {
resp.setStatus(HttpStatus.NOT_FOUND.value());
}
}
}).addMapping("/*");
});
원래 기존에 addMaping()값인 "/hello"를 지우고 모든 요청을 받아들이겠다는 의미인 "/*"를 기재한다. 그리고 나서 요청의 uri 값을 가져와 if문으로 분기처리를 한다. uri 값으로만 분기처리할 수도 있지만, HTTP Method의 종류에 따라서도 분기처리가 가능하다. HelloController에 기재된 hello() 메서드는 GET Method이니 요청 uri이 'hello'이면서 HTTP Method가 'GET'인 요청만 받아서 처리하도록 위의 코드를 작성했다.
서버 재실행 후 테스트
GET 메서드 외 다른 HTTP Method를 사용하면 404 에러가 떨어진다.
Hello 컨트롤러 매핑과 바인딩
현재 작성한 코드들은 frontController에서 로직 처리를 하는 코드이다. 원래 이런 식으로 frontController에서 로직 처리하는게 아니라 분리하는게 원칙이다. 로직 처리 부분을 분리해보자.
일단 HelloController에 작성한 hello() 메서드를 재활용하기 위해 코드 일부분을 삭제한다.
public class HelloController {
public String hello(String name) {
return "hello " + name;
}
}
그리고 frontController에서 HelloController 인스턴스를 만들어 사용하자.
WebServer webServer = serverFactory.getWebServer(servletContext -> {
HelloController helloController = new HelloController();
servletContext.addServlet("frontController", new HttpServlet() {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
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);
}
else if(req.getRequestURI().equals("/user")) {
//
}
else {
resp.setStatus(HttpStatus.NOT_FOUND.value());
}
}
}).addMapping("/*");
});
서버 재실행 후 테스트
- 출처 : 인프런 토비의 스프링부트 - 이해와 원리 강의