5-3. 회원 기본 기능 구현 (Controller)
이제 웹 페이지에서 회원 기능을 사용할 수 있도록 Controller를 만들어 API를 제공하고, 뷰를 제공해 보자.
Model 생성
Controller 계층에서 사용할 모델을 추가해 준다.
Service 계층에서 사용할 모델과 크게 다르지 않다.
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class WebUser {
private String userName;
private String id;
private String password;
}
이번에도 마찬가지로 Model 변환 클래스를 따로 하나 만들어 사용하자.
public class WebUserMapper {
public static ServiceUser getServiceUser(WebUser webUser){
return new ServiceUser(
webUser.getUserName(),
webUser.getId(),
webUser.getPassword());
}
public static WebUser getWebUser(ServiceUser serviceUser){
return new WebUser(
serviceUser.getUserName(),
serviceUser.getId(),
serviceUser.getPassword()
);
}
}
Controller 구현
먼저 Controller를 구현하여 API를 제공해 보고, 직접 URL을 입력해 결과를 확인해 보자.
Controller 기본 구성
기본적인 Controller 구성은 다음과 같다.
간단하게 URL과 메서드에 따른 view 이름정도만 구상을 한 상태이다.
@Controller
@RequestMapping("/user")
public class UserController {
final static String userViewPath = "/user";
@Autowired
private UserService userService;
//유저 홈으로 이동
@GetMapping
public String userHome(){
return userViewPath + "/home";
}
//회원 가입 폼으로 이동
@GetMapping("/join")
public String joinForm(){
return userViewPath + "/join";
}
//회원 가입 후 회원 정보 페이지로
@PostMapping("/join")
public String saveUser(){
return userViewPath + "/user";
}
//회원 조회시 회원 정보 페이지로
@GetMapping("{userIdx}")
public String findUser(){
return userViewPath + "/user";
}
//회원 수정 이후 회원 정보 페이지로
@PostMapping("{userIdx}")
public String modifyUser(){
return userViewPath + "/user";
}
//회원 탈퇴 이후 탈퇴 성공 페이지로
@DeleteMapping("{userIdx}")
public String deleteUser() {
return userViewPath + "/delete";
}
}
이제 각 요청에 대한 기능을 구현해 보자.
회원 홈
회원가입 링크를 가지고 있는 회원 홈을 만들고, 회원 홈으로 이동시켜 주는 컨트롤러 메서드를 만들어 준다.
먼저, 뷰는 다음과 같다.
<!-- home.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Home</title>
</head>
<body>
<h2>유저 홈</h2>
<a th:href="@{/user/join}">회원가입</a>
</body>
</html>
회원가입 링크를 클릭하면 '/user/join' URL로 요청을 보낸다.
회원 가입
뷰 구성
회원 가입을 진행하는 뷰는 다음과 같다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<H2>회원가입 페이지</H2>
<form action="user.html"
th:object="${userClass}"
th:action method="post">
<div>
<label for="userName">이름</label>
<input type="text" id="userName"
th:field="*{userName}"
placeholder="이름을 입력하세요">
</div>
<div>
<label for="id">ID</label>
<input type="text" id="id" th:field="*{id}"
placeholder="ID를 입력하세요">
</div>
<div>
<label for="password">비밀 번호</label>
<input type="password" id="password"
th:field="*{password}"
placeholder="비밀번호를 입력하세요">
</div>
<button type="submit"> 회원가입 </button>
</form>
</body>
</html>
<input> 태그의 id, name, value를 쉽게 작성하기 위해 th:field를 사용하였고, 이를 위해 <form> 태그에 th:object 속성을 사용하였다.
해당 속성은 모델에서 해당 이름의 클래스를 가지고 오기 때문에 컨트롤러에서 모델에 객체를 삽입해 주어야 한다.
//UserController
//...
//회원 가입 폼으로 이동
@GetMapping("/join")
public String joinForm(Model model){
model.addAttribute("userClass", new WebUser());
return userViewPath + "/join";
}
//...
회원가입 완료
회원가입 버튼을 누르면 해당 페이지로 POST 요청이 전달된다.
우선 정상적으로 요청이 전달되는지 확인하기 위해 로그만 간단히 찍어보자.
그리고, 회원가입이 성공하면 유저 상세페이지로 이동할 것이기 때문에 '/user/user'로 요청을 보낸다.
POST 요청에서는 새로고침에 대해 중복 요청이 전달되지 않도록 "redirect:{URL}"을 사용한다.
//...
//회원 가입 후 회원 정보 페이지로
@PostMapping("/join")
public String saveUser(@ModelAttribute WebUser user){
System.out.println(
"username = " + user.getUserName() +
", userID = " + user.getId() +
", userPW = " + user.getPassword());
return "redirect:/user/user";
}
//...
@ModelAttribute를 사용해 전달받은 Model의 값들로 WebUser 객체를 만들어 주었다.
위와 같은 데이터에 대해
위와 같은 출력을 확인할 수 있다.
이제 정말 회원을 저장해 보자.
회원 저장
//...
//회원 가입 후 회원 정보 페이지로
@PostMapping("/join")
public String saveUser(@ModelAttribute WebUser user){
long savedId;
try {
savedId = userService.
saveUser(WebUserMapper.getServiceUser(user));
} catch (DuplicatedUserIdException e) {
e.printStackTrace();
}
return "redirect:/user/{savedId}";
}
UserService의 saveUser() 메서드는 중복된 아이디 값에 대해 Exception을 발생시킨다. 해당 예외 처리는 별도의 뷰를 만들어야 하니 정상적인 동작을 먼저 확인해 보자.
위와 같은 데이터를 입력하면
저장된 회원의 인덱스를 URL로 사용해 요청을 보내고,
데이터 역시 정상적으로 저장됨을 확인할 수 있다.
회원 저장 실패
실패의 경우 경고 메시지를 출력하려 한다.
이를 위해 웹 브라우저에서 공통적으로 사용할 경고메시지 전달용 클래스를 하나 생성한다.
package zangsu.selfmadeBlog.model.web;
@AllArgsConstructor
@Getter
public class Warning {
private String message;
}
그리고, 예외 발생에 대해 Warning 객체를 생성해 Model에 전달한다.
//회원 가입 후 회원 정보 페이지로
@PostMapping("/join")
public String saveUser(/*...*/){
//...
try {
//...
} catch (DuplicatedUserIdException e) {
model.addAttribute("userClass", new WebUser());
model.addAttribute("warnings",
new Warning("중복된 회원 ID 입니다."));
return userViewPath + "/join";
}
//...
}
join.html에서는 <input> 태그에서 사용할 빈 WebUser 객체가 필요하므로 해당 객체도 함께 전달해 준다.
<!-- join.html -->
<!-- ... -->
<script th:if="${warnings} != null" th:inline="javascript">
alert([[${warnings.message}]]);
</script>
<!-- ... -->
뷰에서는 warnings 속성이 존재하는지 확인 후, 존재한다면 해당 경고 메시지를 출력해 준다.
위와 같은 회원 정보를 입력하면 첫 시도에서는
성공적으로 저장되지만
두번쨰 시도에서는
경고창이 뜨면서
다시 회원 가입 페이지로 돌아온다!!
회원 조회
회원을 조회하는 기능을 구현해 본다.
뷰는 회원 정보를 표시하고, 수정할 수 있는 공통의 뷰를 만들 것이다.
뷰
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<H2>유저 상세 페이지</H2>
</body>
<form th:object="${user}" method="post">
<div>
<label for="username">이름 : </label>
<input type="text" id="username"
th:field="*{userName}">
</div>
<div>
<label for="id">ID : </label>
<input type="text" id="id" th:field="*{id}">
</div>
<div>
<label for="password">비밀번호 : </label>
<input type="password" id="password"
th:field="*{password}">
</div>
<button type="submit">수정</button>
</form>
</html>
Model로 전달되는 "user" 객체를 가져와 각각의 값을 사용해 데이터를 표시해 준다.
회원 조회 로직
//회원 조회시 회원 정보 페이지로
@GetMapping("{userIdx}")
public String findUser(@PathVariable long userIdx, Model model){
try {
ServiceUser findUser = userService.findUser(userIdx);
model.addAttribute("user", findUser);
return userViewPath + "/user";
} catch (NoSuchUserException e) {
model.addAttribute("warnings",
new Warning("해당 회원이 존재하지 않습니다"));
return userViewPath + "/home";
}
}
pathVariable로 전달된 유저의 idx 값을 이용해 회원을 조회한다.
회원이 존재하지 않으면 경고 객체를 포함하여 홈으로 돌아가고, 그렇지 않으면 회원의 정보를 회원 상세 정보 페이지에 나타내 준다.
회원이 존재하지 않을 때 경고 메시지를 출력하기 위해 home.html에도 아래의 경고 메시지 코드를 추가해 준다.
<script th:if="${warnings} != null" th:inline="javascript">
alert([[${warnings.message}]]);
</script>
위와 같은 요청에 대해
이렇게 회원 정보가 담겨서 상세 페이지를 확인할 수 있게 되었다!!
아직 존재하지 않는 회원 인덱스를 사용해 회원을 조회하면
경고메시지 출력 후
유저 홈으로 돌아온다.
회원 수정
회원 수정 역시 같은 뷰를 사용한다.
수정 로직
//회원 수정 이후 회원 정보 페이지로
@PostMapping("{userIdx}")
public String modifyUser(@PathVariable long userIdx,
@ModelAttribute WebUser modifiedUser,
Model model){
try {
userService.modify(userIdx,
WebUserMapper.getServiceUser(modifiedUser));
ServiceUser findUser = userService.findUser(userIdx);
model.addAttribute("user", findUser);
return userViewPath + "/user";
} catch (NoSuchUserException e) {
model.addAttribute("warnings",
new Warning("해당 회원이 존재하지 않습니다"));
return userViewPath + "/home";
} catch (CantModifyFieldException e) {
model.addAttribute("warnings",
new Warning("ID는 수정할 수 없습니다."));
return findUser(userIdx, model);
}
}
이번에는 회원 ID를 수정할 경우 경고메시지를 추가로 전달하였다.
이렇게 작성하고 보니 뷰 자체에서 ID는 수정하지 못하도록 해 주는 것이 좋을 것 같다.
뷰의 ID 부분에 disable 속성을 추가해 주자.
회원가입을 하면
ID를 제외한 정보가 저장되며,
이름을 변경하면
엥???
Network의 Payload를 살펴보니 ID부분이 애초에 전송이 안되고 있었다.
아마 user.Id의 값이 null이었을 것으로 예상된다.
disable 대신 readonly 속성을 사용하면 값의 수정 없이 데이터를 넘길 수 있다!!
회원 삭제
마지막으로 회원 삭제 기능을 구현해 보자.
삭제기능을 위해 회원 정보 뷰에서 회원 탈퇴 버튼을 생성해 준다.
<form> 태그는 get, post 메서드밖에 사용하지 못하지만, delete 메서드를 사용하기 위한 방법이 존재한다.
<form> 태그 하위에 <input type="hidden" name="\_method" value="delete"> 를 추가해 주고, 스프링 부트의 아래 속성을 추가해 주면 된다.
spring:
mvc:
hiddenmethod:
filter:
enabled: true
이제 스프링부트는 _method = delete 속성을 가진 POST 요청을 delete 요청으로 인식해 준다.
타임리프를 사용할 경우 히든 속성의 태그 대신 th:method="delete" 속성을 추가해 주면 된다.
뷰
뷰의 수정 버튼 옆에 삭제 버튼을 새로 만들어 준다.
<!-- user -->
<button type="submit">수정</button>
<form th:action="@{/user/{index}(index=${index})}"
th:method="delete">
<button type="submit">삭제</button>
</form>
이제 위 처럼 index 정보를 사용하기 위해 컨트롤러에서 index 정보를 모델에 담아 주어야 한다.
삭제 성공을 확인하기 위해 삭제 성공 페이지를 간단히 구현해 보자.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Home</title>
</head>
<body>
<h2>삭제 성공</h2>
회원이 성공적으로 삭제되었습니다.
</body>
</html>
삭제 로직
//회원 조회시 회원 정보 페이지로
@GetMapping("{userIdx}")
public String findUser(@PathVariable long userIdx,
Model model){
try {
ServiceUser findUser = userService.findUser(userIdx);
model.addAttribute("user", findUser);
// index 정보를 추가해 준다.
model.addAttribute("index", userIdx);
return userViewPath + "/user";
} catch (NoSuchUserException e) {
model.addAttribute("warnings",
new Warning("해당 회원이 존재하지 않습니다"));
return userViewPath + "/home";
}
}
//...
//회원 탈퇴 이후 탈퇴 성공 페이지로
@DeleteMapping("{userIdx}")
public String deleteUser(@PathVariable int userIdx,
Model model) {
try {
userService.delete(userIdx);
return userViewPath + "/delete";
} catch (NoSuchUserException e) {
model.addAttribute("warnings",
new Warning("해당 회원이 존재하지 않습니다"));
return userViewPath + "/home";
}
}
위와 같은 회원을 등록한 후
삭제 버튼을 눌러주면
삭제 성공 페이지로 이동하고
다시 회원을 조회하려 하면 회원이 존재하지 않는다고 뜬다.