독립 실행형 서블릿 애플리케이션

jinni·2023년 3월 2일

Springboot

목록 보기
3/3

해당 내용은 토비의 스프링 부트(인프런 강의)를 수강하고 정리한 내용입니다.

Containerless 개발 준비

public class HellobootApplication {

	public static void main(String[] args) {
    }
    
}

어노테이션과 run 메서드를 날렸다.
이 상태로 실행하게 되면, 아무런 일도 일어나지 않는다.

그렇다면, main() 메서드가 동작하는지 확인해보자.

public class HellobootApplication {

    public static void main(String[] args) {
        System.out.println("Hello Containerless Standalone Application");
    }

}

메인 메서드가 정상적으로 실행되는 것을 알 수 있다.

서블릿 컨테이너 띄우기

서블릿 컨테이너를 설치하지 않고, 직접 만들어보자.
Standalone 프로그램에서 서블릿 컨테이너를 자동으로 띄우기

아무것도 들어있지 않는 서블릿 컨테이너 띄우기

main() 메서드를 통해 톰캣을 띄울 것이다. (톰캣은 자바로 만들어졌기 때문에 만들 수 있다.)

  • 임베디드 톰캣 라이브러리를 사용
public class HellobootApplication {

    public static void main(String[] args) {
        TomcatServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer();
    }

}

TomcatServletWebServerFactory 클래스를 사용하면 된다. 허나, 굳이 해당 클래스를 사용하지 않아도 된다.

왜냐고?

public class HellobootApplication {

	public static void main(String[] args) {
    	ServletWebServerFactory serverFactory =newTomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer();
	}

}

ServletWebServerFactory 인터페이스를 통해 서블릿 컨테이너 종류에 구애받지 않도록 추상화 작업을 진행해놓았기 때문이다.

그래서 우리는 Tomcat 컨테이너를 인스턴스로 생성해도 되고, Jetty 인스턴스를 생성해도 상관없다. 즉, new 연산자 뒤에 쓰고 싶은 걸로 갈아끼워주면 된다. (by 추상화)

이후 webServer의 start() 내장 메서드를 통해 서블릿 컨테이너를 실행시킬 수 있다.

public class HellobootApplication {

	public static void. main(String[] args) {
    	ServletWebServerFactory serverFactory =newTomcatServletWebServerFactory();
        WebServer webServer = serverFactory.getWebServer();
        webServer.start();
    }

}

path가 없는 상태로 컨테이너가 위와 같이 올라온 것을 알 수 있다.

그럼 요청을 보내보자.

404 상태 코드가 날라오는 것을 보면, 요청이 가는 것을 알 수 있음!!

서블릿 등록

기능을 수행하는 웹 컴포넌트를 생성할 것이다. 즉, 서블릿을 생성한다는 소리다.

위 사진처럼 클라이언트의 요청에 따라 1:1로 대응하는 서블릿을 생성해줄 것이다. 그럼 로직을 수행하고 응답해준다.

요청과 응단은 어떻게 생겼을까?

📌  Request

  • Request Line: Method, Path, HTTP Version
  • Headers
  • Message Body

📌 Response

  • Status Line: HTTP Version, Status Code, Status Text
  • Headers
  • Message Body

서블릿 등록하기

getWebServer의 파라미터로 ServletContextInitializer() 메서드를 익명클래스로 아래와 같이 먼저 구현한다.

public static void main(String[] args) {
	ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
    
    // parameter -> 서블릿을 생성해보자.
    // 익명 클래스를 통해 구현.
    WebServer webServer = serverFactory.getWebServer(new ServletContextInitializer() {
    
      @Override
      public void onStartup(ServletContext servletContext) throws ServletException {
      }
    });
    
    webServer.start();
}

허나, ServletContextInitializer라는 녀석은 FuntionalInterface로 메서드가 1개로 이루어진 녀석이기 때문에 우리는 아래와 같이 람다식으로 표현해줄 수 있다.

