본문 바로가기
Dev/SpringBoot

6. [springboot] Spring boot 기초 회원가입 예제

by VIPeveloper 2020. 6. 9.
반응형

1. 서론

스프링 시큐리티를 이용하여 권한을 부여하고, 회원가입 처리를 하는 간단 예제를 만들어 볼 것입니다.

 

이 글을 이해하면 간단한 회원가입을 스프링 시큐리티로 구현할 수 있습니다.

 

간단한 MVC 구조를 알고 계신다면 더 편하게 따라오실 수 있습니다.

2. 본론

전체구조입니다.

엔티티 모델링

먼저, 원활한 회원가입을 위한 엔티티 모델링부터 진행합니다.

 

여기서 주의할 점은 username 부분인데, 시큐리티에서 템플릿과 연동 시 기본으로 제공하는 name 값이 username 이므로 이에 맞추어 줍니다. 커스터마이징 또한 따로 할 수 있으나 추후 포스팅하겠습니다.

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Data
// 다른 패키지에서 생성자 함부로 생성하지 마세요!
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Account {

    @Id @Column(name = "user_id")
    // SQL 에서 자동생성되도록 돕는 어노테이션
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;
    private String password;
    private String email;
    private String age;
    private String role;

    @Builder
    public Account(Long id, String username, String password, String email, String age, String role) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.age = age;
        this.role = role;
    }
}

 

레파지토리 모델링

엔티티를 바탕으로 repository 클래스를 만들어줍니다.

package com.example.springsecurity.repository;

import com.example.springsecurity.domain.Account;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface AccountRepository extends JpaRepository<Account,Long> {
}

- 이를 사용하면 JpaRepository에서 제공하는 여러 함수들을 이용할 수 있게 됩니다.

- 자세한 것은 추후 포스팅하겠습니다.

 

서비스 모델링

- 서비스 클래스를 만들어 Transaction 처리할 수 있는 환경을 만들어줍니다. @Transactional 어노테이션은 함수가 불의의 사고로 구동 실패 시 Rollback 할 수 있도록 안전장치하는 어노테이션이므로 로직 처리는 여기에서 진행해줍니다.

package com.example.springsecurity.service;

import com.example.springsecurity.domain.Account;
import com.example.springsecurity.dto.AccountForm;
import com.example.springsecurity.repository.AccountRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AccountService {

    private final AccountRepository accountRepository;

    @Transactional
    public Long createUser(AccountForm form) {
        Account account = form.toEntity();
        accountRepository.save(account);
        return account.getId();
    }
}

- 여기서 주의할 점은 AccountForm 이라는 DTO 객체가 나와있다는 점입니다. Entity 가 있는데 왜 굳이 DTO를 만들어 toEntity() 처리를 해주어야 하실지 궁금하실 것입니다. 이에 관해서 추후 포스팅하겠습니다.

 

- save(객체) 구문은 JpaRepository에서 제공하는 함수입니다. entityManager.persist()로 DB에 실제적으로 저장시켜줍니다.

 

DTO 모델링

package com.example.springsecurity.dto;

import com.example.springsecurity.domain.Account;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@NoArgsConstructor
public class AccountForm {

    private Long id;
    private String username;
    private String password;
    private String email;
    private String age;
    private String role;

    @Builder
    public AccountForm(Long id, String username, String password, String email, String age, String role) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.age = age;
        this.role = role;
    }

    public Account toEntity(){
        return Account.builder()
                .id(id)
                .username(username)
                .password(new BCryptPasswordEncoder().encode(password))
                .email(email)
                .age(age)
                .role(role)
                .build();
    }
}

 

- 여기서 주의해서 볼 점은 toEntity() 부분입니다. 스프링 시큐리티에서 제공하는 BCryptPasswordEncoder를 이용하여 사용자 비밀번호를 암호화합니다.

 

컨트롤러 모델링

package com.example.springsecurity.controller;

