Spring Boot/STUDY

[Spring Boot] JPA 프로젝트 - 회원가입 기능 구현

코맹 2024. 6. 21. 11:20

 

 

H2에서 Oracle로 변경한 후 회원가입 기능을 구현해보려고 한다.

 

/entity/Member.java 생성
package com.eunji.backboard.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Data;

@Data
@Entity
@Builder
public class Member {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long mid;

  @Column(unique = true, length = 100)
  private String username;

  @Column(unique = true, length = 150)
  private String email;

  private String password;
  
}

 

 

/repository/MemberRepository.java 인터페이스 생성
package com.eunji.backboard.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.eunji.backboard.entity.Member;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long>{
  
}

 

 

/service/MemberService.java 생성

 

  • setMember() 메서드 작성
package com.eunji.backboard.service;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.eunji.backboard.entity.Member;
import com.eunji.backboard.repository.MemberRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class MemberService {

  private final MemberRepository memberRepository;

  public Member setMember(String username, String email, String password) {
    Member member = Member.builder().username(username).email(email).build();

    BCryptPasswordEncoder pwdEncoder = new BCryptPasswordEncoder();
    member.setPassword(pwdEncoder.encode(password));    // 암호화한 값을 DB에 저장
    this.memberRepository.save(member);

    return member;

  }  
  
}

 

💥 MemberService에 BcryptPasswordEncoder() 객체를 매번 새롭게 생성했으나, 이것보다는 Bean 등록해놓고  쓰는 게 유지보수를 위해 더 좋을 것 같아서 /security/SecurityConfig.java에 Bean을 생성해주겠다!

/security/SecurityConfig.java
@Bean
  PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();   // 암호화 빈으로 생성
  }

 

 변경된 MemberService.java
package com.eunji.backboard.service;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import com.eunji.backboard.entity.Member;
import com.eunji.backboard.repository.MemberRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class MemberService {

  private final MemberRepository memberRepository;
  private final PasswordEncoder passwordEncoder;

  public Member setMember(String username, String email, String password) {
    Member member = Member.builder().username(username).email(email).build();

    // BCryptPasswordEncoder 매번 새롭게 객체를 생성한다.
    // 이것보다는 Bean 등록해놓고 쓰는 게 유지보수를 위해서 더 좋음
    // BCryptPasswordEncoder pwdEncoder = new BCryptPasswordEncoder();   
    member.setPassword(passwordEncoder.encode(password));    // 암호화한 값을 DB에 저장
    this.memberRepository.save(member);

    return member;

  }  
  
}

 


회원가입시 빈 값이 들어가지 않도록 유효성 검사를 해야 함

/entity/Member.java 수정
package com.eunji.backboard.entity;

import java.time.LocalDateTime;

import org.springframework.data.annotation.CreatedDate;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Data;

@Data
@Entity
@Builder
public class Member {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long mid;

  @Column(unique = true, length = 100)
  private String username;

  @Column(unique = true, length = 150)
  private String email;

  private String password;

  @CreatedDate
  @Column(name = "regDate", updatable = false)
  private LocalDateTime regDate;  // 회원가입일
  
}

 

 

유효성 검사를 하기 위한 /validation/MemberForm.java 생성
package com.eunji.backboard.validation;

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 MemberForm {
  @Size(min = 4, max = 40)
  @NotBlank(message = "사용자 이름은 필수입니다.")
  private String username;

  @Email
  @NotBlank(message = "이메일은 필수입니다.")
  private String email;
  
  @NotBlank(message = "비밀번호는 필수입니다.")
  private String password1;

  @NotBlank(message = "비밀번호 확인은 필수입니다.")
  private String password2;

}

 

 

/controller/MemberController.java
  @GetMapping("/register")
  public String register(MemberForm memberForm) {
      return "member/register";   // templates/member/register.html
  }

  @PostMapping("/register")
  public String register(@Valid MemberForm memberForm, BindingResult bindingResult) {
    if(bindingResult.hasErrors()) {
      return "member/register";

    }  

    if(!memberForm.getPassword1().equals(memberForm.getPassword2())) {
      bindingResult.rejectValue("password2", "passwordInCorrect", "패스워드가 일치하지 않습니다.");

      return "member/register";

    }
    
    this.memberService.setMember(memberForm.getUsername(), memberForm.getEmail(), memberForm.getPassword1());

    return "redirect:/";
  }
  • 유효성 검사를 위해 @GetMapping 메서드에서 MemberForm을 파라미터로 보낸다.
  • 이후 @PostMapping 메서드에서 빈 값이 있는지 비교
  • 다음으로 password1과 password2가 일치하는지 여부 확인
  • 모든 if문을 통과하면 회원가입 성공

 

