어노테이션
@Retention
@Target
Type
- 클래스, 인터페이스
- 예시
@Controller
public class HomeController {
}
Field
- 멤버변수
- 예시
public class UserService {
@Autowired
private UserMapper userMapper
}
public class PostController {
@value("${attachment.maxuploadsize}")
private long maxUploadSize
}
Method
- 메소드의 매개변수
- 예시
@GetMapping("/home")
public String home() {}
@PostMapping(
Parameter
Constructor
Annotaition Type
LoginUser.java
package com.sample.annotaition;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* 세션에 저장된 로그인된 사용자정보(LoginUserInfo 객체)를 전달받는 매개변수에 이 어노테이션을 지정한다.
* @author user
*
*/
@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface LoginUser {
boolean required() default true;
}
/*
* @Controller
* public class UserController {
* @PostMapping("/password")
* public String changePassword(@LoginUser LoginUserInfo loginUserInfo) {
*
* }
* }
*/
LoginUserHandlerMethodArgumentResolver.java
package com.sample.web.resolver;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import com.sample.utils.SessionUtils;
import com.sample.web.login.LoginUser;
import com.sample.web.login.LoginUserInfo;
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 요청 핸들러 메소드의 매개변수가 @Loginuser 어노테이션을 가지고 있으면 true를 반환한다.
return parameter.hasParameterAnnotation(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 세션에서 "loginUser"라는 속성명으로 저장된 객체를 조회한다.
LoginUserInfo loginUserInfo = (LoginUserInfo) SessionUtils.getAttribute("loginUser");
return loginUserInfo;
}
}
web-context.xml
<!--
<mvc:annotation-driven> 태그는
spring mvc 관련 어노테이션을 감지/분석해서 적절한 작업을 수행하는 객체를 스프링의 빈으로 등록시킨다.
<mvc:argument-resolvers> 태그는
사용자정의 HanderMethodArgumentResolver를 추가시킨다.
* LoginuserHandlerMethodArgumentResolver는 요청핸들러 메소드의 매개변수에 @LoginUser 어노테이션이 지정되어 있으면
세션에서 로그인된 사용자정보를 조회해서 매개변수에 전달한다.
-->
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="com.sample.web.resolver.LoginUserHandlerMethodArgumentResolver"></bean>
</mvc:argument-resolvers>
</mvc:annotation-driven>
UserController.java
package com.sample.web.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.sample.dto.UserDetailDto;
import com.sample.service.UserService;
import com.sample.web.login.LoginUser;
import com.sample.web.login.LoginUserInfo;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/info")
public String info(@LoginUser LoginUserInfo loginUserInfo, Model model) {
UserDetailDto userDetailDto = userService.getUserDetail(loginUserInfo.getId());
model.addAttribute("user", userDetailDto);
return "user/detail";
}
}
실행결과
UserController
@GetMapping("/delete")
public String deleteform() {
return "user/delete-form"; // 회원탈퇴화면의 이름을 반환한다. /WEB-INM/views/user/delete-form.jsp
}
@PostMapping("/delete")
public String delete() {
return "redirect:delete-success"; // 회원탈퇴 성공화면을 재요청하는 URL을 반환한다.
}
@GetMapping("/delete-success")
public String deleteSuccess() {
return "user/delete-success"; // 회원탈퇴성공화면의 이름을 반환한다. /WEB-INF/views/user/delete-success.jsp
}
deleteform.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">내 정보 보기</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 active">탈퇴하기</a>
</div>
</div>
</div>
</div>
<div class="col-9">
<p><strong>${loginUser.name }</strong>님 탈퇴를 요청하였습니다. 비밀번호를 입력하고 탈퇴버튼을 클릭하세요</p>
<form class="border bg-light p-3" method="post" action="delete">
<div class="mb-3">
<label class="form-label">비밀번호</label>
<input type="password" class="form-control" name="password">
</div>
<div class="text-end">
<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>
</body>
</html>
실행결과
deleteform.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">내 정보 보기</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 active">탈퇴하기</a>
</div>
</div>
</div>
</div>
<div class="col-9">
<p><strong>${loginUser.name }</strong>님 탈퇴를 요청하였습니다. 비밀번호를 입력하고 탈퇴버튼을 클릭하세요</p>
<form class="border bg-light p-3" method="post" action="delete">
<div class="mb-3">
<label class="form-label">비밀번호</label>
<input type="password" class="form-control" name="password">
</div>
<div class="text-end">
<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>
</body>
</html>
UserService
public void deleteUser(String userId, String password) {
User user = userMapper.getUserById(userId);
if (user == null) {
throw new ApplicationException("사용자 정보가 존재하지 않습니다.");
}
if("Y".equals(user.getDeleted())) {
throw new ApplicationException("이미 탈퇴처리된 사용자입니다.");
}
if (!user.getPassword().equals(password)) {
throw new ApplicationException("비밀번호가 일치하지 않아서 탈퇴처리할 수 없습니다.");
}
user.setDeleted("Y");
userMapper.updateUser(user);
}
delete-success.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">
<div class="border p-3 bg-light">
<h1 class="mb-4">회원탈퇴 처리 완료</h1>
<p>회원탈퇴처리가 완료되었습니다.</p>
</div>
</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>
실행결과(비밀번호 틀렸을 경우)
실행결과(탈퇴 완료 시)
- ID가 hong인 사용자의 DELETED 여부가 Y로 변경되었다.
실행결과(탈퇴 후 로그인 시)
password-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="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">내 정보 보기</a>
<a href="/user/password" class="list-group-item list-group-item-action active">비밀번호 변경</a>
<a href="/user/delete" class="list-group-item list-group-item-action">탈퇴하기</a>
</div>
</div>
</div>
</div>
<div class="col-9">
<p><strong>${loginUser.name }</strong>님 이전 비밀번호와 새 비밀번호를 입력하세요</p>
<form class="border bg-light p-3" method="post" action="password">
<div class="mb-3">
<label class="form-label">이전 비밀번호</label>
<input type="password" class="form-control" name="oldPassword">
</div>
<div class="mb-3">
<label class="form-label">새 비밀번호</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<div class="mb-3">
<label class="form-label">비밀번호 확인</label>
<input type="password" class="form-control" id="password-confirm">
</div>
<div class="text-end">
<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>
</body>
</html>
UserController
@GetMapping("/password")
public String passwordChangeForm() {
return "user/password-form";
}
@PostMapping("/password")
public String changePassword() {
return "redirect:password-success"; // redirect:뒤에 /를 붙이면 절대경로가 되기 때문에 붙이면 안된다!
}
@GetMapping("/password-success")
public String passwordChangeSuccess() {
return "user/password-success";
}
실행결과
password-form.jsp
<script type="text/javascript">
$(function) {
$("#form-change-password").submit(function) {
let oldPassword = $(":password[name=oldPassword]").val(); //name으로 찾기 때문에 #이 붙지 않는다.
let password = $("#password").val(); //id로 찾기 때문에 #이 붙는다.
let passwordConfirm = $("#password-confirm").val(); //id로 찾기 때문에 #이 붙는다.
if (oldPassword == "") {
alert("이전 비밀번호는 필수 입력값입니다.");
return false;
}
if (password == "") {
alert("새 비밀번호는 필수 입력값입니다.");
return false;
}
if (passwordConfirm == "") {
alert("비밀번호 확인은 필수 입력값입니다.");
return false;
}
if (oldPassword == password) {
alert("새 비밀번호를 이전 비밀번호와 같은 값으로 지정할 수 없습니다.");
return false;
}
if (password != passwordConfirm) {
alert("새 비밀번호와 비밀번호 확인 값이 서로 일치하지 않습니다.");
return false;
}
return true;
});
}
</script>
UserController
@GetMapping("/password")
public String passwordChangeForm() {
return "user/password-form";
}
@PostMapping("/password")
public String changePassword(@LoginUser LoginUserInfo loginUserInfo,
@RequestParam(name = "oldPassword") String oldPassword,
@RequestParam(name = "password") String password) {
userService.changePassword(password, oldPassword, password);
return "redirect:password-success";
}
@GetMapping("/password-success")
public String passwordChangeSuccess() {
return "user/password-success";
}
UserService
public void changePassword(String userId, String oldPassword, String password) {
User user = userMapper.getUserById(userId);
if (user == null) {
throw new ApplicationException("사용자 정보가 존재하지 않아서 비밀번호를 변경할 수 없습니다.");
}
if("Y".equals(user.getDeleted())) {
throw new ApplicationException("이미 탈퇴처리된 사용자는 비밀번호를 변경할 수 없습니다.");
}
if (!user.getPassword().equals(oldPassword)) {
throw new ApplicationException("비밀번호가 일치하지 않아서 비밀번호를 변경할 수 없습니다.");
}
user.setPassword(password);
userMapper.updateUser(user);
}
실행결과(이전 비밀번호 미 입력시)
실행결과(이전 비밀번호 틀릴 시)
실행결과(새 비밀번호 미 입력시)
실행결과(비밀번호 확인 미 입력시)
실행결과(비밀번호 변경 성공 시)
LoginUserHandlerMethodArgumentResolver.java
/**
* @LoginUser 어노테이션을 가지고 있는 요청핸들러 메소드의 매개변수에 제공할 값을 반환한다.
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 세션에서 "loginUser"라는 속성명으로 저장된 객체를 조회한다.
LoginUserInfo loginUserInfo = (LoginUserInfo) SessionUtils.getAttribute("loginUser");
// 매개변수의 어노테이션중에서 @LoginUser 어노테이션을 조회한다.
LoginUser loginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class);
// @LoginUser 어노테이션의 required 속성값을 조회한다.
boolean required = loginUserAnnotation.required();
// required가 true고 loginUserInfo가 null이면 예외를 던진다.
if (required && loginUserInfo == null) {
throw new ApplicationException("로그인이 필요한 서비스입니다.");
}
return loginUserInfo;
LoginCheckHandlerInterceptor.java
package com.sample.web.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class LoginCheckHandlerInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
System.out.println("########## 로그인체크 인터셉트 실행됨");
return true;
}
}
web-context.xml
<!--
사용자정의 인터셉터를 스프링 컨테이너의 빈으로 등록한다.
모든 곳에서 로그인체크를 실행할 것이다.
그러나 home, register, success, login에서는 실행하지 않을 것이다.
-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**/*"/>
<mvc:exclude-mapping path="/home"/>
<mvc:exclude-mapping path="/register"/>
<mvc:exclude-mapping path="/success"/>
<mvc:exclude-mapping path="/login"/>
<bean class="com.sample.web.interceptor.LoginCheckHandlerInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
실행결과
LoginUserHandlerMethodArgumentResolver.java
package com.sample.web.resolver;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import com.sample.exception.ApplicationException;
import com.sample.utils.SessionUtils;
import com.sample.web.login.LoginUser;
import com.sample.web.login.LoginUserInfo;
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
/**
* 요청핸들러 메소드의 매개변수가 @LoginUser 어노테이션을 가지고 있으면 true를 반환한다.
*/
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 요청 핸들러 메소드의 매개변수가 @Loginuser 어노테이션을 가지고 있으면 true를 반환한다.
return parameter.hasParameterAnnotation(LoginUser.class);
}
/**
* @LoginUser 어노테이션을 가지고 있는 요청핸들러 메소드의 매개변수에 제공할 값을 반환한다.
*/
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 세션에서 "loginUser"라는 속성명으로 저장된 객체를 조회한다.
LoginUserInfo loginUserInfo = (LoginUserInfo) SessionUtils.getAttribute("loginUser");
// 매개변수의 어노테이션중에서 @LoginUser 어노테이션을 조회한다.
LoginUser loginUserAnnotation = parameter.getParameterAnnotation(LoginUser.class);
// @LoginUser 어노테이션의 required 속성값을 조회한다.
boolean required = loginUserAnnotation.required();
// required가 true고 loginUserInfo가 null이면 예외를 던진다.
if (required && loginUserInfo == null) {
throw new ApplicationException("로그인이 필요한 서비스입니다.");
}
return loginUserInfo;
}
}
LoginUser.java
package com.sample.web.login;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
* 세션에 저장된 로그인된 사용자정보(LoginUserInfo 객체)를 전달받는 매개변수에 이 어노테이션을 지정한다.
* @author user
*
*/
@Documented
@Retention(RUNTIME)
@Target({PARAMETER, METHOD})
public @interface LoginUser {
/**
* 어노테이션의 속성이다.
* required 속성이 ture로 지정되어 있는 경우, 반드시 LoginUserInfo객체를 제공받아야 한다.
* @return 로그인 정보 필요 여부
*/
boolean required() default true;
}
/*
* @Controller
* public class UserController {
* @PostMapping("/password")
* public String changePassword(@LoginUser LoginUserInfo loginUserInfo) {
*
* }
* }
*/
UserController
package com.sample.web.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.sample.dto.UserDetailDto;
import com.sample.service.UserService;
import com.sample.utils.SessionUtils;
import com.sample.web.login.LoginUser;
import com.sample.web.login.LoginUserInfo;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/info")
public String info(@LoginUser LoginUserInfo loginUserInfo, Model model) {
UserDetailDto userDetailDto = userService.getUserDetail(loginUserInfo.getId());
model.addAttribute("user", userDetailDto);
return "user/detail";
}
@GetMapping("/delete")
@LoginUser
public String deleteform() {
return "user/delete-form"; // 회원탈퇴화면의 이름을 반환한다. /WEB-INM/views/user/delete-form.jsp
}
@PostMapping("/delete")
public String delete(@LoginUser LoginUserInfo loginUserInfo, String password) {
// 탈퇴처리 업무로직을 호출한다.
userService.deleteUser(loginUserInfo.getId(), password);
// 세션에 저장된 로그인정보를 삭제한다.
SessionUtils.removeAttribute("loginUser");
return "redirect:delete-success"; // 회원탈퇴 성공화면을 재요청하는 URL을 반환한다.
}
@GetMapping("/delete-success")
public String deleteSuccess() {
return "user/delete-success"; // 회원탈퇴성공화면의 이름을 반환한다. /WEB-INF/views/user/delete-success.jsp
}
@GetMapping("/password")
@LoginUser
public String passwordChangeForm() {
return "user/password-form";
}
@PostMapping("/password")
public String changePassword(@LoginUser LoginUserInfo loginUserInfo,
@RequestParam(name = "oldPassword") String oldPassword,
@RequestParam(name = "password") String password) {
userService.changePassword(loginUserInfo.getId(), oldPassword, password);
return "redirect:password-success";
}
@GetMapping("/password-success")
public String passwordChangeSuccess() {
return "user/password-success";
}
}
실행결과
- 비로그인시 접근하지 못하도록 하였다.