웹 프로그래밍의 시작(2) - Web MVC 방식, HttpServlet, 모델(Model)

MVC 구조와 서블릿/JSP

서블릿 코드의 경우 자바 코드를 그대로 이용할 수 있고, 상속이나 인터페이스의 처리도 가능하지만,

HTTP로 전달된 메시지를 구성하는 HTML을 처리할 때는 상당한 양의 코드를 작성해야 한다.

JSP의 경우 반대로 HTML 코드를 바로 사용할 수 있으므로 HTTP 메시지 작성에는 적합하지만,

그 안에서 자바 코드를 재사용하는 문제나 자바 코드와 HTML이 호재하는 것과 같은 여러 문제가 존재한다.

 

이를 절충해서 다음과 같은 구조를 이용하게 된다.

(1) 브라우저의 요청은 해당 주소를 처리하는 서블릿에 전달되고 서블릿 내부에서는 응답에 필요한 재료 데이터를 준비한다. 

(2) 서블릿은 준비한 데이터를 JSP로 전달하고 JSP에서는 EL을 이용해서 최종적인 결과 데이터를 생성한다. 

(3) JSP를 이용해서 생성된 결과 화면은 톰캣을 통해서 브라우저로 전송된다. 

Request -> 서블릿 -> JSP -> Response

 

웹 MVC 구조

- 'Model - View - Controller'의 역할을 분리해서 처리하는 구조

- 데이터는 컨트롤러(Controller - 서블릿)에서 결과는 뷰(View - JSP)에서 처리한다. 

- 컨트롤러 역할을 하는 서블릿은 JSP에 필요한 데이터를 가공하는 역할을 하는데 이 때 필요한 데이터를 제공하는 객체를 모델(Model)이라고 한다. 

 

Request  -> Controller(서블릿) <- Model(데이터를 제공하는 객체)

                               |

Response  <-   View(뷰)

 

MVC 구조에서 지켜야할 원칙

- 브라우저의 호출은 반드시 컨트롤러 역할을 하는 서블릿을 호출하도록 구성한다.

- JSP는 브라우저에서 직접 호출하지 않고 Controller를 통해서만 JSP에 접근하도록 구성한다.


이제는 MVC 구조로 계산기를 만들어보자.

 

GET 입력 화면 설계

브라우저의 호출은 '/cal/input'이고 컨트롤러는 InputController, 뷰는 input.jsp

1. 브라우저는 /calc/input 주소를 호출

2. 주소에 맞는 서블릿을 InputController로 작성하고, GET 방식일때만 동작하도록 한다.

3. InputController의 화면 처리는 input.jsp를 이용하도록 지정한다.

4. input.jsp에는 HTML 코드를 이용해서 브라우저에서 볼 수 있는 결과를 생성한다. 

InputController.java

InputController는 @WebServlet으로 urlPatterns 속성을 지정해서 처리해야하는 경로를 지정한다. 

부모 클래스인 HttpServlet의 doGet()을 재정의(오버라이드)하고 GET 방식으로 들어오는 요청(Request)에 대해서만 처리하도록 구성한다.

 

RequestDispatcher를 이용한 요쳥(Request) 배포

위 코드에서 RequestDispatcher를 이용한 forward()를 실행하는 부분이 핵심 코드이다.

RequestDispatcher는 서블릿에 전달된 요청을 다른 쪽으로 전달 혹은 배포하는 역할을 하는 객체이다.

 

Broswer ('/calc/input' 경로 호출) -> InputController (RequestDispatcher를 통해 내부적으로 input.jsp에게 배포) -> /WEB-INF/calc/input.jsp(실제 결과를 만드는 View) -> Broswer(뷰에서 만들어진 결과물 확인)

 


WEB-INF는 브라우저에서 직접 접근이 불가능한 경로 특별한 경로이다. WEB-INF 밑에 jsp 파일을 둔다는 의미는 브라우저에서 .jsp로 직접 호출이 불가능하다는 것을 의미한다. 

