
우리가 앞 시간에서 빈 서블릿 컨테이너를 띄우는 것까지는 코드를 이용해 해보았다. 임베디드 톰캣을 띄워봤다. 이번 시간에는 여기에 어떤 기능을 수행하는 웹 컴포넌트를 넣을 예정. 서블릿 컨테이너 안에 들어가는 웹 컴포넌트를 우리는 서블릿이라 부른다.

원래 서블릿은 컨테이너에 여러 개가 들어가게 되어 있다. 그리고 서블릿 컨테이너가 웹 클라이언트로부터 요청을 받으면 여러 개의 서블릿 중 어떤 서블릿에게 이 일을 맡길까를 결정하는데 이 결정하는 작업을 매핑한다고 이야기한다. 서블릿 컨테이너에 요청을 서블릿의 요청으로 다시 넘겨주는 것. 그러면 서블릿이 웹응답을 만들기 위해 필요로 하는 어떤 작업들을 수행하고 작업을 종료한다. 그러면 컨테이너가 다시 웹 클라이언트한테 웹응답 형태로 이것을 돌려준다.
<aside> ⚠️
웹 요청과 응답이 어떻게 생겼는지에 대해서는 앞서 우리가 자세히 살펴보았다. 절대 잊으면 안되는 것으로 웹을 이용한 개발을 하는 동안에는 웹 표준 프로토콜을 이용해 요청이 어떻게 들어가고 응답을 어떻게 받는지, 그게 어떤 내용으로 구성 되어 있는지에 대해 머릿속에 잘 담고 있어야 한다. 그리고 어떠한 종류의 웹 기술을 만나더라도 이 기술에서는 요청을 이런 방식으로 받아 이렇게 메소드로 전달해주는구나 이렇게 매핑하는데 사용하네와 같은 것들을 빨리 파악해야 한다. 내가 코드를 작성하면 어떻게 응답이 만들어질까 역시 생각해야 한다. 직관적으로 만드는 그런 로우 레벨의 웹 프로그래밍도 있지만 요즘은 그것보다 훨씬 추상화된 방법을 많이 이용해서 얼핏 보면 내가 무언가를 리턴했는데 이게 바디로 들어가는지, 헤더로 들어가는지, 응답코드가 되는 건지, 또 다른 작업을 하는지 분간 못 할 수 있다. 스프링 또한 스프링 MVC가 꽤 복잡하고 어려운데 우리가 쓰는 평범한 Java 메소드의 파라미터와 리턴값 이게 요청과 응답과 어떻게 매핑되는가를 잘 파악하는게 스프링 MVC 를 마스터하는 중요한 과정이다. 그러기 위해서는 웹과 관련된 어떤 코드를 볼 때마다 요청과 응답이라는 관점에서 이렇게 연결되는 구나를 머릿속으로 빠르게 떠오를 수 있어야 하고, 새로운 기술을 보면 여기서는 요청은 어떻게 가져오고 응답은 어떻게 만드는지를 빠르게 생각해야 한다.
</aside>
앞에 서버를 띄우는 것까지 했는데, 그러면 서블릿을 이 서블릿컨테이너에 어떻게 추가할 것이냐하면 추가하는 방법이 있다. Spring 에 ServletFactory.getWebServer 라는 메소드가 있는데 여기에 파라미터를 바는다. 파라미터의 타입은 ServletContextInitializer 이다.
익숙한 타입을 아니라 문서를 열어보면 Spring 의 Web 모듈 안에 들어있는 인터페이스이고 역할은 ServletContext 를 프로그램에 의해 구성하는 작업에 사용되어지는 인터페이스이다. 이걸 이용해 서블릿컨테이너에 서블릿을 등록하는데 필요한 작업을 수행하는 그런 Object 를 만드는데 쓴다고 생각.
@FunctionalInterface
public interface ServletContextInitializer {
void onStartup(ServletContext servletContext) throws ServletException; }
ServletContextInitializer 타입의 Object를 하나 만들어서 여기에 파라미터로 전달해야 하는데, 인터페이스다보니 이걸 구현한 클래스를 하나 만들어 집어넣을 수도 있겠지만 이 클래스를 하나 만들어 집어 넣을 수도 있겠지만 이 클래스를 재사용하고 여러 번 이용할 게 아니니 익명 클래스로 만드는 것이 좋을 것. → new 로 마치 생성자를 호출하는 것처럼 하면서 뒤에는 우리가 클래스를 생성하지 않았으니 그 클래스가 구현했어야 하는 혹은 상속했어야 하는 인터페이스나 클래스 이름을 주고 클래스의 바디 부분을 바로 집어넣으면 된다.
public class HellobootApplication {
public static void main(String[] args) {
ServletWebServerFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer(
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
}
});
webServer.start();
}
}
이 ServletContextInitializer 는 메소드가 하나인 FunctionalInterface 라 했다. 그럼 이것을 람다식으로 대체 가능 → 훨씬 간결해진다.
ServletContextInitializer 이름도 생략하고 ServletContext 라는 파라미터를 받아 어떤 작업을 수행하고 특별히 리턴하지 않는 Lambda 식으로 대체해도 충분.
이제는 ServletContext 를 받아 어떻게 Servlet을 등록할지 알아보도록 하자.
ServletContext 에는 addServlet() 이라는 메소드가 3개가 있다. 세 가지 다 첫번째 파라미터로는 서블릿 이름을 받고 그 다음에 서블릿 클래스 정보를 넣거나 서블릿 타입의 오브젝트를 만들어 전달하는데 우리는 오브젝트를 만들어 전달하는 방법을 사용해볼 것. 서블릿 이름은 우리가 Hello 컨트롤러를 만든 것과 유사하게 동작하는 코드를 넣을 것이므로 hello 라 명명, 두 번째 파라미터는 Servlet 타입이 가야하는데 이 Servlet 도 인터페이스이며 타입이 많이 있다. 이걸 일일이 구현하려면 번거로우니 공통적인 코드를 구현해놓고 상속해 사용할 수 있도록 만들어 놓은 일종의 Adapter 클래스가 있다. 이게 HttpServlet 이라는 클래스인데, 이것도 우리가 상속해 필요한 부분만 오버라이딩 해 사용하면 되므로 여기서도 익명 클래스를 사용해 두 번째 파라미터로 전달할 것. 여기서 실제 Servlet 의 기능을 구현하는 건 이 HttpServlet 을 상속해 만든 익명 클래스에서 HttpServlet 이 가지고 있는 메소드 중 원하는 걸 오버라이딩 해 구현하면 되는데 여기서 service 라는 메소드를 사용할 예정.
public class HellobootApplication {
public static void main(String[] args) {
ServletWebServerFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("hello", new HttpServlet() {
@Override
protected void service(HttpSrvletRequest req, HttpServletResponse res) throws ServletException, IOException {
super.service(req, res);
}
});
webServer.start();
}
}
이 service 메소드가 어떻게 생겼는지 잘 보면 앞에서 계속 강조했던 것처럼 웹 프로그래밍은 웹 요청을 받아 응답을 만들어내는 것이다. 그 요청을 가져오는데 필요한 오브젝트, 그 다음 응답을 만드는데 필요한 오브젝트가 파라미터로 딱딱 하나씩 전달이 된다. 이 두 가지를 잘 이용하면 웹 기능을 만들어낼 수 있는 것. 일단 여기까지 만들어두고 이렇게 등록한 Servlet 은 그러면 addServlet() 만 한 번 호출하면 끝나는 거냐 하면 그건 아니고 아까 Servlet 을 하나 추가한다 할 때 필요한 작업이 있다 했는데 그게 뭐냐하면 Servlet 컨테이너가 컨테이너로 들어오는 웹 요청을 어느 Servlet 에 연결을 해줄 것인가를 결정하는 매핑이 필요하다 언급했었다. 그 매핑을 추가해줘야 한다. 여기서 addServlet() 이 리턴하는 Object를 받아 거기게 설정하는 여러 가지 복잡한 작업이 필요한데 최대한 간결하게 진행할 예정.
public class HellobootApplication {
public static void main(String[] args) {
ServletWebServerFactory = new TomcatServletWebServerFactory();
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("hello", new HttpServlet() {
@Override
protected void service(HttpSrvletRequest req, HttpServletResponse res) throws ServletException, IOException {
super.service(req, res);
}
}).addMapping("/hello");
});
webServer.start();
}
}
addServlet() 을 호출하고 리턴되는 Object 를 바로 addMapping 을 불러 URL 패턴을 넣는다. 기본 매핑은 URL 을 이용한다. /hello 로 들어오는 요청이 있으면 이걸 여기서 익명 클래스로 만든 이 객체가 처리하겠다 정의. 그럼 /hello 로 들어오는 웹 요청이 여기로 전달되고 service 라는 메소드 안에서 우리는 응답을 만들어내야 하는데 요청을 분석하는 등의 과정이 다 필요하지만 생략하고 응답만 만들어볼 예정.
응답의 3가지 요소