부스트코스 강의를 듣고 정리한 내용.

전에 듣다가 환경설정에서 막혀서 하다 말았는데 이번에도 인텔리제이에서는 프로젝트 세팅부터 어려움을 겪었다. maven으로 하니까 artifact 설정해줘야 하는데 war 파일 생성도 안되고 ㅠㅠ 구글링하다가 gradle로 바꾸면 된대서 어찌어찌 해보는데 문제는 application setting을 바꾸니 안됨. 도메인 네임 더러운거 싫단 말이다.. 그래서 하는 수 없이 이클립스 쓰고 있다. 찡찡

Servlet

Servlet은 URL 요청을 처리하는 프로그램이라 생각하면 된다.

http://localhost:8080/프로젝트이름/URL매핑값

HelloServlet이라는 서블릿을 만들어보자.

http://localhost:8080/servlet-test/HelloServlet

@WebServlet("/HelloServlet")
public class HelloServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
    /**
     * @see HttpServlet#HttpServlet()
     */
    public HelloServlet() {
        super();
        // TODO Auto-generated constructor stub
    }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setContentType("text/html;charset=UTF-8");
		PrintWriter out = response.getWriter();
		out.write("<h1>Hello Servlet</h1>");
		response.getWriter().append("Served at: ").append(request.getContextPath());
	}
}

HTTP req의 첫 번째 줄에는 메소드가 온다. 메소드에는 GET, POST, PUT, DELETE 등이 있는데 웹 브라우저가 GET 메소드 방식으로 요청을 보낼 때 서블릿에 doGet()메소드가 호출이 된다, GET 메소드는 웹 브라우저가 서버에게 문서를 요청할 때 사용하는 방식이다.

resp는 응답할 내용을 모아 추상화해놓은 객체이다. 이 응답 결과를 보내줄 객체에다가 ContentType를 정해주는 것이다. 그러면 브라우저가 컨텐츠 타입을 보고 아~ %%구나 하고 해석을 할 수 있게 된다. 만약 “text/html”이면 “난 텍스트를 보낼건데 이 텍스트는 html이야” 라는 의미이다. 그리고 charset이 다르면 한글이 깨질 수 있으니 UTF-8로 설정한다.

출력할 때는 response가 갖고 있는 getWriter()를 호출하고, 여기에 out에다가 보내줄 응답 내용을 써주면 됨

실행할 때 Java Resources > src 폴더 안의 servlet 우클릭 > run as > run on server 해주면 됨.

Java Web Application

  • WAS에 설치(deploy)되어 동작하는 애플리케이션
  • 자바 웹 애플리케이션에는 HTML, CSS, 이미지, 자바로 작성된 클래스 (서블릿 포함, 패키지, 인터페이스 등), 각종 설정 파일 등이 포함된다.

자바 웹 애플리케이션의 디렉토리 구조

  • 자바 웹 애플리케이션
    • WEB-INF 폴더
      • web.xml 파일 (배포기술자, Deployment Descriptor: servlet 3.0 미만에서는 필수, 3.0 이상에서는 어노테이션을 사용): 웹앱에 대한 정보들을 다 갖고 있는 녀석
      • lib 폴더
        • jar 파일들
      • classes 폴더
        • java 패키지, class들, 리소스 들
    • 각종 폴더, 이미지, 다양한 리소스들

Servlet

  • 자바 웹 애플리케이션의 구성요소 중 동적인 처리를 하는 프로그램
  • 서블릿을 정의해보면
    • WAS에서 동작하는 Java 클래스
    • HttpServlet 클래스를 상속받아야 함
    • 서블릿과 JSP로부터 최상의 결과를 얻으려면, 웹페이지를 개발할 때 서블릿과 JSP를 조화롭게 사용해야 함
      • eg. 웹페이지를 구성하는 화면(HTML)은 JSP로 표현하고, 복잡한 플밍은 서블릿으로 구현

서블릿 작성 방법

  1. 서블릿 3.0 이상
    • web.xml 파일 사용 X
    • 자바 어노테이션 사용
  2. 서블릿 3.0 미만
    • 서블릿을 등록할 때 web.xml 파일에 등록