따라서 WEB-INF에 있는 jsp 파일들은 서블릿이 실행된 결과일 때 볼 수 있다.



POST 처리의 설계

1. input.jsp의 <form>태그의 action을 '/calcResult'와 같이 변경하고 이에 해당하는 calcResultServlet 서블릿을 컨트롤러로 작성한다.

2.  CalcResultServlet은 <form>으로 전달된 데이터를 읽어내서 결과 데이터를 만들어내야 한다.

3. 만들어진 결과를JSP로 전달해야 하고 JSP에서는 결과 데이터를 출력한다.

 

CalcController 생성

- urlPatterns 속성값이 <form> 태그 action과 당연히 동일

- doPost() 메소드를 오버라이드해서 POST 방식으로 들어오는 요청을 처리

- req.getParameter() 메소드를 이용해서 쿼리 스트링으로 전달된 num1, num2 파라미터를 처리하고, 이때 숫자가 아닌 문자열(String)로 처리하고 있다. JSP에서는 ${param.num1}과 같이 단순하게 사용가능하지만, 서블릿에서는 HttpServletRequest라는 API를 이용해야만 한다. 

CalcController.java
input.jsp

sendRedirect()

sendRedirect()

POST 방식의 처리는 가능하면 빨리 다른 페이지를 보도록 브라우저 화면을 이동시키는 것이 좋다. 

보여줄 결과를 만들지 않아 브라우저에 아무런 변화가 없는데, 그렇기 때문에 브라우저에서 [SEND]버튼을 계속 클릭할 수 있게 되고 브라우저에서는 위의 사진처럼 경고메시지를 보여주는 방식으로 다시 POST 방식의 호출을 시도할 것인지 물어보게 된다.

 

POST 방식으로 처리하고 JSP를 이용해서 결과를 보여주는 방식을 이용할 때도 브라우저 창에서 앞선 방법과 같이 다시 호출할 수 있기 때문에 처리가 끝난 후에는 다른 경로로 이동하게 하는 것이 일반적이다. 이 때 사용하는 메소드가 HttpServletResponse의 sendRedirect()이다.

 

resp.sendRedirect("/index");

해당 코드로 인해 응답 헤더에 'Location'이 포함된다.

location

브라우저는 응답 헤더에 'Location"이 포함되면 브라우저의 주소창을 변경하고 해당 주소를 호출하게 된다.(GET 방식으로 호출)

 


PRG 패턴 (Post-Redirect-GET)

- 웹 MVC 구조에서 흔히 사용하는 패턴은 앞서 언급했던 POST방식과 Redirect를 결합해서 사용하는 PRG 패턴이다.

- POST 방식의 처리 후에 바로 다른 주소로 브라우저가 이동하기 때문에 반복적으로 POST 호출이 되는 상황을 막을 수 있고, 사용자의 입장에서도 처리가 끝나고 다시 처음 단계로 돌아간다는 느낌을 주게 된다. 

 

 

PRG패턴의 흐름

- 사용자는 컨트롤러에 원하는 작업을 POST 방식으로 처리하기를 요청

- POST 방식을 컨트롤러에서 처리하고 브라우저는 다른 경로로 이동(GET)하라는 응답(Redirect)

- 브라우저는 GET 방식으로 이동

RPG 패턴

 

 

예시 - 게시판

- 사용자가 새로운 게시글의 내용을 작성하고 POST 방식으로 전송

- 서버에서 새로운 게시글을 처리한 후에 브라우저의 주소를 목록 화면 경로로 이동하도록 응답(Redirect)

- 브라우저는 목록 화면을 보여주고 사용자는 자신이 추가한 게시글이 추가된 결과를 확인

입력 화면까지 흐름에 포함한 그림


HttpServlet의 특징

- HttpServlet은 GET/POST 등에 맞게 doGet(), doPost() 등을 제공하므로, 개발자들은 필요한 메소드를 오버라이드하는 것만으로 GET/POST 방식 처리를 나눠서 처리할 수 있다.

