스프링 웹 MVC-02.서블릿 소개, 애플리케이션 개발, 리스너와 필터
서블릿 소개
서블릿 (Servlet)
- 자바 엔터프라이즈 에디션은 웹 애플리케이션 개발용 스팩과 API 제공.
자바로 웹 애플리케이션을 개발할 수 있는 스펙과 API 제공(자바로 웹을 만들기 위해 필요한 기술)
좀 더 설명하면, 클라이언트가 어떠한 요청을 하면 그에 대한 결과를 다시 전송해 주어야 하는데, 이러한 역할을 하는 자바 프로그램. - 요청 당 쓰레드 (만들거나, 풀에서 가져다가) 사용
한 요청을 처리할때마다 새로운 프로세스를 만들고 죽이는게 아니라, 한 프로세스 내의 자원을 공유하는 스레드를 만들어서 요청을 처리한다.
그 중에 가장 중요한 클래스중 하나가 HttpServlet.
- 서블릿 등장 이전에 사용하던 기술인 CGI (Common Gateway Interface)
요청 당 프로세스를 만들어 사용
서블릿 동작 방식
- 사용자(클라이언트)가 URL을 클릭하면 HTTP Request를 Servlet Container로 전송합니다.
- HTTP Request를 전송받은 Servlet Container는 HttpServletRequest, HttpServletResponse 두 객체를 생성합니다.
- web.xml은 사용자가 요청한 URL을 분석하여 어느 서블릿에 대해 요청을 한 것인지 찾습니다.
- 해당 서블릿에서 service메소드를 호출한 후 클리이언트의 POST, GET여부에 따라 doGet() 또는 doPost()를 호출합니다.
- doGet() or doPost() 메소드는 동적 페이지를 생성한 후 HttpServletResponse객체에 응답을 보냅니다.
- 응답이 끝나면 HttpServletRequest, HttpServletResponse 두 객체를 소멸시킵니다.
CGI(Common Gateway Interface)란?
- 특별한 라이브러리나 도구를 의미하는 것이 아니고, 별도로 제작된 웹서버와 프로그램간의 교환방식
- CGI방식은 어떠한 프로그래밍언어로도 구현 가능
- 별도로 만들어 놓은 프로그램에 HTML의 Get or Post 방법으로 클라이언트의 데이터를 환경변수로 전달하고, 프로그램의 표준 출력 결과를 클라이언트에게 전송
- 한 요청을 처리할때마다 새로운 프로세스를 만들어 사용한다.
서블릿의 장점 (CGI에 비해)
- 빠르다.
- 플랫폼 독립적
- 이식성
자바 기반이기 때문에 플랫폼(OS)에 독립적이고, 이식성이 좋다.
- 보안
자바 자체가 지원하는 보안, 컨테이너가 제공하는 기능의 지원을 받아 보안성이 좋다.
서블릿 엔진 또는 서블릿 컨테이너 (톰캣, 제티, 언더토우, …)
서블릿 스펙을 준수하는 컨테이너들.(서블릿을 관리해주는 컨테이너)
서블릿 컨테이너는 클라이언트의 요청(Request)을 받아주고 응답(Response)할 수 있게, 웹서버와 소켓을 만들어 통신
- 네트워크 서비스
웹서버와의 통신 지원(서블릿과 웹서버가 손쉽게 통신할 수 있게) 일반적으로 우리는 소켓을 만들고 주시(listen)하고, 요청이 수신되면 요청을 받아들여(accept). 등을 해야하지만
서블릿 컨테이너는 이러한 기능을 API로 제공하여 복잡한 과정을 생략할 수 있게 해줍니다.
그래서 개발자가 서블릿에 구현해야 할 비지니스 로직에 대해서만 초점을 두게끔 도와줌 - 서블릿 생명주기 관리
서블릿 컨테이너는 서블릿의 탄생과 죽음을 관리. 서블릿 클래스를 로딩하여 인스턴스화하고,
초기화 메소드를 호출하고, 요청이 들어오면 적절한 서블릿 메소드를 호출.
또한 서블릿이 생명을 다 한 순간에는 적절하게 Garbage Collection(가비지 컬렉션)을 진행하여 편의를 제공
(서블릿을 어떻게 초기화하고, 어떻게 실행하고 사용해야 하는지 관리할 줄 안다.) - 세션 관리
- MIME 기반 메시지 인코딩 디코딩 …
서블릿 생명주기
- 서블릿 컨테이너가 서블릿 인스턴스의 init() 메소드를 호출하여 초기화 한다.
- 최초 요청을 받았을 때 한번 초기화 하고 나면 그 다음 요청부터는 이 과정을 생략한다.
클라이언트의 요청이 들어오면 컨테이너는 해당 서블릿이 메모리에 있는지 확인하고, 없을 경우 init()메소드를 호출하여 적재.
- 최초 요청을 받았을 때 한번 초기화 하고 나면 그 다음 요청부터는 이 과정을 생략한다.
- 서블릿이 초기화 된 다음부터 클라이언트의 요청을 처리할 수 있다. 각 요청은 별도의 쓰레드로 처리하고 이때 서블릿 인스턴스의 service() 메소드를 호출한다.
- 이 안에서 HTTP 요청을 받고 클라이언트로 보낼 HTTP 응답을 만든다.
- service()는 보통 HTTP Method에 따라 doGet(), doPost() 등으로 처리를 위임한다.
- 따라서 보통 doGet() 또는 doPost()를 구현한다.
- 서블릿 컨테이너 판단에 따라 해당 서블릿을 메모리에서 내려야 할 시점에 destroy()를 호출한다.
참고사항
- 서블릿은 자바 코드 속에 HTML 코드.
- JSP는 HTML 코드 속에 자바 코드.
서블릿 애플리케이션 개발
준비물: 메이븐, 톰캣
의존성 추가(pom.xml)
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope> <!-- 코딩하는 시점에는 쓸 수 있으나, 런타임시점(war,jar 패키징시) 클래스패스에서 빠짐.
톰캣 컨테이너에서 제공해줄 것이기 때문에, 런타임 시에도 클래스패스에 가지고 있지 않아도 된다. 지정해준 것 -->
</dependency>
main/java 디렉토리 생성 후
project structer - modules
source 디렉토리로 설정
서블릿 구현
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("init");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doGet");
resp.getWriter().write("Hello Servlet");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
톰캣 처음 사용시
/bin 폴더 가서 실행 권한 부여해줘야 함.(mac, 권한없다는 오류 발생)
chmod +x ./*.sh
서블릿 등록(web.xml)
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!-- hello 라는 서블릿 -->
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>me.whiteship.HelloServlet</servlet-class>
</servlet>
<!-- hello 서블릿을 /hello 요청에 매핑-->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
톰캣으로 띄우고 /hello 요청을 하면
init
doGet
이 출력된다.
이후에 /hello 요청을 한번 더 하면
doGet
만 출력이 된다.
(해당 서블릿이 이미 init 된 상태이기 때문에 init 하지 않음.)
서블릿 컨테이너(톰캣)을 종료하면
메모리가 올려져 있던 서블릿들을 종료해야 하니
destroy
가 출력된다.
서블릿 리스너와 필터
서블릿 리스너
- 웹 애플리케이션에서 발생하는 주요 이벤트를 감지하고 각 이벤트에 특별한 작업이 필요한 경우에 사용할 수 있다.
- 서블릿 컨텍스트 수준의 이벤트
- 컨텍스트 라이프사이클 이벤트
- 컨텍스트 애트리뷰트 변경 이벤트
- 세션 수준의 이벤트
- 세션 라이프사이클 이벤트
- 세션 애트리뷰트 변경 이벤트
- 서블릿 컨텍스트 수준의 이벤트
리스너 예제
public class MyListner implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("context Initialized");
sce.getServletContext().setAttribute("name", "jaeuk");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("context Destroyed");
}
}
리스너 등록(web.xml)
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<listener>
<listener-class>me.jaeuk.MyListner</listener-class>
</listener>
<!-- hello 라는 서블릿 -->
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>me.jaeuk.HelloServlet</servlet-class>
</servlet>
<!-- hello 서블릿을 /hello 요청에 매핑-->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
리스너에서 보내준 것을 doGet에서 활용 가능하다.
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("doGet");
System.out.println("getName : " + getName());
resp.getWriter().write("Hello Servlet");
}
private Object getName(){
return getServletContext().getAttribute("name");
}
실행을 하면
- 리스너의 context Initialized
- 요청이 들어온다면
- 서블릿의 init
- 서블릿의 doGet // 만약 몇번 더 요청한다면 doGet만 호출
- 종료시
- 서블릿의 destroy
- 리스너의 context Destroyed
순으로 출력되는 것을 볼 수 있다.
서블릿 필터
- 들어온 요청을 서블릿으로 보내고, 또 서블릿이 작성한 응답을 클라이언트로 보내기 전에 특별한 처리가 필요한 경우에 사용할 수 있다.
- 체인 형태의 구조
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter init");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("Filter doFilter");
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("Filter destroy");
}
}
필터 등록(web.xml)
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<filter>
<filter-name>myFilter</filter-name>
<filter-class>me.jaeuk.MyFilter></filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<servlet-name>hello</servlet-name>
<!-- 여러 개의 서블릿에 적용을 해야 한다면 패턴으로도 적용 가능 -->
<!-- <url-pattern></url-pattern> -->
</filter-mapping>
<listener>
<listener-class>me.jaeuk.MyListner</listener-class>
</listener>
<!-- hello 라는 서블릿 -->
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>me.jaeuk.HelloServlet</servlet-class>
</servlet>
<!-- hello 서블릿을 /hello 요청에 매핑-->
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
실행을 하면
- 리스너의 context Initialized
- 필터의 Filter init
- 요청이 들어온다면
- 서블릿의 init
- 필터의 Filter doFilter
- 체이닝을 거쳐서 서블릿한테 간다. // filterChain.doFilter(servletRequest, servletResponse);
- 서블릿의 doGet // 리스너에 넣어놓았던 정보 가져와서 출력
- 종료시
- 서블릿의 destroy
- 필터의 Filter destroy
- 리스너의 context Destroyed
참고
https://docs.oracle.com/cd/B14099_19/web.1012/b14017/filters.htm#i1000654