[2023.1.6.금] 스프링 MVC 웹 애플리케이션 3 (로그인, 내정보 조회)
* 의존성 관계
- 스프링 컨테이너가 객체 생성 + 의존성 관계 분석해서 조립까지 해줌
- ContextLoaderListener 가 만드는 스프링 컨테이너, DispatcherServlet 이 만드는 스프링 컨테이너
* 개발자가 해야할 일
1) 매핑정보 (어떤 요청 URI로 받을지 결정) ( @PostMapping("/register") )
2) 메소드 생성
3) 매개변수를 뭐로 받을지 결정
4) 반환타입 결정
Controller의 요청핸들러 메소드
@PostMapping("/login")
public String login(String id, String password)
- 요청핸들러 메소드의 매개변수가 기본자료형 혹은 String 인 경우
- 매개변수의 이름과 동일한 이름으로 요청파라미터 값을 조회해서 매개변수로 전달한다.
(로그인 폼의 name 변수를 직접 매개변수로 넣음) - 매개변수의 타입이 기본자료형 타입인 경우 해당 타입으로 형변환해서 전달한다.
(String을 int로) - 매개변수의 타입이 기본자료형(정수, 실수, 문자, 불린)일 때, 요청파라미터값이 존재하지 않으면 오류가 발생한다. (String 문자열일 때는 값을 넣지않고 넘겨도 오류 X)
요청파라미터 값을 해당 타입으로 변환할 수 없을 때 오류가 발생한다. (입력폼에 입력한 값이 매개변수의 타입과 다를 때)
- 매개변수의 이름과 동일한 이름으로 요청파라미터 값을 조회해서 매개변수로 전달한다.
- @RequestParam
- 요청파라미터 값을 요청핸들러의 매개변수와 매핑시키는 어노테이션이다.
- 주요 속성
- name : 요청파라미터의 이름 (입력폼의 name)
- required : 기본값은 true, false로 지정하면 name에 지정한 요청파라미터값이 없어도 오류가 발생하지 않는다.
- defaultValue : name에 지정한 요청파라미터값이 존재하지 않을 때의 기본값을 설정한다.
- defaultValue의 값은 문자열로 설정되지만 매개변수에 대입될 때는 해당 타입으로 형변환된다.
- public String list(@RequestParam(name = "page", required = false, defaultValue = "1") int page,
@RequestParam(name = "rows", required = false, defaultValue = "10") int rows,
@RequestParam(name = "sort", required = false, defaultValue = "date") String sort)
- 요청핸들러 메소드의 매개변수
HttpServletRequest : 요청객체
HttpServletResponse : 응답객체
HttpSession : 세션객체
WebRequest : Spring이 제공하는 객체. 요청객체가 가지고 있는 정보 대부분을 제공하는 객체
TimeZone : 시간정보
Locale : 지역정보 (국가, 언어)
InputStream : 클라이언트와 연결된 읽기 전용 스트림
OutputStream : 클라이언트와 연결된 쓰기 전용 스트림
Reader : 클라이언트와 연결된 텍스트 읽기 전용 스트림
Writer : 클라이언트와 연결된 텍스트 읽기 전용 스트림
@RequestParam : 요청파라미터와 매개변수를 매핑시키는 어노테이션
@PathVariable : 요청 URL 경로에 포함된 파라미터값과 매개변수를 매핑시키는 어노테이션
@ModelAttribute : 요청파라미터의 해당값을 저장하는 객체를 매핑시키는 어노테이션
@RequestBody : 요청메시지의 바디부 정보와 매개변수를 매핑시키는 어노테이션
@Valid : 요청파라미터값의 유효성 여부를 검증시키는 어노테이션
Model : 뷰에 전달할 정보를 저장하는 객체
Errors : 요청파라미터값의 유효성 검증 결과를 저장하는 객체
BindingResult : 요청파라미터값의 유효성 검증 결과를 저장하는 객체
SessionStatus : 세션에 저장된 정보를 삭제하는 객체
기본자료형 : 요청파라미터값을 전달받는다.
String : 요청파라미터값을 전달받는다.
사용자정의 객체 : 요청파라미터값을 전달받는다.
- 요청 URL 매핑하기
- 요청 URL과 요청핸들러 메소드를 매핑시키는 어노테이션
- @RequestMapping : 요청방식에 상관없이(GET, POST) 요청URL을 기준으로 매핑
- @GetMapping : 요청방식이 GET 방식이고 요청 URL이 일치하는 요청핸들러 메소드와 매핑
- @PostMapping : 요청방식이 POST 방식이고 요청 URL이 일치하는 요청핸들러 메소드와 매핑
- @PutMapping : 요청방식이 PUT 방식이고 요청 URL이 일치하는 요청핸들러 메소드와 매핑
- @DeleteMapping : 요청방식이 DELETE 방식이고 요청 URL이 일치하는 요청핸들러 메소드와 매핑
- 요청 URL과 요청핸들러 메소드를 매핑시키는 어노테이션
- 요청방식
- GET : 서버에서 정보 조회하는 요청 (링크 클릭, 주소창에 입력)
- POST : 서버에서 정보 추가하는 요청 (입력폼에 입력)
- PUT : 서버에서 정보 변경하는 요청
- DELETE : 서버에서 정보 삭제하는 요청
* 일반적인 웹 애플리케이션에서는 GET, POST 두가지 방식을 사용한다.
* REST 방식의 웹 애플리케이션에서는 GET, POST, PUT, DELETE 방식을 전부 사용한다.
* SessionUtils 객체에 사용자 정보 담기
내 정보 조회, 회원 탈퇴, 게시글 작성/수정/삭제, 게시글 추천하기 등등
-> 세션객체의 로그인된 사용자 정보가 필요
-> 요청핸들러 메소드의 매개변수로 세션객체 (HttpSession session) 가 항상 필요
-> 근데 너무 번거로워!
-> 가급적이면 Controller에서는 톰캣이 제공하는 객체 (HttpRequest, HttpResponse, HttpSession)를 안적고싶어
-> com.sample.utils -> SessionUtils 객체 생성
* 스프링이 제공하는 RequestContextHolder의 RequestAttributes 메소드
: 지금 현재 요청 정보(요청객체의 속성, 세션객체의 속성)가 들어있는 RequestAttributes 객체 반환
-> 이 객체는 setAttribute, removeAttribute, getAttribute 메소드를 가지고 있음
- HomeController
HttpSession session 매개변수의 session.setAttribute -> SessionUtils. setAttribute("loginUser", user)
1. 로그인 폼 요청
* HomeController (내부이동 -> 로그인폼 jsp를 바로 반환)
* login-form.jsp (+자바스크립트로 유효성체크)
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<title>애플리케이션</title>
</head>
<body>
<c:set var="menu" value="login" />
<%@ include file="common/navbar.jsp" %>
<div class="container">
<div class="row mb-3">
<div class="col-12">
<h1 class="border bg-light p-2 fs-4">로그인</h1>
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<p>아이디와 비밀번호를 입력하고 로그인 버튼을 클릭하세요</p>
<form id="form-register" class="border bg-light p-3" method="post" action="login">
<div class="mb-3">
<label class="form-label">아이디</label>
<input type="text" class="form-control form-control-sm" name="id" />
</div>
<div class="mb-3">
<label class="form-label">비밀번호</label>
<input type="password" class="form-control form-control-sm" name="password" />
</div>
<div class="text-end">
<a href="/home" class="btn btn-secondary btn-sm">취소</a>
<button type="submit" class="btn btn-primary btn-sm">로그인</button>
</div>
</form>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script type="text/javascript">
$(function(){
$("#form-register").submit(function(){
let id = $("#form-register :input[name=id]").val();
let password = $("#form-register :input[name=password]").val();
if(id == ""){
alert("아이디는 필수입력값입니다.");
return false;
}
if(password == ""){
alert("비밀번호는 필수입력값입니다.");
return false;
}
return true;
})
})
</script>
</body>
</html>
2. 로그인
* HomeController (로그인폼, 로그인, 로그아웃 요청 -> 업무로직 호출 -> 페이지 반환)
** SessionUtils.setAttributes("loginUser", loginUserInfo);
: "loginUser" 라는 이름으로 loginUserInfo 객체 저장 => 세션에 저장됨 !!
* LoginUserInfo 객체 생성 (세션객체에 담을 아이디, 이름만 담긴 로그인 객체)
- 보통은 세션객체에 사용자의 모든 정보를 담지않고, 아이디와 이름 정도만 담음
* UserService (로그인 업무로직 - 유효성 체크)
* navbar.jsp (로그인한 사용자별로 다른 메뉴 출력)
로그인하기 정리
3. 로그아웃
* HomeController (그냥 세션에서 삭제하기만 하면 됨)
로그아웃하기 정리
4. 내 정보 조회
* UserDetailDto (내정보 조회의 항목들로 이루어진 객체를 따로 만들기)
* UserService (업무로직)
* UserController (업무로직 호출)
- @RequestMapping("/user") : 이 클래스 내의 메소드를 실행할때 앞에 무조건 /user가 붙음
Q. 왜 컨트롤러에서 로그인 유효성 체크를 할까?
웹(표현계층) 친화적인 코딩 : 컨트롤러
서버 친화적인 코딩 : 서비스
Q. Service에서 Dto는 왜 싱글턴으로 안만드는지?
A. 정보를 담는 객체는 싱글턴으로 만들지 않는다. -> 동시에 실행하면 정보가 덮어써질 수 있기 때문에
* detail.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" trimDirectiveWhitespaces="true"%>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<title>애플리케이션</title>
</head>
<body>
<c:set var="menu" value="user" />
<%@ include file="../common/navbar.jsp" %>
<div class="container">
<div class="row mb-3">
<div class="col-12">
<h1 class="border bg-light p-2 fs-4">내 정보</h1>
</div>
</div>
<div class="row mb-3">
<div class="col-3">
<div class="card">
<div class="card-header">
메뉴
</div>
<div class="card-body">
<div class="list-group">
<a href="/user/info" class="list-group-item list-group-item-action active">내 정보 보기</a>
<a href="/user/password" class="list-group-item list-group-item-action">비밀번호 변경</a>
<a href="/user/delete" class="list-group-item list-group-item-action">탈퇴하기</a>
</div>
</div>
</div>
</div>
<div class="col-9">
<p>내 정보를 확인하세요.</p>
<table class="table table-bordered">
<colgroup>
<col width="15%">
<col width="35%">
<col width="15%">
<col width="35%">
</colgroup>
<tbody>
<tr>
<th>아이디</th>
<td>${user.id }</td>
<th>가입일</th>
<td><fmt:formatDate value="${user.createdDate }" pattern="yyyy년 M월 d일" /></td>
</tr>
<tr>
<th>이름</th>
<td>${user.name }</td>
<th>접근권한</th>
<td>
<c:forEach var="userRole" items="${user.userRoles }">
<c:choose>
<c:when test="${userRole.roleName eq 'ROLE_GUEST' }">
<span class="badge text-bg-primary">${userRole.roleName }</span>
</c:when>
<c:when test="${userRole.roleName eq 'ROLE_USER' }">
<span class="badge text-bg-warning">${userRole.roleName }</span>
</c:when>
<c:when test="${userRole.roleName eq 'ROLE_ADMIN' }">
<span class="badge text-bg-success">${userRole.roleName }</span>
</c:when>
</c:choose>
</c:forEach>
</td>
</tr>
<tr>
<th>이메일</th>
<td>${user.email }</td>
<th>전화번호</th>
<td>${user.tel }</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
</body>
</html>