회원정보 수정 중 '비밀번호' 수정을 해보겠다.
이와 같은 과정으로 '비밀번호 수정' 페이지로 이동한다.
1. 비밀번호 수정을 위한 요청 DTO (MemberRequestDto)
[MemberRequestDto.class]
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MemberRequestDto {
public interface Create {}
public interface UpdateEmailPhone {}
public interface UpdatePassword {}
public interface IdOnly {}
@NotNull(groups = {UpdateEmailPhone.class, UpdatePassword.class, IdOnly.class}, message = "ID는 필수 입력 값입니다.")
private Long id;
@Pattern(regexp = "^[a-zA-Z0-9]{4,20}$", groups = Create.class,
message = "아이디는 영문 대소문자, 숫자로 이루어진 4~20자리여야 합니다.")
@NotBlank(groups = Create.class, message = "아이디는 필수 입력 값입니다.")
private String username;
@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,16}", groups = {Create.class, UpdatePassword.class},
message = "비밀번호는 8~16자 영문 대 소문자, 숫자, 특수문자를 사용하세요.")
@NotBlank(groups = {Create.class, UpdatePassword.class}, message = "비밀번호는 필수 입력 값입니다.")
private String password;
@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,16}", groups = {Create.class, UpdatePassword.class},
message = "비밀번호는 8~16자 영문 대 소문자, 숫자, 특수문자를 사용하세요.")
@NotBlank(groups = {Create.class, UpdatePassword.class}, message = "비밀번호는 필수 입력 값입니다.")
private String passwordCheck;
@Pattern(regexp = "^(?:\\w+\\.?)*\\w+@(?:\\w+\\.)+\\w+$", groups = {Create.class, UpdateEmailPhone.class},
message = "이메일 형식이 올바르지 않습니다.")
@NotBlank(groups = {Create.class, UpdateEmailPhone.class}, message = "이메일은 필수 입력 값입니다.")
private String email;
@Pattern(regexp = "^01(?:0|1|[6-9])-(?:\\d{3}|\\d{4})-\\d{4}$", groups = {Create.class, UpdateEmailPhone.class},
message = "전화번호 형식이 올바르지 않습니다.")
@NotBlank(groups = {Create.class, UpdateEmailPhone.class}, message = "전화번호는 필수 입력 값입니다.")
private String phone;
@Pattern(regexp = "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,16}", groups = {UpdatePassword.class},
message = "비밀번호는 8~16자 영문 대 소문자, 숫자, 특수문자를 사용하세요.")
@NotBlank(groups = {Create.class, UpdatePassword.class}, message = "비밀번호는 필수 입력 값입니다.")
private String currentPassword;
// update Form
/**
* 수정 가능 필드
* @param email
* @param phone
*/
public MemberRequestDto(Long id, String email, String phone) {
this.id = id;
this.email = email;
this.phone = phone;
}
/**
* 수정 가능 필드
* @param password
* @param passwordCheck
*
* email, phone 수정 요청 생성자와
* password 수정 요청 생성자는 서로 매개변수의 순서와 구성이 같기 때문에
* 구분이 불가하다
* 따라서 'isPasswordUpdate' 를 매개변수로 추가하여 구분한다.
*/
public MemberRequestDto(Long id, String currentPassword, String password, String passwordCheck, boolean isPasswordUpdate) {
this.id = id;
this.currentPassword = currentPassword;
this.password = password;
this.passwordCheck = passwordCheck;
}
}
여기서 중요한 부분은 2가지 이다.
1. "UpdatePassword" 인터페이스로 묶인 '비밀번호 변경 요청 DTO' 속성
비밀번호 수정 페이지에 있는 것을 보았을때,
입력해야 하는 필드는 3가지 이다.
'현재 비밀번호',
'새 비밀번호',
'새 비밀번호 확인'
이렇게 3가지 이다.
"UpdatePassword" 그룹의 속성은 {"id", "currentPassword", "password", "passwordCheck"} 로 구성되어 있다.
2. '비밀번호 변경 요청 DTO' 생성자
비밀번호 변경 요청 DTO 생성자의 파라미터는 ("Long", "String", "String", "String") 로 구성되어 있다.
생성자를 구분하는 방식은 "파라미터의 순서와 구성" 이다.
기존 생성자에 ("Long", "String", "String", "String") 이 이미 존재하였기 때문에,
boolean type 의 파라미터를 추가하여, 생성자를 구성한다.
2. 비밀번호 수정을 위한 Service 구현
//MemberService.class 中...
//Update Member(회원 정보 수정)
@Transactional
public MemberResponseDto updateMember(MemberRequestDto requestDto) {
//1. member 찾기
Member member = memberRepository.findById(requestDto.getId())
.orElseThrow(() -> new DataNotFoundException("존재하지 않는 회원 입니다."));
//비밀번호 수정 시, password == passwordCheck 확인
if (!(requestDto.getPassword().isEmpty() | requestDto.getPasswordCheck().isEmpty())) {
passwordDoubleCheck(requestDto);
}
// 비밀번호 암호화
passwordEncoding(requestDto);
//2. update member
member.update(requestDto);
//3. member -> MemberResponseDto
return new MemberResponseDto(member);
}
//비밀번호 암호화
public void passwordEncoding(MemberRequestDto dto) {
String encoded = passwordEncoder.encode(dto.getPassword());
dto.setPassword(encoded);
}
//비밀번호 이중 검사
public void passwordDoubleCheck(MemberRequestDto requestDto) {
if (!requestDto.getPassword().equals(requestDto.getPasswordCheck())) {
throw new PasswordCheckFailedException("비밀번호가 동일하지 않습니다.");
}
}
//로그인 인증 로직
public boolean authenticate(String username, String password) {
Optional<Member> findMember = memberRepository.findByUsername(username);
if (findMember.isPresent()) {
return passwordEncoder.matches(password, findMember.get().getPassword());
}
return false;
}
로그인 인증 로직 (authenticate) 은 requestDto 의 currentPassword과 저장된 password 가 서로 매칭되는지를 통해,
현재 비밀번호가 제대로 입력 되었는지를 확인한다.
3. 비밀번호 변경을 위한 Controller 구성
//MemberController.class 中...
/**
* password update
*/
@GetMapping("/pw-update")
public String passwordUpdateForm(Model model) {
Member currentMember = getCurrentMember();
//id, currentPassword, new password, new password check
MemberRequestDto requestDto = new MemberRequestDto(currentMember.getId(), "", "", "", Boolean.TRUE);
model.addAttribute("requestDto", requestDto);
return "members/password-update";
}
requestDto : 비밀번호 수정 요청 DTO (id, currentPassword, password, passwordCheck)
//MemberController.class 中...
@PostMapping("/pw-update")
public String passwordUpdate(@ModelAttribute("requestDto") @Validated(MemberRequestDto.UpdatePassword.class) MemberRequestDto dto,
BindingResult bindingResult, Model model) {
// 현재 로그인된 회원 객체
Member currentMember = getCurrentMember();
//유효성 검사 오류 발생시
if (bindingResult.hasErrors()) {
//id, currentPassword, new password, new password check
MemberRequestDto requestDto = new MemberRequestDto(currentMember.getId(), "", "", "", Boolean.TRUE);
model.addAttribute("requestDto", requestDto);
//에러 메시지 반환
List<String> errorMassage = bindingResult.getAllErrors().stream()
.map(objectError -> objectError.getDefaultMessage())
.collect(Collectors.toList());
model.addAttribute("errorMessage", errorMassage);
return "members/password-update"; // 유효성 검사 실패 시 다시 폼으로
}
//현재 비밀번호 검사
if (memberService.authenticate(currentMember.getUsername(), dto.getPassword())) {
//id, currentPassword, new password, new password check
MemberRequestDto requestDto = new MemberRequestDto(currentMember.getId(), "", "", "", Boolean.TRUE);
model.addAttribute("requestDto", requestDto);
model.addAttribute("errorMessage", "현재 비밀번호가 알맞지 않습니다.");
return "members/password-update";
}
try {
//회원 정보 업데이트
memberService.updateMember(dto);
}
// 중복 검사 오류 시, 에러 처리 로직
catch (DataAlreadyExistsException | PasswordCheckFailedException ex) {
bindingResult.reject("errorMessage", ex.getMessage());
model.addAttribute("errorMessage", ex.getMessage());
return "members/password-update";
}
return "redirect:/members/info";
}
4. 비밀번호 변경을 위한 VIEW
<!--password-update.html 中...-->
<!-- Password Update Form -->
<div class="info-box">
<h2>비밀번호 수정</h2>
<form th:action="@{/members/pw-update}" th:object="${requestDto}" method="post">
<!-- Hidden ID Field -->
<input type="hidden" th:field="*{id}">
<div>
<input id="current-password" th:field="*{currentPassword}" type="password" required>
<label for="current-password">현재 비밀번호</label>
</div>
<div>
<input id="new-password" th:field="*{password}" type="password" required>
<label for="new-password">새 비밀번호</label>
</div>
<div>
<input id="confirm-password" th:field="*{passwordCheck}" type="password" required>
<label for="confirm-password">새 비밀번호 확인</label>
</div>
<div th:if="${errorMessage}" style="color: red;">
<p th:text="${errorMessage}">Error message here</p>
</div>
<button type="submit" style="background: green;">비밀번호 수정</button>
</form>
</div>
5. 완성 화면