- HttpServlet을 상속받은 클래스 객체는 톰캣과 같은 WAS의 내부에서 자동으로 객체를 생성하고 관리하기 때문에 개발자가 객체 관리를 신경쓸 필요가 없다.

- HttpServlet은 멀티 스레드에 의해서 동시에 실행될 수 있도록 처리되기 때문에 개발자는 동시에 많은 사용자들을 어떻게 처리해야 하는지에 대한 고민을 줄일 수 있다. 

 

Servlet   Serializable   ServletConfig

          |            |             |

            GenericServlet

                       |

              HttpServlet

                       |

        User defined Servlet

 

HttpServlet은 상위 클래스로 GenericServlect이라는 추상 클래스를 상속한다. GenericServlet과 HttpServlet의 가장 큰 차이는 GenericServlect의 경우 HTTP 프로토콜에 특화되지 않은 요청(Request)과 응답(Response)에 대한 기능을 정의하고 있다는 점이다.


GenericServlet은 HTTP가 아닌 요청과 응답을 의미하는 ServletRequest/ServletResponse라는 타입을 이용한다.

일반적으로 개발자가 작성하는 Servlet은 GenericServlet을 상속한 HttpServlet을 상속받아서 처리하는데 말 그대로 HTTP 프로토콜에 특화된 기능을 처리하기 위한 용도로 사용한다.

 


HttpServlet의 라이프사이클

서블릿은 기본적으로 요청을 처리해서 응답을 목적으로 설계됐다.

다만, 웹이라는 특수한 환경으로 인해서 개발자가 직접 객체르르 생성하는 대신에 톰캣에서 객체들을 관리한다. 

(때문에 서블릿 관점에서 톰캣은 서블릿 컨테이너라는 의미로 바라볼 수 있다.)

개발자가 작성하는 서블릿 클래스는 다음과 같은 과정을 통해서 처리된다.

1) 브라우저가 톰캣에서 서블릿이 처리해야 하는 특정한 경로를 호출한다.

2) 톰캣은 해당 경로에 맞는 서블릿 클래스를 로딩하고 객체를 생성한다. 이 과정에서 init() 메소드를 실행해서 서블릿 객체가 동작하기 전에수행해야 하는 일들을 처리할 수 있다. (최초 필요한 시점에 서블릿 클래스를 로딩하는 대신에 톰캣 실행 시에 로딩하도록 하는 load-on-startup이라는옵션도 있다.)

3) 생성된 서블릿 객체는 브라우저의 요청에 대한 정보를 분석해서 GET/POST 등의 정보와 함께 같이 전달되는 파라미터(쿼리 스트링의 내용)들을 HttpServletRequest라는 타입의 파라미터로 전달받는다. 이 과정에서 응답을 처리하는데 필요한 기능들은 HttpServletResponse라는 타입의 객체로 전달받는다.  

4) 서블릿 내부에서는 GET/POST에 맞게 doGet()/doPost() 등의 메소드를 실행한다. 이 후 동일한 주소의 호출이 있을 때 서블릿은 동일한 객체 하나만을 이용해서 이를 처리한다.

5) 톰캣이 종료될 때는 서블릿의 destory() 메소드가 실행된다.

 

앞의 과정에서 중요한 점은

1. 서블릿의 객체는 경로에 맞게 하나만 만들어진다는 점

2. 매번 호출 시에는 자동으로 doGet()/doPost()를 이용해서 처리된다는 점

이다.

 

SampleServlet
init() & destory()

브라우저를 통해 '/sample'을 호출하면 init()의 메소와 doGet()이 실행된다.

 '/sample'을 여러 번 호출하면 'init...' 부분은 더 출력되지 않고 'doGet...'부분만 출력된다.

여기서 this의 결과로 출력되는 '@'이하의 값이 모두 같다. 

즉, 동일하게 하나의 객체로 처리된다는 것을 의미한다.

마지막으로 톰캣 종료하면 destroy()가 호출된다.

 

정리하면 init()와 destroy()는 한 번씩만 호출되고 doGet()/doPost()는 동일한 객체를 이용해서 여러 번 호출된다. 