서블릿 3.0 이상

1부터 10까지 출력하는 TenServlet을 작성해보자.

http://localhost:8080/exam31/ten

@WebServlet("/ten")
public class TenServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
    /**
     * @see HttpServlet#HttpServlet()
     */
    public TenServlet() {
        super();
        // TODO Auto-generated constructor stub
    }

	/**
	 * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setContentType("text/html;charset=UTF-8");
		PrintWriter out = response.getWriter();
		for(int i = 0; i < 10; i++) {
			out.write("<h3>" + i + "</h3><br>");
		}
		out.close();
		response.getWriter().append("Served at: ").append(request.getContextPath());
	}
}

서블릿은 동적으로 응답 결과를 만들어낸다. 동적으로 만든다는 것은 이미 응답할 페이지를 갖고 있는게 아니라 요청이 들어왔을 때 이 프로그램이 실행되면서, 다시 말하자면 ten이라고 요청이 들어오면 이 TenServlet이 실행되면서 응답할 코드를 만들어낸 후 응답하는 것을 의미한다.

클라이언트가 요청을 보낼 때 서버는 요청을 받아내는 객체와 응답을 하기 위한 객체 두 개를 자동으로 생성한다. 요청에 대한 정보들을 모두 req 객체 안에 추상화시켜 갖고 있고, 응답할 정보들은 모두 resp 객체 안에 추상화시켜 갖고 있다.

서블릿 2.5 이상

서블릿은 요청이 들어왔을 때 반드시 서블릿 이름으로 요청하지는 않기 때문에 web.xml은 클라가 요청할 때 이런 url로 요청을 하게 되면 servlet-name이 같은 서블릿을 찾는다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  <display-name>exam25</display-name>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
  <servlet>
    <description></description>
    <display-name>TenServlet</display-name>
    <servlet-name>TenServlet</servlet-name>
    <servlet-class>exam.TenServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>TenServlet</servlet-name>
    <url-pattern>/ten</url-pattern>
  </servlet-mapping>
</web-app>

URL이 /ten이라고 요청이 들어오면 이 URL 매핑에서 찾고, 찾지 못하면 404 페이지를 보인다. 만약 존재한다면 servlet-name이라는 엘리먼트를 확인한다. 다시 말하면, servlet-name이라는 이름을 가지고 실제 servlet이라는 태그 안에서 똑같은 이름의 servlet-name을 찾는다. 실제로 내가 실행시킬 서블릿이 누군지 찾는 것이다.

서블릿의 Life Cycle

서블릿이 언제 생성이 되고 어떤 메소드들이 언제 어떻게 호출되는지 알아보자.

  1. 해당 URL로 클라이언트가 서버에게 요청
  2. 서버는 이 URL을 받아서 이 매핑은 LifecycleServlet이라는 정보를 알아내 해당 클래스가 멤에 존재하는지 체크
  3. 메모리에 존재하지 않으면 해당 객체를 생성해 메모리에 올려줘야 함

최초로 실행했을 때는 당연히 메모리에 없으니까 객체를 생성해 초기화하고 service() 메소드를 실행한다. 그리고 이 상태에서 새로고침하면 현재 메모리에 있으니까 service() 메소드만 호출된다.

서블릿은 서버에 서블릿 객체를 여러 개 만들지 않는다. 요청이 여러 번 들어오면 매번 생성하고 이런 일들을 반복하는게 아니라 실제 요청된 객체가 메모리에 있는지 없는지를 체크하기 때문에 메모리에 객체가 있는 경우는 service()만 호출한다. 그래서 한 번 호출된 이후에는 계속 service()만 호출된다.

그렇다면 destroy()는 언제 호출될까? 서블릿을 한 번 수정해보자. 서블릿이 수정되었기 때문에 현재 메모리에 올라와 있는 서블릿 객체는 더 이상 사용할 수 없게 된다. 이때 destroy()가 호출되고, 브라우저를 새로고침 해보면 다시 처음부터 객체를 생성해 init()을 호출한 다음 service()를 호출하는 것을 확인할 수 있다.

정리하자면,

  • WAS는 서블릿 요청을 받으면 해당 서블릿이 메모리에 있는지 확인
  • if(메모리에 없음) {
    • 해당 서블릿 클래스를 메모리에 올림
    • init() 메소드 실행
  • }
    • service() 메소드 실행: 요청이 들어왔을 때 응답해야되는 모든 내용은 여기에 구현해야 함
  • WAS가 종료되거나, 웹 애플리케이션이 새롭게 갱신될 경우 destroy() 메소드 실행

service(request, response) 메소드

WAS는 매번 service()만 호출한다. 그래서 만약 service()를 오버라이드 하지 않았다면 이 service()는 서블릿의 부모인 HttpServlet의 service() 메소드가 실행된다.

  • Http Servlet의 service 메소드는 템플릿 메소드 패턴으로 구현
    • 클라이언트의 요청이 GET인 경우에는 자신이 갖고 있는 doGet(req, res) 메소드 호출
    • 클라이언트의 요청이 POST인 경우에는 자신이 갖고 있는 doPost(req, res) 메소드 호출

아래와 같은 코드를 작성해보자.

@WebServlet("/LifecycleServlet")
public class LifecycleServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
    public LifecycleServlet() {
        super();
        System.out.println("Lifecycle servlet 생성");
    }

	public void init(ServletConfig config) throws ServletException {
        System.out.println("init 호출");
	}

	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setContentType("text/html");
		PrintWriter out = resp.getWriter();
		out.println("<html>");
		out.println("<head><title>form</title></head>");
		out.println("<body>");
		out.println("<form method='post' action='/servlet-test/LifecycleServlet'>");
		out.println("name : <input type='text' name='name'><br>");
		out.println("<input type='submit' value='ok'><br>");                                                 
		out.println("</form>");
		out.println("</body>");
		out.println("</html>");
		out.close();
		super.doGet(req, resp);
	}

	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		resp.setContentType("text/html");
		PrintWriter out = resp.getWriter();
		String name = req.getParameter("name");
		out.println("<h1> hello " + name + "</h1>");
		out.close();
//		super.doPost(req, resp);
	}

	public void destroy() {
        System.out.println("destroy 호출");
	}

//	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//        System.out.println("service 호출");
//	}
}

form 태그 안에는 method와 action이라는 값이 있는데, 이는 “submit이라는 버튼이 눌렸을 action이라는 주소로 주고, 요청이 들어갈 때 메소드는 post라는 값으로 넣어주세요”라는 의미이다. URL에서 직접 요청했을 때에는 메소드 값이 GET으로 넘어간다.

getParameter(“name”)은 “req객체로부터 parameter가 name으로 지정된 녀석을 꺼내 String name이라는 곳에 넣어줘요”라는 의미이다.

프로그램이 동작하면서 응답 결과를 만들어내기 때문에 응답 결과가 매번 바뀐다. 이런 것을 동적인 페이지라 한다.

HttpServletRequest와 HttpServletResponse 객체

웹브라우저에 URL을 입력하고 엔터를 누르면 웹 브라우저는는 도메인과 포트번호를 이용해 서버에 접속한다. 그리고 나서 path 정보, 클라이언트의 IP, 클라이언트의 다양한 정보를 포함한 요청 정보를 서버에게 전송한다.

  • WAS는 웹 브라우저로부터 Servlet 요청을 받으면
    • 요청할 때 가지고 있는 정보를 HttpServletRequest 객체를 생성하여 저장
    • 웹브라우저에게 응답을 보낼 때 사용하기 위하여 HttpServletResponse 객체 생성
    • 생성된 HttpServletRequest, HttpServletResponse 객체를 요청 정보에 있는 path로 매핑된 서블릿에게 전달
    • 이렇게 전달한 객체는 service(), doGet(), doPost() 같은 메소드에 parameter로 전달돼 사용

HttpServletRequest

  • HTTP 프로토콜의 request 정보를 서블릿에게 전달하기 위한 목적으로 사용
  • 헤더 정보, 파라미터, 쿠키, URI, URL 등의 정보를 읽어들이는 메소드들을 갖고 있음
  • body의 stream을 읽어들이는 메소드를 갖고 있음

HttpServletResponse

  • WAS는 어떤 클라이언트가 요청을 보냈는지 알고 있고, 해당 클라이언트에게 응답을 보내기 위한 HttpServletResponse 객체를 생성해 서블릿에게 전달
  • 서블릿은 해당 객체를 이용해 content type, 응답 코드, 응답 메시지 등을 전송

헤더 정보 읽어들이기

웹 브라우저가 요청정보에 담아서 보내는 header 값을 읽어들어 브라우저 화면에 출력해보자.

http://localhost:8080/servlet-test/header

@WebServlet("/header")
public class HeaderServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
    public HeaderServlet() {
        super();
    }

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setContentType("text/html");
		PrintWriter out = response.getWriter();
		out.println("<html>");
		out.println("<head><title>form</title></head>");
		out.println("<body>");
		
		Enumeration<String> headerNames = request.getHeaderNames();
		while(headerNames.hasMoreElements()) {
			String headerName = headerNames.nextElement();
			String headerValue = request.getHeader(headerName);
			out.println(headerName + ": " + headerValue + "<br>");
		}
		out.println("</body>");
		out.println("</html>");
		response.getWriter().append("Served at: ").append(request.getContextPath());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	}
}

getHeaderName()는 모든 헤더 이름을 String Enumeration 객체로 반환해준다.

파라미터 읽어들이기

URL 주소의 파라미터 정보를 읽어들여 브라우저 화면해 출력해보자.

http://localhost:8080/servlet-test/param?name=kim&age=5

@WebServlet("/param")
public class ParameterServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
    public ParameterServlet() {
        super();
    }

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		response.setContentType("text/html");
		PrintWriter out = response.getWriter();
		out.println("<html>");
		out.println("<head><title>form</title></head>");
		out.println("<body>");
		
		String name = request.getParameter("name");
		String age = request.getParameter("age");
		
		out.println("name: " + name + "<br>");
		out.println("age: " + age + "<br>");
		
		out.println("</body>");
		out.println("</html>");
		response.getWriter().append("Served at: ").append(request.getContextPath());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	}
}

실행시킨 직후에는 파라미터 값을 안 줬으므로 name과 age 모두 null 값을 가진다. 위의 http://localhost:8080/servlet-test/param?name=kim&age=5 처럼 파라키트 이름과 파라미터 값을 줘서 주소를 입력해야 함. 파라미터들은 ‘?’으로 구분한다.

그 외의 요청정보 출력

URI, URL, PATH, Remote host 등에 대한 정보를 출력해보자. http://localhost:8080/servlet-test/info

@WebServlet("/info")
public class InfoServlet extends HttpServlet {
	private static final long serialVersionUID = 1L;
       
    public InfoServlet() {
        super();
    }

	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {		// TODO Auto-generated method stub
		response.setContentType("text/html");
		PrintWriter out = response.getWriter();
		out.println("<html>");
		out.println("<head><title>form</title></head>");
		out.println("<body>");
		
		String uri = request.getRequestURI();
		StringBuffer url = request.getRequestURL();
		String contextPath = request.getContextPath();
		String remoteAddr = request.getRemoteAddr();
		
		out.println("uri: " + uri + "<br>");
		out.println("url: " + url + "<br>");
		out.println("contextPath: " + contextPath + "<br>");
		out.println("remoteAddr: " + remoteAddr + "<br>");
		response.getWriter().append("Served at: ").append(request.getContextPath());
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doGet(request, response);
	}
}

코드를 작성하면 다음과 같은 값을 얻는다.

uri: /servlet-test/info
url: http://localhost:8080/servlet-test/info
contextPath: /servlet-test
remoteAddr: 0:0:0:0:0:0:0:1
  • URI: 도메인과 포트 이하에 있는 값. 요청한 URL에서 포트 번호 이하
  • URL: 요청 주소 전체
  • contextPath: 웹 애플리케이션과 매핑된 path. WAS 내에 웹 애플리케이션이 여러 개 있을 수 있는데, 그때 웹 애플리케이션을 찾아가는 이름을 의미한다.
  • remoteAddr: 출력해주는 클라이언트의 주솟값. 로컬에서 접속해서 저럼.