public static void main(String[] args) {
	ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
    
      // parameter -> 서블릿을 생성해보자.
      // 익명 클래스를 통해 구현.
      WebServer webServer = serverFactory.getWebServer(servletContext -> {
      });
    
    webServer.start();
}

순서를 간략하게 설명해보자면,

  • servletContext를 통해 addServlet() 메서드로 등록해준다.
  • 인자로 String ServletName, Servlet servlet 을 받아야함.
  • HttpServlet 클래스를 통해 Servlet 을 넣어줌.
  • service 클래스 오버라이딩 진행.
  • 요청에 대한 서블릿 매핑 진행.
  • 응답 작성.

위의 순서를 토대로 코드를 작성해보자.

public static void main(String[] args) {
	ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
    
    // parameter -> 서블릿을 생성해보자.
    // 익명 클래스를 통해 구현.
    WebServer webServer = serverFactory.getWebServer(servletContext -> {
      servletContext.addServlet("hello", new HttpServlet() {

        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.setStatus(200);                                            // StatusCode 설정
            resp.setHeader("Content-Type", "text/plane");       // 헤더 -> Content_Type 설정 
            resp.getWriter().print("Hello Servlet");                        // Message Body 설정
        }
      }).addMapping("/hello");
    });
    
    webServer.start();
}

여기서 addMapping()은 어떤 URI 요청을 매핑받을 것인지 설정하는 것이다. 우리는 /hello 라는 URI에 대해 요청을 받을 것이기 때문에 해당 값을 인자로 넣어준다.

요청을 보내면 아래와 같이 응답이 오는 것을 알 수 있다.

하지만, status, header 같은 경우는 String으로 작성된 것을 알 수 있다. 이는 휴먼 에러를 일으키기 딱이다. 즉, 오타가 날 경우, 클라이언트가 원하는 값을 얻지 못 할 수도 있다는 뜻이다.
그렇기 때문에 우리는 스프링에서 기본적으로 제공하는 상수를 통해 리팩토링을 아래와 같이 진행해주면 좋다.

public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        // parameter -> 서블릿을 생성해보자.
        // 익명 클래스를 통해 구현.
        WebServer webServer = serverFactory.getWebServer(servletContext -> {
            servletContext.addServlet("hello", new HttpServlet() {
                @Override
                protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
                    resp.setStatus(HttpStatus.OK.value());                                      // StatusCode 설정
                    resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);       // 헤더 -> Content_Type 설정
                    resp.getWriter().print("Hello Servlet");                                    // Message Body 설정
                }
            }).addMapping("/hello");
        });
        webServer.start();
}
  • HttpStatus 클래스는 상태 코드를 상수로 제공해주고 있다.
  • HttpHeaders 클래스 안에 상수로 여러 가지의 헤더 key가 존재한다.
  • MediaType 클래스는 Content-Type에 대한 value를 상수로 제공하는 클래스다.

서블릿 요청 처리

클라이언트의 요청은 쿼리 스트링으로 name에 대해 응답 받고 싶어 한다. 이는 HttpServletRequest 객체를 통해 아래와 같이 처리할 수 있다.

public static void main(String[] args) {
	ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
    // parameter -> 서블릿을 생성해보자.
    // 익명 클래스를 통해 구현.
    WebServer webServer = serverFactory.getWebServer(servletContext -> {
    	servletContext.addServlet("hello", new HttpServlet() {
          @Override
          protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
              String name = req.getParameter("name");

              resp.setStatus(HttpStatus.OK.value());                                      // StatusCode 설정
              resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);       // 헤더 -> Content_Type 설정
              resp.getWriter().print("Hello " + name);                                    // Message Body 설정
          }
		}).addMapping("/hello");
	});
    
    webServer.start();
}
  • getParameter()를 통해 name 값을 추출해주고 바인딩해준다.

따로 요청 보낸 것에 대한 결과값은 넣지 않겠다.

