사이먼's 코딩노트
[SpringBoot] 회원가입 구현 본문
[회원가입 구현하기]
- 웹 프로그래밍의 꽃이기도 한 회원 가입 기능을 구현해봅시다.
- 먼저 지금까지 구현된 코드의 패키징을 보면 질문을 관리하는 Question, 답변을 관리하는 Answer, 템플릿을 관리하는 templates, 스타일시트를 관리하는 static이 존재한다.
- 회원 가입이나 뒤에 추가할 로그인/로그아웃을 구현하기 위해서는 회원과 관련된 데이터를 저장하고 관리하는 User 패키지를 새로 생성해야한다.
[SiteUser 생성]
- 가장 먼저 회원 정보와 관련된 데이터를 저장하는 엔티티가 필요하기 때문에 SiteUser.java 클래스를 하나 생성하여 아래와 같이 회원 엔티티를 작성한다.
- 여기서 클래스 네임을 User.java가 아닌 SiteUser.java라 하는 이유는 스프링 시큐리티에 이미 User.java 클래스가 존재하기 때문이다.
@Getter
@Setter
@Entity
public class SiteUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String username;
private String password;
@Column(unique = true)
private String email;
}
- 회원 엔티티에는 최소한 사용자 ID, 비밀번호, E-mail이 필요하다.
- username, email 속성에는 @Column(unique = true) 어노테이션을 사용하여 해당 컬럼을 고유키로 설정한다.
- 고유키로 설정하면 값을 중복되게 저장할 수 없으며, 동일한 값이 저장되는 것을 막을 수 있다.
[UserRepository 생성]
- 회원 엔티티를 생성했다면 이제 리포지터리와 서비스를 만들어봅시다.
- 아래는 UserRepository.java 클래스를 생성하여 작성한 코드이다.
package com.sbs.sbb.User;
import com.sbs.sbb.Answer.Answer;
import jakarta.transaction.Transactional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface UserRepository extends JpaRepository<SiteUser, Integer> {
@Transactional
@Modifying
@Query(value="ALTER TABLE site_user AUTO_INCREMENT = 1", nativeQuery = true)
void clearAutoIncrement();
}
- 엔티티에서 정의한 id가 Long 타입이기 때문에 JpaRepository<SiteUser, Long>으로 사용했다.
- clearAutoIncrement() 메서드는 테스트 회원 데이터를 만들기 위해 임시적으로 만든 메서드이다.
[UserService 생성]
- 아래는 UserService.java 클래스를 생성하여 서비스를 활용하기 위해 작성한 코드이다.
package com.sbs.sbb.User;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class UserService {
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
public SiteUser create(String username, String email, String password) {
SiteUser user = new SiteUser();
user.setUsername(username);
user.setEmail(email);
user.setPassword(passwordEncoder.encode(password));
this.userRepository.save(user);
return user;
}
}
- UserService에는 UserRepository를 사용하여 회원 데이터를 생성하는 create() 메서드를 작성하였다.
- 이 때 User의 비밀번호는 보안을 위해 반드시 암호화하여 저장하기 때문에 스프링 시큐리티의 PasswordEncoder 객체를 빈으로 등록하여 비밀번호를 저장하도록 하였다. 암호화된 비밀번호는 복호화는 불가능하다.
- PasswordEncoder는 BCryptPasswordEncoder의 인터페이스로서 비크립트 해시 함수를 사용하는데, 비크립트는해시 함수의 하나로 주로 비밀번호와 같은 보안 정보를 안전하게 저장하고 검증할 때 사용하는 암호화 기술이다.
- PasswordEncoder 빈을 만드는 방법은 스프링 시큐리티가 적용된 SecurityConfig.java 클래스에 @Bean 메서드를 추가하는 것이고, 아래는 해당 코드를 SecurityConfig.java 클래스에 추가한 모습이다.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
[UserCreateForm 생성]
- 이번에는 유효성 체크 역할을 위해 UserCreateForm.java 클래스를 생성하고 아래와 같이 코드를 작성한다.
package com.sbs.sbb.User;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserCreateForm {
@Size(min = 3, max = 25)
@NotBlank(message = "사용자ID는 필수항목입니다.")
private String username;
@NotBlank(message = "비밀번호는 필수항목입니다.")
private String password1;
@NotBlank(message = "비밀번호 확인은 필수항목입니다.")
private String password2;
@NotBlank(message = "이메일은 필수항목입니다.")
@Email
private String email;
}
- username은 @Size 어노테이션을 통해 입력받는 데이터의 길이가 3 ~ 25자 사이여야 한다는 검증 조건을 설정하였다.
- password1과 password2는 '비밀번호'와 '비밀번호 확인'에 대한 속성이다.
- 로그인을 할 때는 비밀번호가 한 번만 필요하지만 회원가입 시에는 입력한 비밀번호가 정확한 지 확인하기 위해 2개의 필드가 필요하므로 위와 같이 작성하였다.
- email 속성에는 @Email 어노테이션을 적용하여 해당 속성 값이 이메일 형식과 일치하는 지 검증한다.
[UserController 생성]
- 회원가입을 위한 엔티티, 서비스, 리포지터리 그리고 폼이 모두 준비되었다면 URL 매핑을 위한 UserController가 필요하다.
- 아래는 UserController.java 클래스를 생성하여 작성한 코드이다.
package com.sbs.sbb.User;
import jakarta.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
@RequestMapping("/user")
public class UserController {
private final UserService userService;
@GetMapping("/signup")
public String signup(UserCreateForm userCreateForm) {
return "signup_form";
}
@PostMapping("/signup")
public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "signup_form";
}
if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
bindingResult.rejectValue("password2", "passwordInCorrect",
"2개의 패스워드가 일치하지 않습니다.");
return "signup_form";
}
userService.create(userCreateForm.getUsername(),
userCreateForm.getEmail(), userCreateForm.getPassword1());
return "redirect:/";
}
}
- @RequestMapping("/user") 어노테이션을 통해 UserController의 모든 URL 매핑 시작은 /user로 시작하도록 설정하였다.
- /user/signup URL이 GET 방식으로 요청되면 회원가입을 위한 signup_form.html 템플릿을 랜더링하고 POST 방식으로 요청되면 회원가입을 진행하도록 하였다.
- UserCreateForm.java 에서 생성한 password1과 password를 통해 '비밀번호'와 '비밀번호 확인'이 일치한지 검증하는 조건문을 추가하였다.
- 만약 2개의 값이 서로 일치하지 않을 경우에는 bindingResult.rejectValue()를 사용하여 입력받은 2개의 비밀번호가 일치하지 않다는 에러가 발생하도록 하였다.
- 마지막으로 userService의 create() 메서드를 호출하여 사용자로부터 전달받은 데이터를 전달한다.
[signup_form.html 템플릿 생성]
- 모든 준비가 완료되었다면, 이제는 실제로 페이지에 회원가입 화면이 표출될 수 있도록 템플릿을 생성해봅시다.
- 아래는 signup_form.html 템플릿을 생성하여 작성한 코드이다.
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<div class="my-3 border-bottom">
<div>
<h4>회원가입</h4>
</div>
</div>
<form th:action="@{/user/signup}" th:object="${userCreateForm}" method="post">
<div th:replace="~{form_errors :: formErrorsFragment}"></div>
<div class="mb-3">
<label for="username" class="form-label">사용자ID</label>
<input type="text" th:field="*{username}" class="form-control">
</div>
<div class="mb-3">
<label for="password1" class="form-label">비밀번호</label>
<input type="password" th:field="*{password1}" class="form-control">
</div>
<div class="mb-3">
<label for="password2" class="form-label">비밀번호 확인</label>
<input type="password" th:field="*{password2}" class="form-control">
</div>
<div class="mb-3">
<label for="email" class="form-label">이메일</label>
<input type="email" th:field="*{email}" class="form-control">
</div>
<button type="submit" class="btn btn-primary">회원가입</button>
</form>
</div>
</html>
- ID, 비밀번호, 비밀번호 확인, 이메일에 해당하는 input 요소들을 추가하여 회원가입 화면에 각각의 필드가 나타나도록 하고, '회원가입' 버튼을 클릭하게 되면 form 데이터가 POST 방식으로 /user/signup URL에 전송된다.
[내비게이션 바에 회원가입 추가]
- 사용자가 매번 브라우저에 접속해서 localhost:8090/user/signup URL을 입력하고 회원가입을 할 수는 없다.
- 사용자가 회원가입을 쉽게 할 수 있도록 회원가입 화면으로 이동할 수 있는 링크를 내비게이션 바에 추가해봅시다.
- 아래는 분리해놨던 navbar.html 템플릿에 추가 작성된 코드이다.
<nav th:fragment="navbarFragment" class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
<div class="container-fluid">
<a class="navbar-brand" href="/">SBB</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" th:href="@{/user/login}">로그인</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/user/signup}">회원가입</a>
</li>
</ul>
</div>
</div>
</nav>
- 회원가입 버튼의 위치는 로그인 버튼의 오른쪽에 배치하도록 li 태그를 추가하였다.
- th:href="@{/user/signup}" 을 통해 '회원가입' 버튼 클릭 시 localhost:8090/user/signup로 링크하였다.
[중복 회원 가입 방지]
- 마지막으로 회원가입 시, 이미 등록된 사용자 ID 또는 이메일 주소로 회원가입을 시도하면 실제로 회원가입이 성공되지는 않지만 화면에서는 500 에러 메시지를 보여주기 때문에 사용자가 봤을 때 좋지 않는 환경이 된다.
- UserCreateForm.java 클래스에는 각 필드의 값이 비어있다는 유효성 체크를 하고, UserController.java 클래스에서는 두 비밀번호가 일치하지 않는다는 에러 메시지를 표시한 것처럼, 회원가입 시 동일한 ID와 이메일 주소가 있다는 것을 알리는 메시지가 나타나도록 해봅시다.
- 아래는 UserController.java 클래스에서 try, catch를 사용하여 예외처리를 추가한 모습이다.
@PostMapping("/signup")
public String signup(@Valid UserCreateForm userCreateForm, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "signup_form";
}
if (!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())) {
bindingResult.rejectValue("password2", "passwordInCorrect",
"2개의 패스워드가 일치하지 않습니다.");
return "signup_form";
}
try {
userService.create(userCreateForm.getUsername(),
userCreateForm.getEmail(), userCreateForm.getPassword1());
} catch(DataIntegrityViolationException e) {
e.printStackTrace();
bindingResult.reject("signupFailed", "이미 등록된 사용자입니다.");
return "signup_form";
} catch(Exception e) {
e.printStackTrace();
bindingResult.reject("signupFailed", e.getMessage());
return "signup_form";
}
return "redirect:/";
}
- 사용자 ID 또는 이메일 주소가 이미 존재할 경우에는 DataIntegrityViolationException이라는 예외가 발생하므로 '이미 등록된 사용자입니다.' 라는 에러 메시지가 화면에 표시되도록 하였다.
- 그 밖에 다른 예외들은 해당 예외에 관한 구체적인 에러 메시지를 출력하도록 e.getMessage() 메서드를 호출하였다.
- 여기서 bindingResult.reject() 메서드는 UsesrCreateForm의 검증에 의한 에러 외에 일반적인 에러를 발생시킬 때 사용한다.
- 위에 작성된 모든 회원가입을 위한 코드 작성을 마쳤다면, 로컬 서버를 다시 실행했을 때, 아래 사진들과 같이 회원가입 페이지가 성공적으로 완성된 것을 확인할 수 있다.
반응형
'Java > SpringBoot' 카테고리의 다른 글
[SpringBoot] 로그아웃 / 작성자 추가 (0) | 2024.05.14 |
---|---|
[SpringBoot] 로그인 구현 (0) | 2024.05.13 |
[SpringBoot] 게시물 번호 정렬 / 답변 개수 표시 (0) | 2024.05.13 |
[SpringBoot] 내비게이션 바 / 페이징 (2) | 2024.05.13 |
[SpringBoot] 유효성 체크 및 에러 처리(2) (0) | 2024.05.12 |