import com.example.springsecurity.dto.AccountForm;
import com.example.springsecurity.service.AccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import javax.validation.Valid;

@Controller
@RequiredArgsConstructor
public class UserController {

    private final AccountService accountService;

    @GetMapping("/loginUser")
    public String createUserForm(Model model){
        model.addAttribute("userForm",new AccountForm());
        return "user/login/register";
    }

    @PostMapping("/loginUser")
    public String createUser(@Valid AccountForm form, BindingResult result){
        if(result.hasErrors()){
            return "user/login/register";
        }
        accountService.createUser(form);

        return "redirect:/";
    }
}

 

- 주의해서 볼 점은 둘 다 "/loginUser" 임에도 불구하고 @GetMapping 인지, @PostMapping 인지에 따라 다른 함수가 실행된다는 것입니다. 자세한 내용은 get, post 방식을 선행 학습하시면 됩니다.

 

- get 방식으로 접속 시, userForm이라는 객체를 HTML에 전달해주게 됩니다. 

 

- post 방식으로 접속 시, 회원가입 처리를 하게 됩니다.

- BindingResult는 @Valid 어노테이션에서 문제가 생겼을 경우, 처리를 돕는 기본 제공 로직입니다. 

 

- 회원가입이 완료되지 않아 문제가 생겼을 경우 다시 회원가입 페이지로 가도록 하였고, 성공 시 redirect를 사용하여 루트 페이지로 오게 만들었습니다.

프런트엔드 로직

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"></head>
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader"></div>
    <h1>회원가입</h1>
    <form role="form" th:action="@{/loginUser}" th:object="${userForm}" method="post">
        <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

        <div class="form-group">
            <label th:for="username">아이디</label>
            <input type="text" th:field="*{username}" class="form-control" placeholder="아이디 입력해주세요">
        </div>
        <div class="form-group">
            <label th:for="password">비밀번호</label>
            <input type="password" class="form-control" th:field="*{password}" placeholder="비밀번호">
        </div>
        <div class="form-group">
            <label th:for="email">이메일</label>
            <input type="email" th:field="*{email}" class="form-control" placeholder="이메일 입력해주세요">
        </div>
        <div class="form-group">
            <label th:for="age">나이</label>
            <input class="form-control" placeholder="나이를 입력하세요." th:field="*{age}" type="number">
        </div>
        <div class="form-group">
            <label th:for="role">권한</label>
            <input class="form-control" placeholder="권한 입력하세요." th:field="*{role}" type="text">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    <br/>
    <div th:replace="fragments/footer :: footer"></div>
</div>
</body>
</html>

 

- 저 같은 경우 fragments 내부에 각각의 템플릿 파일들을 삽입하여 꾸며보았습니다.

- 지금 포스팅에서는 설명하지 않겠지만, 관심 있으시다면 thymeleaf th:replace를 검색하여 공부하시면 좋을 것 같습니다.

 

- 타임리프 문법 중에 th:object = ${userForm}이라는 객체로 controller에서 매핑한 이름을 가지고 받아왔습니다. 옵젝으로 받은 하위 변수에는 ${} 이 아닌 *{}으로 변수를 할당한다는 것에 유의하세요.

 

- th:field는 name + id입니다.

 

3. 결론

회원가입 기능 추가된 front

- 회원가입 기능을 추가하였습니다.

 

회원가입 예제

클릭 시 해당 회원가입을 진행하게 됩니다.

 

h2

 

회원가입 완료 후 데이터가 잘 저장된 것을 볼 수 있습니다.

password는 아까 설명드린 곳에서 보았듯, 암호화되어 저장되어있습니다.

 

4. 마무리

- 이상으로 간단한 회원가입 예제를 구현해보았습니다. 이어서 custom 로그인을 구현해보는 예제를 포스팅해볼까 합니다.

 

+) version 2.0을 만들었습니다. 

반응형