프론트 컨트롤러


Servlet은 요청에 대해 1:1로 매핑해주어야 한다. 그럼, 서블릿이 많아질 수록 생겨나는 문제점들이 있다.
1. 중복되는 코드 발생
2. request, response 객체 핸들링

위와 같은 요구사항으로 인해 FrontController가 등장했다.
FrontController는 모든 서블릿에 등장하는 코드를 중앙화된 상태로 앞단에서 요청을 처리한다. 이후, 요청의 종류에 따라 핵심 로직을 처리해주는 또다른 오브젝트에게 요청을 위임한다.

프론트 컨트롤러의 역할

  1. 인증
  2. 보안
  3. 다국어 처리
  4. 공통 기능(로직) 등.

프론트 컨트롤러로 전환

우리의 코드를 프론트 컨트롤러로 바꿔보자.
프론트 컨트롤러를 생성하게 되면, 핵심 로직은 다른 오브젝트에 위임해야 한다.

public static void main(String[] args) {
        ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
        // parameter -> 서블릿을 생성해보자.
        // 익명 클래스를 통해 구현.
        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());                                      // StatusCode 설정
                        resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);       // 헤더 -> Content_Type 설정
                        resp.getWriter().print("Hello " + name);                                    // Message Body 설정
                    } else if (req.getRequestURI().equals("/user")) {
                        //
                    } else {
                        // 404 처리
                        resp.setStatus(HttpStatus.NOT_FOUND.value());
                    }
                }
            }).addMapping("/*");                                                                    // * -> 모든 것
        });
        webServer.start();
}

여기서 post 요청을 보내면 404 상태 코드가 날라오게 된다. 따로 사진은 넣지 않겠다.

이렇게 프론트 컨트롤러를 생성하게 되면, 위에서도 언급했듯이 핵심 로직은 어떻게 처리할 지다.
1. 다른 컨트롤러에 요청을 위임
2. 다른 방식으로 요청할 지

위 두 가지 요소에 대해 고민해보면 좋다.

HelloController 매핑과 바인딩

바로 위에서 짠 코드의 분기문을 잘게 쪼개보자.

HelloController

public class HelloController {

    public String hello(String name) {
        return "Hello " + name;
    }
}
public static void main(String[] args) {
	ServletWebServerFactory serverFactory = new TomcatServletWebServerFactory();
    
    // parameter -> 서블릿을 생성해보자.
    // 익명 클래스를 통해 구현.
    WebServer webServer = serverFactory.getWebServer(servletContext -> {
    	servletContext.addServlet("frontcontroller", new HttpServlet() {
        	HelloController helloController = new HelloController();
                
			@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);                                   // HelloController

					resp.setStatus(HttpStatus.OK.value());                                      // StatusCode 설정
                    resp.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE);       // 헤더 -> Content_Type 설정
                    resp.getWriter().print(ret);                                                // Message Body 설정
				} else if (req.getRequestURI().equals("/user")) {
                	//
                } else {
                	// 404 처리
                	resp.setStatus(HttpStatus.NOT_FOUND.value());
                }
			}
		}).addMapping("/*");                                                         // * -> 모든 것
    });
    
    webServer.start();
}

위와 같이 request 객체와 분기문을 통해 쪼갤 수 있다.

매핑이란?

웹 요청에 들어있는 정보를 활용해, 어떤 로직을 수행하는 코드를 호출할 것인가에 대해 결정하는 작업이다.

바인딩이란?

간단하게 말해, 웹 요청을 가지고 새로운 타입으로 변환해주는 작업을 의미한다. 위 코드에서는 name 값을 String ret로 변환해준 것처럼 말이다.

더 알아볼 것

그렇다면, Spring MVC 에서는 바인딩을 어떻게 할까? 를 고민해보고 공부해보면 좋을 것 같다!!

profile
조금씩 천천히 꾸준하게

0개의 댓글