#6 : 회원가입
01. 회원 정보를 위한 엔티티
- 회원에는 username, password, email 속성이 필요
- SiteUser.java 엔티티 생성
package com.mysite.sbb.entity;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Getter
@Setter
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;
}
* 스프링 시큐리티에 이미 User 클래스가 있기 때문에 SiteUser로 명명
* username과 email은 중복될 수 없도록 unique=true 적용
- SiteUser 테이블 확인
* 서버 실행 후 H2 콘솔 접속

* 유니크 설정으로 인해 UK_로 시작하는 인덱스 확인 가능
02. UserRepository와 Service
- UserRepository.java 생성
package com.mysite.sbb.repository;
import com.mysite.sbb.entity.SiteUser;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<SiteUser, Long> {
}
- UserService.java 생성
package com.mysite.sbb.service;
import com.mysite.sbb.entity.SiteUser;
import com.mysite.sbb.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
public SiteUser create(String username, String email, String password){
SiteUser user = new SiteUser();
user.setUsername(username);
user.setEmail(email);
//BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
user.setPassword(password);
this.userRepository.save(user);
return user;
}
}
* 스프링 시큐리티의 기능을 이용하여 비밀번호를 암호화하여 저장한다.
* BCryptPasswordEncoder는 BCrypt 해싱 함수를 사용해 비밀번호를 암호화
* 해당 객체를 직접 new로 생성하기 보다는 PasswordEncoder(BCryptPasswordEncoder의 인터페이스)를 빈으로 등록해서 사용하는 것이 좋다. 암호화 방식을 변경할 시 사용한 모든 프로그램을 일일이 찾아 수정해주어야 하기 때문이다.
* 현재 시큐리티 오류로 임시로 그냥 비밀번호를 저장해두었다. 추후 수정 예정
- SecurityConfig.java에 @Bean 메서드 생성
package com.mysite.sbb;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
.requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
.csrf((csrf) -> csrf
.ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
.headers((headers) -> headers
.addHeaderWriter(new XFrameOptionsHeaderWriter(
XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
;
return http.build();
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
- PasswordEncoder를 Bean으로 등록 시 UserService.java도 수정
package com.mysite.sbb.service;
import com.mysite.sbb.entity.SiteUser;
import com.mysite.sbb.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public SiteUser create(String username, String email, String password){
SiteUser user = new SiteUser();
user.setUsername(username);
user.setEmail(email);
//BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
user.setPassword(passwordEncoder.encoder(password));
//user.setPassword(password);
this.userRepository.save(user);
return user;
}
}
* 빈으로 등록한 객체를 주입받아 사용
03. 회원가입 폼
- 폼 클래스 UserCreateForm.java 생성
package com.mysite.sbb.form;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class UserCreateForm {
@Size(min=3,max=25)
@NotEmpty(message = "id는 필수항목입니다.")
private String username;
@NotEmpty(message = "비밀번호는 필수항목입니다.")
private String password1;
@NotEmpty(message = "비밀번호 확인은 필수항목입니다.")
private String password2;
@NotEmpty(message = "이메일은 필수항목입니다.")
@Email
private String email;
}
- UserController.java 생성
package com.mysite.sbb.controller;
import com.mysite.sbb.form.UserCreateForm;
import com.mysite.sbb.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
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;
@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 result){
if(result.hasErrors()){
return "signup_form";
}
if(!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())){
result.rejectValue("password2","passwordInCorrect",
"2개의 패스워드가 일치하지 않습니다.");
return "signup_form";
}
userService.create(userCreateForm.getUsername(),
userCreateForm.getEmail(), userCreateForm.getPassword1());
return "redirect:/";
}
}
* GET 요청 시 : 회원가입 템플릿 렌더링, POST 요청 시 : 회원가입 진행
* 비밀번호와 비밀번호 확인이 일치하는지 검증하기 위해 result.rejectValue로 오류 발생,
result.rejectValue(필드명, 오류코드, 에러메세지)를 의미한다.
04. 회원가입 템플릿
- signup_form.html 생성
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}">
<div layout:fragment="content" class="container my-3">
<div calss="my-3 border-bottom">
<div>
<h4>회원가입</h4>
</div>
</div>
<form th:action="@{/user/signup}" th:object="${userCreateForm}" method="post">
<div th:replace="~{common/form_errors :: formErrorsFragment}"></div>
<div class="mb-3">
<label for="username" class="form-label">ID</label>
<input type="text" th:field="*{username}" id="username" class="form-control">
</div>
<div class="mb-3">
<label for="password1" class="form-label">비밀번호</label>
<input type="text" th:field="*{password1}" id="password1" class="form-control">
</div>
<div class="mb-3">
<label for="password2" class="form-label">비밀번호 확인</label>
<input type="text" th:field="*{password2}" id="password2" class="form-control">
</div>
<div class="mb-3">
<label for="email" class="form-label">이메일</label>
<input type="text" th:field="*{email}" id="email" class="form-control">
</div>
<button type="submit" class="btn btn-primary">회원가입</button>
</form>
</div>
</html>
* 회원가입 버튼을 누르면 POST 방식으로 요청 호출
05. 내비게이션바에 회원가입 링크 추가
- 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" href="#">로그인</a>
</li>
<li>
<a class="nav-link" th:href="@{/user/signup}">회원가입</a>
</li>
</ul>
</div>
</div>
</nav>
06. 회원가입 실행
- 회원가입 실행


