[카테캠 BE] week6 - 스프링 MVC, Servlet 등
6주차는 스프링 MVC 패턴을 비롯한 심화를 배웠습니다. 저번 시간에 의문이 들었던 Servlet도 배우고 JSP(Java Server Page)를 다루는 법도 상세히 배웠어요.
MVC 패턴
관심사(concern) 분리
OOP 5대 설계 원칙(SOLID)의 SRP(Single Responsibility Principle)에 의해 한 메서드는 하나의 기능만 수행하도록 분리해야한다.
- 관심사(concern)
- 변하는 것 / 변하지 않는 것
- 공통(중복) 코드
을 분리해야한다.
MVC 패턴
- 요청이 들어오면
Dispatcher Servlet
이 입력처리 수행Dispatcher Servelt
의 역할- 입력과 변환을 담당
- 모델 생성 수행
Controller
에게model
를 넘겨주며 처리를 맡김- 모델은 key-value 형식
- 결과가 담긴 모델을 넘겨받아 View로 전송하여 출력
- 반환할 때 결과를 보여줄 View의 이름(.jsp 파일의 이름)을 반환
- 메서드 반환타입 void로 별도의 View 이름 반환하지 않을 시
- @RequestMapping된 url이름의 jsp파일을 탐색함
- view파일은 src/main/webapp/WEB-INF/views 폴더 안에 .jsp파일로 작성
- WEB-INF/spring/appServlet/servlet-context.xml 내부에 위치 매핑되어있음
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <beans:property name="prefix" value="/WEB-INF/views/" /> <beans:property name="suffix" value=".jsp" /> </beans:bean>
- 반환할 때 결과를 보여줄 View의 이름(.jsp 파일의 이름)을 반환
예시
@RequestMapping("/getYoilMVC")
public String main(int year, int month, int day, Model model) {
// Dispatcher Servlet이 입력 처리와 변환 및 모델 생성을 수행
// 1. 유효성 검사
if(!isValid(year, month, day))
return "yoilError"; // 유효하지 않으면, /WEB-INF/views/yoilError.jsp로 이동
// 2. 처리
har yoil = getYoil(year, month, day);
// 3. Model에 작업 결과 저장
model.addAttribute("year", year);
model.addAttribute("month", month);
model.addAttribute("day", day);
model.addAttribute("yoil", yoil);
// 4. 작업 결과를 보여줄 View의 이름을 반환
return "yoil"; // /WEB-INF/views/yoil.jsp
}
- return 타입을
ModelAndView
로 하여 매개변수로 Model을 받지 않고 직접 생성하여 처리할 수 도 있다.ModelAndView mv = new ModelAndView(); mv.addObject("year", year); mv.setViewName("yoil"); return mv;
서블릿과 JSP
서블릿을 발전시켜 spring이 만들어 졌다.
@WebServlet("/rollDice2")
public class TwoDiceServlet extends HttpServlet {
int getRandomInt(int range) {
return new Random().nextInt(range)+1;
}
public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
int idx1 = getRandomInt(6);
int idx2 = getRandomInt(6);
response.setContentType("text/html");
response.setCharacterEncoding("utf-8");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("</head>");
out.println("<body>");
out.println("<img src='resources/img/dice"+idx1+".jpg'>");
out.println("<img src='resources/img/dice"+idx2+".jpg'>");
out.println("</body>");
out.println("</html>");
out.close();
}
}
@WebServlet
은@Controller
+@RequestMapping
을 합친 것과 같다.- 클래스마다 매핑을해야하므로 클래스를 많이 만들어야한다.
- 스프링은
@RequestMapping
을 Controller 내부에 여러 메서드가 가능하므로 효율적이다.
extends HttpServlet
이 필요하다.- 메서드는
service
메서드를override
하는 것이 필수적이다.
서블릿의 생명주기
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
// 서블릿의 초기화될 떄 자동 호출되는 메서드
// 1. 서블릿의 초기화 작업 담당
System.out.println("[HelloServlet] init() is called.");
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 입력
// 2. 처리
// 3. 출력
System.out.println("[HelloSerlvet] service()");
}
@Override
public void destroy() {
// 뒷정리 작업 - 서블릿이 제거(unload)될 때, 단 한번만 수행
System.out.println("[HelloSerlvet] destroy()");
}
}
- init → service → destroy
- 매핑 주소로 처음 요청이 왔을 때 init 실행 후 작업을 하며 service 실행
- 이후 접속 시 생성 돼 있기 때문에 service만 계속 실행
- 서블릿이 완전히 사라질때 destroy
- 예를 들어 클래스 파일을 수정했다면 destroy되고 이후 접속 시 init으로 재 생성
서블릿은 Singleton으로 1개의 인스턴스만 있고 재활용을 한다.
JSP(Java Server Page)
jsp파일은 내부에 클래스 영역과 메서드 영역을 가지고 나중에 JspServlet이 이를 java파일로 변환시켜 서비스를 제공하게 된다.
- jsp는 서블릿과 거의 흡사하다!
<%! %>
로 감싸진 영역은 클래스 영역이 된다.<% %>
로 감싸진 영역은 메서드 영역이 된다.- 변수 사용 -
<%=변수%>
<%@ page contentType="text/html;charset=utf-8"%>
<%@ page import="java.util.Random" %>
<%-- <%! 클래스 영역 %> --%>
<%!
int getRandomInt(int range){
return new Random().nextInt(range)+1;
}
%>
<%-- <% 메서드 영역 - service()의 내부 %> --%>
<%
int idx1 = getRandomInt(6);
int idx2 = getRandomInt(6);
%>
<html>
<head>
<title>twoDice.jsp</title>
</head>
<body>
<img src='resources/img/dice<%=idx1%>.jpg'>
<img src='resources/img/dice<%=idx2%>.jpg'>
</body>
</html>
src/main/webapp
폴더에.jsp
파일을 위치시킨다.- JSP의 호출과정
- url로
.jsp
의 요청이 오면JspServlet
이 확인 - 서블릿 인스턴스가 존재하지 않으면 해당 url 요청의 파일이 존재하는지 확인한다.
localhost/ch2/twoDice.jsp
로 요청이 왔으면twoDice.jsp
파일이 존재하는지 확인
twoDice_jsp.java
파일로 변환한다.twoDice_jsp.class
파일로 컴파일한다.- 인스턴스를 생성한다.(
_jspInit()
) - 서비스를 제공한다.(
_jspService()
)
- url로
변환과 init 때문에 첫번째 호출에서는 지연 발생
서블릿: lazy - init
spring: early - init( 미리 인스턴스를 생성해놓고 제공함)
JSP의 기본 객체
생성 없이 사용할 수 있는 객체
- request, response, out, session, page, pageContext, application, config, exception 등
- 변환 시 _jspService 메서드의 로컬 변수로 맨 위에 자동 생성됨
- 따라서 별도의 선언/생성 없이 바로 사용 가능
정보 저장소와 유효범위
- pageContext
- 한 JSP 페이지 내부에서만 사용가능
- EL(Expression Language) - ${}
- 정보 처리를 하며 사용 할 뿐 보관 하기에는 나쁜 선택이다.
- application
- Web Application이 가진 정보
- context 어디에서나 접근 가능
- 모든 클라이언트가 공유한다.
- session
- 클라이언트마다 존재
- 로그인 정보 같은 것
- 클라이언트의 쿠키와 매칭하여 사용한다.
- 메모리 부담이 높다(사용 최소화 해야함)
- request
- forward를 통해 다른 JSP로도 전달 가능
- HTTP 요청마다 하나의 request를 만들어 전달 가능하다.
URL 패턴
@WebServlet("/hello")
와 같이 서블릿에서 URL 매핑하는 방식
- 우선순위
- exact mapping > path mapping > extension mapping > defualt mapping
- /login/hello.do > /login/* > *.do > /
**servletMappings 테이블
과children(서블릿) 테이블
이 존재**- url 패턴으로 servletMappings에서 매칭되는 서블릿을 확인
- 서블릿 테이블을 바탕으로 서블릿을 실행시킨다.
- 클래스로 만든 서블릿은 매핑을 직접 시킴
- *.jsp의 경우 JspServlet과 매핑되어 있다.
- 그외는 / → DefaultServlet으로 매핑되어 있다.
- spring의 경우 default / 가 DispatcherServlet으로 매핑
- 모든 @RequestMapping이 DispatcherServlet으로 이루어지고 DispatcherServlet 내부에서 각 Request가 매핑되어진다.
JSTL(JSP Standard Tag Library)
- 상단에 taglib 추가 필요
<%@ taglib prefix=”c” uri=”http://java.sun.com/jsp/jstl/core”%>
<%@ taglib prefix=”fmt” uri=”http://java.sun.com/jsp/jstl/fmt”%>
- if문/for문 사용가능
<%@ page contentType="text/html;charset=utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
<title>JSTL</title>
</head>
<body>
<c:set var="to" value="10"/> to에 10 저장
<c:set var="arr" value="10,20,30,40,50,60,70"/> arr에 배열 저장
<c:forEach var="i" begin="1" end="${to}"> 1부터 10까지 반복문
${i}
</c:forEach>
<br>
<c:if test="${not empty arr}"> 조건문 시작
<c:forEach var="elem" items="${arr}" varStatus="status"> 배열로 반복, status는 count와 index를 가짐
${status.count}. arr[${status.index}]=${elem}<BR> count는 몇 번째(1부터 시작), index는 배열의 index(0부터 시작)
</c:forEach>
</c:if> 조건문 종료
<c:if test="${param.msg != null}"> param은 url뒤의(?msg="") query 파라미터
msg=${param.msg}
msg=<c:out value="${param.msg}"/> 위와 같지만, out으로 감싸 그대로 출력해줌 <pre>
</c:if>
<br>
<c:if test="${param.msg == null}">메시지가 없습니다.<br></c:if>
<c:set var="age" value="${param.age}"/>
<c:choose> if else if 문
<c:when test="${age >= 19}">성인입니다.</c:when>
<c:when test="${0 <= age && age < 19}">성인이 아닙니다.</c:when>
<c:otherwise>값이 유효하지 않습니다.</c:otherwise>
</c:choose> if else if 문 종료
<br>
<c:set var="now" value="<%=new java.util.Date() %>"/>
Server time is <fmt:formatDate value="${now}" type="both" pattern="yyyy/MM/dd HH:mm:ss"/>
</body>
Filter
로깅, 인코딩 등의 중복코드 처리를 위해 사용
- 전처리 → 서블릿 호출 → 후처리
- 각 서블릿은 각자 처리만 하면 된다.
// 필터를 적용할 요청의 패턴 지정 - 모든 요청에 필터를 적용.
@WebFilter(urlPatterns="/*")
public class PerformanceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 1. 전처리 작업
long startTime = System.currentTimeMillis();
// 2. 서블릿 또는 다음 필터를 호출
chain.doFilter(request, response);
// 3. 후처리 작업
System.out.print("["+((HttpServletRequest)request).getRequestURI()+"]");
System.out.println(" 소요시간="+(System.currentTimeMillis()-startTime)+"ms");
}
}
- 전처리와 후처리 작업을 작성해주면 된다.
@RequestParam
요청의 파라미터를 연결할 매개변수에 붙이는 애너테이션
// 아래와 동일
//public String main2(@RequestParam(name="year", required=false) String year) {
public String main2(String year) {
- 기본형과 String일 때 생략 가능
- 참조형일 때는 애초에 못 붙인다.
- 1대1 매칭이 되어야하는데 여러개 e.g. Date(년, 월, 일)에 매칭 시킬 수 없다.
- name: 파라미터 이름
- required: 파라미터 필수 여부
- 파라미터가 없이 요청이 왔으면
null
이 할당됨
// 아래와 동일
//public String main3(@RequestParam(name="year", required=true) String year) {
public String main3(@RequestParam String year) {
- 필수 입력일 시, 간단하게 변수 앞에
@RequestParam
을 붙임으로써 생략 가능 - 필수인데 없으면 404 에러가 나온다.
예외 처리
ExceptionHandler 에노테이션을 사용하여 에러에 대한 처리를 할 수 있다.
// 404예외처리 핸들러
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handle404(NoHandlerFoundException e){
return "error/notFound";
}
// 데이터베이스오류
@ExceptionHandler(DataAccessException.class)
public String handleDataAccessException(DataAccessException e) {
e.printStackTrace();
return "error/databaseError";
}
// 500에러처리
@ExceptionHandler(Exception.class)
public String handleException(Exception e) {
e.printStackTrace();
return "error/serverError";
}
@ModelAttribute
- 매개변수 앞
// 아래와 동일 // public String main(@ModelAttribute("myDate") MyDate date, Model m) { public String main(@ModelAttribute MyDate date, Model m) { // @ModelAttribute사용, 반환 타입은 String
addAtrribute
를 하지 않아도 자동으로 모델에 추가해준다.- 괄호 안의 key 이름 생략 시 타입명의 첫글자 대문자를 소문자로 바꿔
myDate
로 저장 - 참조형 매개변수에서는 생략이 가능하다!
- 그냥
MyDate date
로하면@ModelAttribute MyDate date
한 결과로 된다. - 기본형과
String
은${param.파라미터명}
으로 바로 접근 가능하므로 의미가 없다.
- 그냥
- 반환 타입 앞
private @ModelAttribute("yoil") char getYoil(MyDate date) { return getYoil(date.getYear(), date.getMonth(), date.getDay()); }
- 호출된 메서드의 결과를 모델에 자동으로 저장
WebDataBinder
타입변환과 데이터 검증을 수행한다.
String
으로 받은 데이터를타입 변환
- 데이터의 세부적인 검증 (e.g. day은 31 이하 여야 한다.)
- 각각의 결과와 에러를
BindingResult
에 보관 - 컨트롤러의 메서드에 매개변수로
BindingReuslt
를 받을 수 있다.- (주의)바인딩할 객체 바로 뒤에 와야 한다.
@GetMapping과 @PostMapping
어떤 request method인지에 따라 메서드를 별개로 설정이 가능하다.
@RequestMapping(value = "/", method = RequestMethod.GET)
@RequestMapping(value = "/", method = RequestMethod.POST)
이런 식으로도 사용 가능하다.
@RequestMapping(value = "/", method = {RequestMethod.GET, RequestMethod.POST})
RequestMepping을 간단히 축약하기 위해 다음과 같은 애노테이션을 지원한다.
@GetMapping("/")
@PostMapping("/")
@DeleteMapping("/")
@PutMapping("/")
view-controller
@GetMapping("/register/add")
public String register() {
return "registerForm";
}
위와 같이 단순히 view를 보여주기만 할 뿐인 코드는 controller에 메서드를 적지 않고
src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml 에다가 아래의 코드를 추가하여 메서드를 만들지 않아도 되게 된다.
<view-controller path="/register/add" view-name="registerForm"/>
- 최상위 beans 태그 내에 위치 시켜야 한다.
- 원래는 mvc:view-controller인데 beans에서 mvc는 안 붙여도 되게 해놓았다.
GET
요청만 허용
RequestMapping의 URL 패턴
- 우선순위
- exact mapping > path mapping > extension mapping
- /login/hello.do > /login/* > *.do
- 서블릿과 거의 동일하다.
- 와일드카드
- ?는 한 글자, *는 여러 글자, **는 하위 경로까지 포함
URL 인코딩 - 퍼센트 인코딩
URL에 포함된 non-ASCII 문자를 문자코드(16진수) 문자열로 변환
- 한글이 url에 포함되면 메모장에 복사 시, %EB 등으로 변환되어 진다.
- 인코딩한 것을 디코딩하여 사용한다.
- 인코딩 예시
if(!isValid(user)) { String msg = URLEncoder.encode("id를 잘못입력하셨습니다.", "utf-8"); m.addAttribute("msg", msg); return "redirect:/register/add"; // 신규회원 가입화면으로 이동(redirect) }
- 디코딩 예시
${URLDecoder.decode(param.msg)}
- 사용하는 곳(예시에서는 jsp파일)에서 디코딩을 해 원문을 사용가능하다.
- 인코딩 예시
Redirect와 Forward
Redirect
- http 상태코드 300번대는 redirect
- 응답 헤더만 존재, Body 존재 X
- 헤더의 location을 바탕으로 브라우저가 자동으로 url에 GET 요청을 보냄
- 요청 후 응답 바탕으로 다시 요청 및 응답(요청 2, 응답 2)
if(!isValid(user)) {
String msg = URLEncoder.encode("id를 잘못입력하셨습니다.", "utf-8");
m.addAttribute("msg", msg);
return "redirect:/register/add"; // 신규회원 가입화면으로 이동(redirect)
}
post /register/save
에서 valid하지 않아 다시register/add
로redirect
- 재 요청이 일어나
register/add
로 url에 나온다.
Forward
- 클라이언트에서 받은 요청을 다른 곳으로 넘겨버리고 그곳에서 응답
- 요청 2, 응답 1
- 클라이언트는 단순히 자신이 요청 한 곳만 인지
- spring의 MVC 패턴
- Controller가 요청을 받고
- Model에 담아서(2번째 forward request)
- View가 클라이언트에게 응답
if(!isValid(user)) {
String msg = URLEncoder.encode("id를 잘못입력하셨습니다.", "utf-8");
m.addAttribute("msg", msg);
return "forward:/register/add"; // 신규회원 가입화면으로 이동(redirect)
}
- 재 요청이 일어나 register/add로 url에 나온다.
post /register/save
에서 valid하지 않아register/add
로forward
- 요청 후
forward
로 변경한 것이 때문에, url은register/add
가 아닌register/save
로 나온다.
✏️여담
이번엔 내용을 다양하게 배웠어요. 이제부턴 생각보다 모르는 내용도 있고 외울부분도 존재하네요. 다음주도 열심히 배워야겠어요!
댓글남기기