티스토리 뷰
1) Spring MVC에서 Session사용하기
@SessionAttributes & @ModelAttribute
- @SessionAttributes 파라미터로 지정된 이름과 같은 이름이 @ModelAttribute에 지정되어 있을 경우 메소드가 반환되는 값은 세션에 저장됩니다.
- Controller 위쪽에다가 @SessionAttributes를 적고 메서드 위에 ModelAttribute를 적었는데 인자로 전달할 이름이 같은 경우에 메서드가 return 한 값은 argument의 이름을 key로 하여 세션에 저장되게 됩니다.
- 아래 예제에서는 setUpUserForm() 이라는 메서드에서 return 하고 있는 User 객체가 이 user라는 이름으로 세션에 저장됩니다.
@SessionAttributes("user") public class LoginController { @ModelAttribute("user") public User setUpUserForm() { return new User(); } } |
- @SessionAttributes의 파라미터와 같은 이름이 @ModelAttribute에 있을 경우 세션에 있는 객체를 가져온 후, 클라이언트로 전송받은 값을 설정합니다.
- Controller 위에 SessionAttributes 어노테이션이 사용되고 메서드의 인자로 ModelAttribute를 적었는데 SessionAttributes와 ModelAttribute 각각의 파라미터 이름이 같을 경우에는
먼저 세션에서 인자로 전달된 이름으로 저장된 객체를 찾고
해당 객체에 요청으로부터 넘어온 값을 설정해서 메서드의 인자로 전달하게 됩니다.
@Controller @SessionAttributes("user") public class LoginController { ...... @PostMapping("/dologin") public String doLogin(@ModelAttribute("user") User user, Model model) { ...... } } |
@SessionAttribute
- 메소드에 @SessionAttribute가 있을 경우 파라미터로 지정된 이름으로 등록된 세션 정보를 읽어와서 변수에 할당합니다.
- 메서드의 인자로 사용된 SessionAttribute 어노테이션은 어노테이션의 인자로 전달된 이 이름에 해당하는 정보를 세션에서 찾아서 선언되어있는 메서드의 인자에다가 전달해줍니다.
@GetMapping("/info") public String userInfo(@SessionAttribute("user") User user) { //... //... return "user"; } |
SessionStatus
- SessionStatus 는 컨트롤러 메소드의 파라미터로 사용할 수 있는 스프링 내장 타입입니다.
- 이 오브젝트를 이용하면 현재 컨트롤러의 @SessionAttributes에 의해 저장된 오브젝트를 제거할 수 있습니다.
- 여기서 SessionAttributes와 ModelAttribute 어노테이션의 인자 이름이 같기 때문에 세션에서 정보를 찾아서 메서드의 인자인 user에다가 전달해줍니다. 전달받은 값을 삭제하고자 할 때는 sessionStatus가 가지고 있는 setComplete()이라는 메서드를 호출하면 세션에서 삭제할 수 있습니다.
@Controller @SessionAttributes("user") public class UserController { ...... @RequestMapping(value = "/user/add", method = RequestMethod.POST) public String submit(@ModelAttribute("user") User user, SessionStatus sessionStatus) { ...... sessionStatus.setComplete(); ...... } } |
Spring MVC - form tag 라이브러리
- modelAttribute속성으로 지정된 이름의 객체를 세션에서 읽어와서 form태그로 설정된 태그에 값을 설정합니다.
- Spring MVC가 제공하는 tag 라이브러리를 이용하면 세션에 있는 정보들을 form에다가 출력을 할 수도 있습니다.
<form:form action="login" method="post" modelAttribute="user"> Email : <form:input path="email" /><br> Password : <form:password path="password" /><br> <button type="submit">Login</button> </form:form> |
실습코드
요구사항
- 관리자는 /loginform에서 암호를 입력해 로그인을 한다.
- 관리자가 암호를 맞게 입력할 경우 세션에 로그인 정보가 저장된다.
- 세션에 로그인 정보가 있을 경우 방명록에는 "삭제" 링크가 보여진다.
- 삭제 링크를 누르면 삭제가 된다. 삭제 작업에서도 로그인 정보가 있는지를 검사해야 한다.
- 먼저 GuestbookAdminController를 생성합니다. 이 Controller에서 해야 될 일은 로그인 폼을 요청하면 해당 요청을 처리하는 일과 로그인 요청하면 로그인을 처리해주는 두가지 역할을 합니다. loginform() 메서드는 클라이언트가 loginform이라고 요청을 하게 되면 이 요청에 대해서 처리하는 메서드입니다. loginform이라고 요청이 들어오면 해당 요청을 받아서 loginform.jsp가 해당 일을 수행을 할 거니까 이 view의 정보를 보내주기만 하는 메서드를 작성하면 됩니다.
- 그다음은 loginform.jsp를 만듭니다. 해당 jsp는 화면 내에서 input 상자를 이용해서 passwd를 입력받고 submit 버튼을 누르게 되면 Login이라고 요청을 보냅니다. 요청하는 방식은 post 방식으로 보내게 할 거예요.
- login이라고 요청을 하게 되면 컨트롤러에서 해당 요청을 받아서 처리할 수 있는 메서드를 만듭니다. PostMapping으로 login이라는 값을 받아옵니다. 로그인 메서드는 loginform.jsp로부터 암호를 전달을 받아서 해당 암호가 일치할 경우에 세션에 로그인 정보를 저장하는 목적으로 사용하게 될 겁니다.
- 로그인 처리화면에서 패스워드를 passwd로 받았습니다. @RequestParam은 그 이름이 일치해야 합니다. 그래야RequestParam의 name이 이 passwd를 받아들여서 String 변수 passwd에 넣을 수 있습니다.
- 이때 받아온 passwd가 임의의 값, 1234와 동일하다면 isAdmin 이라는 key에 true라는 value를 가진 세션을 넘겨줍니다.
- 암호가 맞지 않을 경우에는 RedirectAttributes인 redirectAttr 에다가 errorMessage를 저장합니다.
- RedirectAttributes는 DispatcherServlet이 관리하는 FlashMap에 값을 저장합니다. FlashMap은 redirect할 때 딱 한 번만 값을 유지할 목적으로 사용을 하는 것입니다.
- 항상 이 redirect와 forward의 차이점 잘 기억하셔야 되는데 redirect는 요청이 한 번이 아니라 두 번 오는 거라 그랬죠.
- 이 요청은 login이고 redirect를 한다는 건 loginform이라는 요청을 다시 보내겠다는 건데 이 loginform의 errormessage 값을 쓰겠다는 거잖아요.
- 요청이 달라진 것이기 때문에 forward 같은 경우는 하나의 요청이기 때문에 유지시키고 싶은 값들은 request 영역에다가 넣어놓고 사용했었는데 redirect됐을 때는 request가 저장이 되지 않습니다. 그래서 이러한 부분들을 위해 세션에 저장할 수도 있지만 이런 RedirectAttribute를 이용하시면 조금 더 효율적일 수 있습니다.
- 그리고 비밀번호가 일치해서 setAttribute가 됐다면 list로 redirect 시킵니다.
- 다른 값을 넣었을 때 일치하지 않으면 '암호가 틀렸습니다'라는 메시지 값을 하나 넣어줬고 redirect로 loginform을 다시 요청했죠. 그러면 loginform에서 errorMessage 값을 FlashMap 에서 꺼내가지고 보여주고 다시 똑같이 form을 보여주게 됩니다. 1234를 입력하면 세션에다가 isAdmin이라는 이름으로 true 라는 값을 넣고 list로 redirect 하라고 해서 list 화면이 나왔습니다.
GuestbookAdminController.java
package kr.or.connect.guestbook.controller;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.support.SessionStatus; import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller public class GuestbookAdminController {
@GetMapping(path="/loginform") public String loginform() { return "loginform"; }
@PostMapping(path="/login") public String login(@RequestParam(name="passwd", required=true) String passwd, HttpSession session, RedirectAttributes redirectAttr) {
if("1234".equals(passwd)) { session.setAttribute("isAdmin", "true"); }else { redirectAttr.addFlashAttribute("errorMessage","암호가 틀렸습니다."); return "redirect:/loginform"; } return "redirect:/list"; }
@GetMapping(path="/logout") public String login(HttpSession session) { session.removeAttribute("isAdmin"); return "redirect:/list"; } } |
loginform.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>loginform</title> </head> <body> <h1>관리자 로그인</h1> <br><br> ${errorMessage}<br>
<form method="post" action="login"> 암호 : <input type="password" name="passwd"><br> <input type="submit"> </form>
</body> </html> |
- 기존 코드에서 /delete 삭제 부분을 추가하겠습니다.
- 세션에 isAdmin이름의 값이 있을 경우에만 삭제 처리를 하도록 합니다.
- list 화면에서 진짜 로그인해서 들어갔는지 확인하기 위해 list.jsp에다가 isAdmin이라는 세션 값이 있을 경우에 삭제 링크를 하나 걸어줍니다.
- 그 다음 삭제가 되도록 GuestbookController.java를 수정합니다. 이 부분에서 삭제하는 메서드를 어디에다 둬야 할지 고민해야 합니다.
- 지금 여기서, GuestbookAdminController에다가 delete라는 메서드를 두기 보다는 delete는 방명록에 있는 글을 삭제하기 위함이므로 방명록에 대한 메서드들을 갖고 있던 GuestbookController에 위치시키도록 합니다. GuestbookController는 리스트에 대한 CRUD가 수행되는 컨트롤러이기 때문입니다.
- request.getRemoteAddr()은 delete를 수행할 때 ip를 log 테이블에도 기록하기 위함입니다.
- 마지막으로 logout이라고 URL에 요청이 들어온다면 logout을 처리하도록 합니다. 세션에서 값을 지울 때는 removeAttribute() 이용해서 세션 이름만 넘겨주면 삭제할 수 있습니다. 이렇게 logout을 하고 다시 list 화면으로 넘겨주도록 합니다.
- 완성된 화면입니다.
GuestbookController.java
package kr.or.connect.guestbook.controller;
import java.util.ArrayList; import java.util.List;
import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.CookieValue; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.SessionAttribute; import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import kr.or.connect.guestbook.dto.Guestbook; import kr.or.connect.guestbook.service.GuestbookService;
@Controller public class GuestbookController { @Autowired GuestbookService guestbookService;
@GetMapping(path="/list") public String list(@RequestParam(name="start", required=false, defaultValue="0") int start, ModelMap model, @CookieValue(value="count", defaultValue="1", required=true) String value, HttpServletResponse response) {
try { int i = Integer.parseInt(value); value = Integer.toString(++i); }catch(Exception ex){ value = "1"; }
Cookie cookie = new Cookie("count", value); cookie.setMaxAge(60 * 60 * 24 * 365); // 1년 동안 유지. cookie.setPath("/"); // / 경로 이하에 모두 쿠키 적용. response.addCookie(cookie);
List<Guestbook> list = guestbookService.getGuestbooks(start);
int count = guestbookService.getCount(); int pageCount = count / GuestbookService.LIMIT; if(count % GuestbookService.LIMIT > 0) pageCount++;
List<Integer> pageStartList = new ArrayList<>(); for(int i = 0; i < pageCount; i++) { pageStartList.add(i * GuestbookService.LIMIT); }
model.addAttribute("list", list); model.addAttribute("count", count); model.addAttribute("pageStartList", pageStartList); model.addAttribute("cookieCount", value);
return "list"; }
@PostMapping(path="/write") public String write(@ModelAttribute Guestbook guestbook, HttpServletRequest request) { String clientIp = request.getRemoteAddr(); System.out.println("clientIp : " + clientIp); guestbookService.addGuestbook(guestbook, clientIp); return "redirect:list"; }
//delete @GetMapping(path="/delete") public String delete(@RequestParam(name="id", required=true) Long id, @SessionAttribute("isAdmin") String isAdmin, HttpServletRequest request, RedirectAttributes redirectAttr) { if(isAdmin == null || !"true".equals(isAdmin)) { // 세션값이 true가 아닐 경우 redirectAttr.addFlashAttribute("errorMessage", "로그인을 하지 않았습니다."); return "redirect:loginform"; } String clientIp = request.getRemoteAddr(); guestbookService.deleteGuestbook(id, clientIp); return "redirect:list"; } } |
List.jsp (기존 list.jsp에서 isAdmin세션값이 있을 경우 삭제 링크를 걸어줍니다)
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>방명록 목록</title> </head> <body>
<h1>방명록</h1> <br> 방명록 전체 수 : ${count }, 방문한 수 : ${cookieCount }<br><br>
<c:forEach items="${list}" var="guestbook">
${guestbook.id }<br> ${guestbook.name }<br> ${guestbook.content }<br> ${guestbook.regdate }<br> <!-- 삭제 버튼 시작--> <c:if test="${sessionScope.isAdmin == 'true'}"><a href="delete?id=${guestbook.id}">삭제</a><br><br></c:if> <!-- 삭제 버튼 끝--> </c:forEach> <br>
<c:forEach items="${pageStartList}" var="pageIndex" varStatus="status"> <a href="list?start=${pageIndex}">${status.index +1 }</a> </c:forEach>
<br><br> <form method="post" action="write"> name : <input type="text" name="name"><br> <textarea name="content" cols="60" rows="6"></textarea><br> <input type="submit" value="등록"> </form> </body> </html> |
생각해보기
- Spring에서는 인증 등을 처리하기 위해 Spring Security라는 모듈을 제공합니다. 우리 수업에서는 Spring Security를 다루지 않고 있는데요. Spring을 이용한 상용 웹 어플리케이션에서는 굉장히 많이 사용되고 있습니다. 모든 과정을 학습한 이후에 Spring Security를 별도로 학습해서 인증처리 구현을 한번 해보세요.
참고 자료
[참고링크] Web MVC framework
https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/mvc.html#mvc-ann-sessionattrib
'부스트코스 웹 프로그래밍 > 5. 웹 앱 개발: 예약서비스 3' 카테고리의 다른 글
6. 인터셉터 - BE (2) (0) | 2019.08.10 |
---|---|
6. 인터셉터 - BE (1) (0) | 2019.08.10 |
4. 상태유지기술(Cookie & Session) - BE (5) (0) | 2019.08.09 |
4. 상태유지기술(Cookie & Session) - BE (4) (0) | 2019.08.09 |
4. 상태유지기술(Cookie & Session) - BE (3) (0) | 2019.08.09 |