07. 회원가입 데이터 확인
- 회원가입 진행 후 h2 콘솔에서 저장된 데이터 확인

* 비밀번호 암호화가 적용된다면 암호화된 비밀번호가 저장된다.
08. 중복 회원가입
- 이미 가입한 동일한 id나 이메일을 입력 후 가입을 시도하면 500 에러 페이지가 출력된다.
500에러 페이지가 그대로 출력되지 않도록 UserController.java 수정
package com.mysite.sbb.controller;
import com.mysite.sbb.form.UserCreateForm;
import com.mysite.sbb.service.UserService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
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;
@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 result){
if(result.hasErrors()){
return "signup_form";
}
if(!userCreateForm.getPassword1().equals(userCreateForm.getPassword2())){
result.rejectValue("password2","passwordInCorrect",
"2개의 패스워드가 일치하지 않습니다.");
return "signup_form";
}
try {
userService.create(userCreateForm.getUsername(),
userCreateForm.getEmail(), userCreateForm.getPassword1());
}catch (DataIntegrityViolationException e){
e.printStackTrace();
result.reject("signupFailed","이미 등록된 사용자입니다.");
return "signup_form";
}catch (Exception e){
e.printStackTrace();
result.reject("signupFailed",e.getMessage());
return "signup_form";
}
return "redirect:/";
}
}
* 중복된 데이터 입력 시 DataIntegrityViolationException이 발생하므로 예외 발생 시 처리할 사항을 try~catch문으로 지정해준다. 해당 오류가 아닌 오류는 각 오류의 메세지를 출력하도록 한다.
* result.reject(오류코드,오류메세지)는 특정필드의 오류가 아닌 일반적인 오류를 등록할 때 사용한다.

'T-I-L > [책] 요약&정리' 카테고리의 다른 글
| [점프 투 스프링부트] 3장 SBB 서비스 개발(엔티티 변경) - 2023. 09. 01. (0) | 2023.09.01 |
|---|---|
| [점프 투 스프링부트] 3장 SBB 서비스 개발(로그인&로그아웃) - 2023. 08. 29. ~ 2023. 08. 30. (0) | 2023.08.29 |
| [점프 투 스프링부트] 3장 SBB 서비스 개발(스프링 시큐리티) - 2023. 08. 24. (0) | 2023.08.25 |
| [점프 투 스프링부트] 3장 SBB 서비스 개발(일련번호, 답변개수) - 2023. 08. 23. (0) | 2023.08.23 |
| [점프 투 스프링부트] 3장 SBB 서비스 개발(내비게이션바, 페이징) - 2023. 08. 22. (0) | 2023.08.22 |