HttpServletRequest의 주요 기능

(주로 읽기 기능 제공)

서블릿 객체에서 최종적으로 요청을 처리하는 doGet()/doPost() 등은 HttpServletRequest와 HttpServletResponse를 파라미터로 전달받는다.

HttpServletRequest는 HTTP 메시지 형태로 들어오는 요청에 대한 정보를 파악하기 위해서 여러 기능을 제공한다.

 

getParameter()

- HttpServletRequest에서 가장 많이 사용되는 메소드로 '?name=AAA&age=20'과 같은 쿼리 스트링에서 'name'이나 'age'라는 키(key)를 이용해서 값(value)을 얻는 역할을 위해서 사용한다.

- 주의할 점은 getParameter()의 결과는 항상 String이라는 점이다. 만약 해당 파라미터가 존재하지 않는다면 null을 반환할 수 있다.(따라서 항상 null 체크를 주의해야 한다.)

- 또한 문자열로 반환되기 때문에 숫자를 처리할 때는 예외가 발생할 수 있으므로 이 또한 주의해야한다. 

 

getParameterValues()

- getParameter()와 유사하며 동일한 이름의 파라미터가 여러 개 잇는 경우에 사용한다.

- 예를 들어 name이라는 이름의 파라미터가 여러 개 존재한다면 getParameterValues()를 이용해서 String[ ] 타입으로 반환된다. 

 

setAttribute()

- JSP로 전달할 데이터를 추가할 때 사용한다.

- '키(key)'와 '값(value)'의 형태로 데이터를 저장하며, 이 때 키(key)는 문자열로 지정하고, 값(value)은 모든 객체 타입을 이용할 수 있다. 

- JSP에는 서블릿에서 setAttribute()로 전달된 데이터를 화면에 출력하게 된다.

 

RequestDispatcher()

- 웹 MVC 구조에서 HttpServletRequest의 getRequestDispatcher()를 이용해서 RequestDispatcher 타입의 객체를 구할 수 있다.

- RequestDispatcher는 현재의 요청(Request)을 다른 서버의 자원(서블릿이나 JSP)에게 전달하는 용도로 사용한다.

- forward()와 include() 메소드가 존재하며 실제 개발에서는 거의 forward()만을 이용한다. 

 

forward() - 현재까지의 모든 응답(Response) 내용은 무시하고 JSP가 작성하는 내용만 브라우저로 전달

include() - 지금까지 만들어진 응답(Response) 내용 + JSP가 만든 내용을 브라우저로 전달

 


HttpServletResponse의 주요 기능

(주로 쓰기 기능 제공)

웹 MVC 구조에서 HttpServletResponse는 JSP에서 주로 처리되기 때문에 서블릿 내에서 직접 사용되는 일은 많지 않고 주로 sendRedirect()를 이용하는 경우가 많다.

 

sendRedirect()

sendRedirect()

- 웹 MVC 구조에서 HttpServletResponse 중에서 가장 많이 사용되는 메소드

- 브라우저에게 '다른 곳으로 가버려!'라는 응답(Response) 메시지를 전달한다. - 'Location 헤더'

- HTTP에서 'Location' 이름의 HTTP 헤더로 전달되는데 브라우저는 'Location'이 있는 응답을 받으면 화면을 처리하는 대신에 주소창에 지정된 주소로 이동하고, 다시 호출하게 된다. 

- sendRedirect()를 사용하면 브라우저의 주소가 아예 변경되기 때문에 사용자의 '새로고침'과 같은 요청을 미리 방지할 수 있고, 특정한 작업이 완전히 끝나고 새로 시작하는 흐름을 만들 수 있다. 

 

 

 

 

 


참고자료 - 자바 웹 개발 워크북(2022) | 프리렉

https://freelec.co.kr/book/%EC%9E%90%EB%B0%94-%EC%9B%B9-%EA%B0%9C%EB%B0%9C-%EC%9B%8C%ED%81%AC%EB%B6%812022/