/templates/member/register.html 생성
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" layout:decorate="~{layout}">
  <div layout:fragment="main-content" class="card container my-3 form-register">
    <div class="my-3 border-bottom">
      <h4>회원가입</h4>
    </div>
    <form th:action="@{/member/register}" th:object="${memberForm}" method="post" >
      <!-- /templates/errors.html -->
      <div th:replace="~{errors :: formErrorFragment}"></div>
      <div class="mb-3">
        <label for="username" class="form-lable">사용자이름</label>
        <input type="text" th:field="*{username}" class="form-control">
      </div>
      <div class="mb-3">
        <label for="email" class="form-lable">이메일</label>
        <input type="text" th:field="*{email}" class="form-control">
      </div>
      <div class="mb-3">
        <label for="password1" class="form-lable">비밀번호</label>
        <input type="password" th:field="*{password1}" class="form-control">
      </div>
      <div class="mb-3">
        <label for="password2" class="form-lable">비밀번호 확인</label>
        <input type="password" th:field="*{password2}" class="form-control">
      </div>

      <div class="d-flex justify-content-center">
        <button type="submit" class="btn btn-sm btn-primary mx-2">회원가입</button>
        <a href="/" class="btn btn-sm btn-secondary">취소</a>
      </div>
    </form>
  </div>
</html>
  • @Getmapping에서 보낸 memberForm을 th:object=${ memberForm}에 넣어준다.
  • 유효성 검사시 /validation/MemberForm.java에 message로 적어준 에러메시지를 띄우기 위해 th:replace="~{errors::formErrorFragment}" 추가

 

회원가입 기능이 되는지 확인

  •  DB 확인 결과 값이 잘 들어오는 것을 확인할 수 있다.

회원가입이 중복으로 되는 것을 막기 위해 유효성 검사를 하겠다.

 

/controller/MemberController.java @PostMapping register()에 중복회원가입 방지 추가
// 중복 사용자 처리
    try{
      this.memberService.setMember(memberForm.getUsername(), memberForm.getEmail(), memberForm.getPassword1());

    } catch(DataIntegrityViolationException e) {
      e.printStackTrace();
      bindingResult.reject("registerFailed", "이미 등록된 사용자입니다.");
      
      return "member/register";

    } catch(Exception e) {
      e.printStackTrace();
      bindingResult.reject("registerFailed", e.getMessage());

      return "member/register";

    }
  • db에 select 하지 않고 등록할 때 insert가 되지 않도록 함

 

전체 / controller/MemberController.java 코드
package com.eunji.backboard.controller;

import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;

import com.eunji.backboard.service.MemberService;
import com.eunji.backboard.validation.MemberForm;

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@RequestMapping("/member")
@Controller
@RequiredArgsConstructor
public class MemberController {
  private final MemberService memberService;

  @GetMapping("/register")
  public String register(MemberForm memberForm) {
      return "member/register";   // templates/member/register.html
  }

  @PostMapping("/register")
  public String register(@Valid MemberForm memberForm, BindingResult bindingResult) {
    if(bindingResult.hasErrors()) {
      return "member/register";

    }  

    if(!memberForm.getPassword1().equals(memberForm.getPassword2())) {
      bindingResult.rejectValue("password2", "passwordInCorrect", "패스워드가 일치하지 않습니다.");

      return "member/register";

    }

    // 중복 사용자 처리
    try{
      this.memberService.setMember(memberForm.getUsername(), memberForm.getEmail(), memberForm.getPassword1());

    } catch(DataIntegrityViolationException e) {
      e.printStackTrace();
      bindingResult.reject("registerFailed", "이미 등록된 사용자입니다.");
      
      return "member/register";

    } catch(Exception e) {
      e.printStackTrace();
      bindingResult.reject("registerFailed", e.getMessage());

      return "member/register";

    }
    return "redirect:/";

  }
  
  

}

 

결과화면

  • 동일한 사용자 이름, 이메일을 입력하게 되면 위 메세지를 띄운다.