스프링과 JPA 기반 웹 애플리케이션 개발 #36 닉네임 수정
해당 내용은 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발의 강의 내용을 바탕으로 작성된 내용입니다.
강의를 학습하며 요약한 내용을 출처를 표기하고 블로깅 또는 문서로 공개하는 것을 허용합니다 라는 원칙 하에 요약 내용을 공개합니다. 출처는 위에 언급되어있듯, 인프런, 스프링과 JPA 기반 웹 애플리케이션 개발입니다.
제가 학습한 소스코드는 https://github.com/n00nietzsche/jakestudy_webapp 에 지속적으로 업로드 됩니다. 매 커밋 메세지에 강의의 어디 부분까지 진행됐는지 기록해놓겠습니다.
@Data
public class NicknameForm {
@NotBlank
@Length(min = 3, max = 20)
@Pattern(regexp = "^[ㄱ-ㅎ가-힣a-z0-9_-]{3,20}$")
private String nickname;
}
Form Backing Object
는 나중에 스프링 MVC의 @ModelAttribute
로 활용되어 데이터의 기본적인 유효성 검증 및 뷰와의 연동에 도움을 준다. 여기서 @NotBlank
, @Length
, @Pattern
등의 검증을 할 수 있다. 스프링 MVC의 아규먼트로서 이용할 때는 반드시 앞에 @Valid
애노테이션을 붙여주어야 한다.
@Component
@RequiredArgsConstructor
public class NicknameFormValidator implements Validator {
private final AccountRepository accountRepository;
@Override
public boolean supports(Class<?> clazz) {
return NicknameForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
NicknameForm nicknameForm = (NicknameForm) target;
if(accountRepository.existsByNickname(nicknameForm.getNickname())){
errors.rejectValue("nickname", "invalid.nickname", new Object[]{nicknameForm.getNickname()}, "이미 사용중인 닉네임입니다.");
}
}
}
Validator
는 이전에 Form Backing Object
와 같은 일반적인 클래스에서 할 수 없고, 빈에서 할 수 있는 검증들을 해주면 된다. supports()
는 해당 클래스에 .isAssianbleFrom()
메소드를 넣어서 위와 같이 작성해주면 된다. Validator
는 웹 데이터를 바인드할 때 검증해주는 단계이다.
빈으로 하는 검증을 하는 만큼 AccountRepository
빈을 이용하여 데이터 중복 검사를 해주었다. 중복된 닉네임이 있는 경우 닉네임 변경을 허락하지 않는다. 중복된 닉네임이 있는 경우 errors.rejectValue()
와 같은 메소드로 반환을 해주게 되면, 해당 에러를 추후에 컨트롤러에서 Errors
객체로 받을 수 있다.
문서에서 필드명
, 에러코드
, 에러 인자
, 기본 메세지
등을 입력하라고 잘 설명이 되어있다.
@InitBinder("nicknameForm")
public void initBinderNicknameForm(WebDataBinder webDataBinder) {
webDataBinder.addValidators(nicknameFormValidator);
}
애노테이트된 핸들로 메소드의 커멘드나 폼 오브젝트 인자를 바인딩할 때의 WebDataBinder
객체를 이용할 수 있다. WebDataBinder
의 .addValidators()
메소드를 통해 우리가 만든 Validator
를 넣어주면, Validator
에 따른 검증이 들어간다.
@GetMapping(ACCOUNT_MAPPING_PATH)
public String updateAccountForm(@LoginAccount Account loginAccount, Model model) {
model.addAttribute(loginAccount);
model.addAttribute(new NicknameForm());
return ACCOUNT_MAPPING_PATH;
}
@PostMapping(NICKNAME_MAPPING_PATH)
public String updateNickname(@LoginAccount Account loginAccount, @Valid @ModelAttribute NicknameForm nicknameForm, Errors errors, Model model) {
if(errors.hasErrors()) {
model.addAttribute(loginAccount);
return ACCOUNT_MAPPING_PATH;
}
redirectAttributes.addFlashAttribute("message", "닉네임이 변경되었습니다.");
accountService.updateNickname(loginAccount, nicknameForm.getNickname());
model.addAttribute(loginAccount);
return "redirect:/" + ACCOUNT_MAPPING_PATH;
}
GPRG 로직은 Get -> Post -> Redirect -> Get으로 이어지는 HTTP 요청을 줄여본 것이다. 스프링 MVC 컨트롤러에서 폼데이터를 처리하기 위한 순서는 먼저
Model
을 이용하여 addAttribute(new Form())
과 같은 형식으로 Form
을 내려주어, th:object="${form}"
에 사용될 수 있게 해준다.@Valid @ModelAttribute
애노테이션을 이용하여 검증해주고, 해당 데이터를 받아 처리해준다.Service
레이어에 위임하고 Redirect:/
로 기존의 Form 페이지 혹은 다른 페이지로 이동한다.RedirectAttributes.addFlashAttribute()
등을 이용하여 메세지를 전달할 수 있다.Redirect:/
를 받고 마지막에 다른 페이지로 GET
메소드를 통해 이동하게 된다.여기서 비즈니스 로직은 폼에 데이터를 뿌려주고, 폼을 이용해 데이터를 받고 닉네임을 업데이트하는 부분이 들어갔다. 닉네임을 업데이트하는 것과 같이 DB 데이터에 변화가 일어나는 부분은 반드시 서비스쪽으로 로직을 빼주자.
public void updateNickname(Account account, String nickname) {
// 닉네임을 변경할 때는, Authentication 의 Username 이 변하는 케이스 이므로, 다시 재로그인을 해줌
// NavBar 에서 쓰여서 Authentication 을 바꾸어주어야 뷰에 올바르게 반영됨
account.setNickname(nickname);
}
기존 강의에서는 닉네임을 업데이트하고,
login()
을 다시 수행해서 변경된 정보로 다시 로그인되도록 해준다. 왜냐하면 뷰단의 네비게이션에서 계속SecurityContext
내부Authentication
에 있는 데이터를 갖다 쓰는데, 유저네임 부분은 업데이트 되지 않기 때문이다.
그런데 나는 애초에 다른 방식으로 설계를 구성해서, 다시 로그인하지 않는다 단, HTML에서 데이터를 빼올 때 조금 다른 방식으로 빼온다.
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>계정 설정</title>
<th:block th:replace="fragments :: headLibraryInjection"></th:block>
</head>
<body class="bg-light">
<th:block th:replace="fragments :: main-nav"></th:block>
<div th:replace="fragments :: email-verify-alarm"></div>
<div class="container">
<!-- row의 기본은 col-12만큼의 크기를 갖는다.-->
<div class="row mt-5 justify-content-center">
<div class="col-2">
<div th:replace="fragments :: settings-menu(currentMenu='account')"></div>
</div>
<div class="col-8">
<div th:if="${message}" class="alert alert-info alert-dismissible fade show mt-3" role="alert">
<span th:text="${message}">메시지</span>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">x</span>
</button>
</div>
<div class="row">
<h2 class="col-sm-12">닉네임 변경</h2>
</div>
<div class="row mt-3">
<form class="needs-validation col-12" action="#" th:action="@{/settings/account/nickname}"
th:object="${nicknameForm}" method="post" novalidate>
<div class="form-group">
<label for="newPassword">닉네임</label>
<input id="newPassword" type="text" th:field="*{nickname}"
class="form-control" aria-describedby="nicknameHelp" required min="3" max="20">
<small id="newPasswordHelp" class="form-text text-muted">
공백없이 문자와 숫자로만 3자 이상 20자 이내로 입력하세요. 가입 후에 변경할 수 있습니다.
</small>
<small class="invalid-feedback">닉네임을 입력하세요.</small>
<small class="form-text text-danger" th:if="${#fields.hasErrors('nickname')}"
th:errors="*{nickname}">
닉네임 형식을 맞춰주세요.
</small>
</div>
<div class="form-group">
<button class="btn btn-primary btn-block" type="submit"
aria-describedby="submitHelp">수정하기
</button>
</div>
</form>
</div>
</div>
</div>
<th:block th:replace="fragments :: footer"></th:block>
</div>
<script th:replace="fragments :: form-validation"></script>
</body>
</html>
여기서 일반 html의 서브밋과 다르게 중요한 건 th:object="${nicknameForm}"
를 이용하여 폼을 매핑했다는 것이다. 매핑한 요소들은 th:field="*{nickname}"
과 같은 방식 혹은 th:error="*{nickname}"
과 같은 방식을 이용하여 서버로부터 내려오는 값을 받거나 서버로 값을 올릴 수 있다.
input
에 name
속성을 넣지 않았는데,
name
속성이 잘 들어가있는 것을 확인할 수 있다 이는 모두 th:field
가 한 일이다.
위와 같이 th:text="${#authentication.getPrincipal().getAccount().getNickname()}"
를 이용하였다. 나는 SecurityContext
에 Username
부분에는 변하지 않는 email
을 넣었고, 해당 id를 기억해서 authentication
에 대한 정보가 필요할 때는 매번 불러오도록 했다.
그래서 detached
된 객체를 불러오지 않고, 강의처럼 login()
을 한번 더 안해도 된다. 또한 로그인한 계정 정보가 변할 때, 매번 .save()
도 안해도 된다.