본문 바로가기
공부 기록

[스프링 부트로 게시판 만들기] 05. 회원가입

by 매트(Mat) 2021. 7. 20.

회원가입 페이지 만들기

  • 그 전에 application.yml 파일에서 코드 추가하겠습니다.
spring:
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3308/myblog?serverTimezone=Asia/Seoul
    username: azure
    password: azure1234

  jpa:
    open-in-view: true
    hibernate:
      ddl-auto: create
      use-new-id-generate-mappings: false
    show-sql: true
    properties:
      hibernate.format_sql: true

  thymeleaf:
    cache: false
  • spring.thymeleaf.cache : Thymeleaf 파일 수정하고 저장한 후에 브라우저에서 변경된 결과를 확인하기 위한 설정입니다. 즉, 개발할 때 false로 두어 편하게 개발하면 되고, 운영시에는 true로 변경하면 됩니다.
  • 이제 회원가입을 진행하기 위한 회원가입 페이지를 만들도록 하겠습니다.
  • 폴더 경로는 다음과 같이 만들어줍니다.
캡처

user-save.html

<!doctype html>
<html lang="en" class="h-100" xmlns:th="http://www.thymeleaf.org">

<head th:replace="layout/header :: head ('로그인')"></head>

<body class="text-center d-flex flex-column h-100">
<header th:replace="layout/header :: header"></header>

<main class="form-signin">
    <div class="container border rounded flex-md-row mb-4 shadow-sm h-md-250">
        <form>
            <h1 class="h3 m-3 fw-normal">회원가입</h1>

            <div class="form-floating m-3">
                <input type="text" class="form-control" id="username" placeholder="아이디를 입력하세요.">
                <label for="username">아이디</label>
            </div>
            <div class="form-floating m-3">
                <input type="password" class="form-control" id="password" placeholder="패스워드를 입력하세요.">
                <label for="password">패스워드</label>
            </div>
            <div class="form-floating m-3">
                <input type="email" class="form-control" id="email" placeholder="이메일을 입력하세요.">
                <label for="email">이메일</label>
            </div>
            <div class="form-floating m-3">
                <input type="text" class="form-control" id="nickname" placeholder="닉네임을 입력하세요.">
                <label for="nickname">닉네임</label>
            </div>
            <button class="w-100 btn btn-lg btn-primary mb-3" type="submit">회원가입</button>
        </form>
    </div>
</main>

<footer th:replace="layout/footer :: footer"></footer>

<!-- Option 1: Bootstrap Bundle with Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
</body>

</html>

user-save.css

html,
body {
  height: 100%;
}

body {
  display: flex;
  align-items: center;
  padding-top: 40px;
  padding-bottom: 40px;
  background-color: #f5f5f5;
}

.form-signin {
  width: 100%;
  max-width: 330px;
  padding: 15px;
  margin: auto;
}

.form-signin .checkbox {
  font-weight: 400;
}

.form-signin .form-floating:focus-within {
  z-index: 2;
}

.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}

.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

.btn-primary {
    background-color: #15751b;
    border-color: #4caf50;
}

.fw-normal {
    color: green;
}
  • header.html에서 css 경로를 추가해주세요.

header.html

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">

<head th:fragment="head (title)">
    ...
    <link th:href="@{../css/sticky-footer-navbar.css}" rel="stylesheet">
    <link rel="stylesheet" th:href="@{../css/style.css}">
    <link rel="stylesheet" th:href="@{../css/user-save.css}">

    <title th:text="${title}">블로그</title>

</head>
  • header에 link 태그 경로를 변경해주었습니다!

UserController 클래스

package com.azurealstn.blogproject.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class UserController {

    @GetMapping("/auth/user/save")
    public String userSave() {
        return "layout/user/user-save";
    }
}

ajax

  • user-save.html에서 버튼을 form 바깥에다 두겠습니다. 그리고 id값을 주겠습니다.
  • 그리고 ajax 사용을 위해 <script th:src="@{/js/user.js}"></script> 이 script 태그도 추가하겠습니다.
  • 폴더 경로는 이렇습니다.
  • jquery도 추가해주겠습니다.
캡처

user-save.html

<body class="text-center d-flex flex-column h-100">
<header th:replace="layout/header :: header"></header>

<main class="form-signin">
    <div class="container border rounded flex-md-row mb-4 shadow-sm h-md-250">
        <form>
        ...
        </form>
        <button class="w-100 btn btn-lg btn-primary" id="btn-save">회원가입</button>
    </div>
</main>

<footer th:replace="layout/footer :: footer"></footer>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script th:src="@{/js/user.js}"></script>
</body>

ajax를 사용하는 이유

요청에 대한 응답을 .html 파일이 아닌 JSON Data로 받기 위해.
비동기 통신을 하기 위해.

user.js

'use strict';

let index = {
    init: function() {
        $("#btn-save").on("click", () => { //this를 바인딩하기 위해 화샬표 함수 사용
            this.save();
        });
    },

    save: function() {
        let data = { //JavaScript Object
            username: $("#username").val(),
            password: $("#password").val(),
            email: $("#email").val(),
            nickname: $("#nickname").val()
        }

        $.ajax({
            type: "POST", //Http method
            url: "/api/v1/user", //API 주소
            data: JSON.stringify(data), //JSON으로 변환
            contentType: "application/json; charset=utf-8", //MIME 타입
            dataType: "json" //응답 데이터
        }).done(function(res) {
            alert("회원가입이 완료되었습니다.");
            location.href = "/";
        }).fail(function(err) {
            alert(JSON.stringify(err));
        });
    }
}
index.init();

회원가입 테스트

  • 회원가입을 테스트 하기 위해 Repository 인터페이스를 생성해야 합니다.
캡처

UserRepository 인터페이스

package com.azurealstn.blogproject.domain.user;

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

public interface UserRepository extends JpaRepository<User, Long> {
}
  • 흔히 DAO라 불리는 DB 계층 접근자입니다. JpaRepository 인터페이스를 상속받아야 메소드를 사용할 수 있습니다.
  • 여기서 @Repository를 추가하지 않아도 IoC 알아서 관리해줍니다. (DI 사용 가능)
  • JpaRepository<Entity 타입, PK 타입>
  • 테스트 코드 작성하기
캡처

UserRepositoryTest 클래스

package com.azurealstn.blogproject.domain.user;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@SpringBootTest
public class UserRepositoryTest {

    @Autowired
    UserRepository userRepository;

    @AfterEach
    public void cleanup() {
        userRepository.deleteAll();
    }

    @Test
    public void 회원가입_테스트() {
        //given
        String username = "test";
        String nickname = "babo";

        userRepository.save(User.builder()
                .username(username)
                .password("1234")
                .email("test@naver.com")
                .nickname(nickname)
                .role(Role.USER)
                .build());

        //when
        List<User> userList = userRepository.findAll();

        //then
        User user = userList.get(0);
        assertThat(user.getUsername()).isEqualTo(username);
        assertThat(user.getNickname()).isEqualTo(nickname);
    }
}
  • JUnit5에서는 @AfterEach로 사용해야 합니다.
    • 단위 테스트가 끝날 때마다 수행되는 메소드를 지정합니다.
    • 여러 테스트 진행시 데이터가 남아있어서 실패의 원인이 됩니다.
  • userRepository.save를 실행하게 되면 insert 혹은 update 쿼리가 실행됩니다.
  • userRepository.findAll() : 모든 데이터를 조회

이제 구현을 해보도록 하겠습니다.

  • 회원가입 위한 UserSaveRequestDto 클래스를 만들어 줍니다.
  • Entity 클래스는 DB와 매우 밀접한 관계이기 때문에 Request/Response할 때는 따로 Dto 클래스를 만들어주는 것이 좋습니다.
캡처

UserSaveRequestDto 클래스

package com.azurealstn.blogproject.dto.user;

import com.azurealstn.blogproject.domain.user.Role;
import com.azurealstn.blogproject.domain.user.User;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Builder
@AllArgsConstructor
@Getter
@NoArgsConstructor
public class UserSaveRequestDto {

    private String username;
    private String password;
    private String email;
    private String nickname;
    private Role role;

    public User toEntity() {
        return User.builder()
                .username(username)
                .password(password)
                .email(email)
                .nickname(nickname)
                .role(Role.USER)
                .build();
    }
}
캡처

UserService 클래스

package com.azurealstn.blogproject.service;

import com.azurealstn.blogproject.domain.user.User;
import com.azurealstn.blogproject.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;

    @Transactional
    public Long save(User user) {
        return userRepository.save(user).getId();
    }
}
  • private final UserRepository userRepository : 생성자 주입을 받기 위해 @RequiredArgsConstructor 어노테이션을 썼습니다.

UserApiController 클래스

package com.azurealstn.blogproject.controller.api;

import com.azurealstn.blogproject.dto.user.UserSaveRequestDto;
import com.azurealstn.blogproject.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class UserApiController {

    private final UserService userService;

    @PostMapping("/api/v1/user")
    public Long save(@RequestBody UserSaveRequestDto userSaveRequestDto) {
        return userService.save(userSaveRequestDto.toEntity());
    }
}

테스트 코드 작성

캡처

UserApiControllerTest 클래스

package com.azurealstn.blogproject.controller.api;

import com.azurealstn.blogproject.domain.user.Role;
import com.azurealstn.blogproject.domain.user.User;
import com.azurealstn.blogproject.domain.user.UserRepository;
import com.azurealstn.blogproject.dto.user.UserSaveRequestDto;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserApiControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @AfterEach
    public void cleanup() throws Exception {
        userRepository.deleteAll();
    }

    @Test
    public void User_가입완료_테스트() throws Exception {
        //given
        String username = "test";
        String nickname = "babo";

        UserSaveRequestDto userSaveRequestDto = UserSaveRequestDto.builder()
                .username(username)
                .password("1234")
                .email("test@naver.com")
                .nickname(nickname)
                .role(Role.USER)
                .build();

        String url = "http://localhost:" + port + "/api/v1/user";

        //when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, userSaveRequestDto, Long.class);

        //then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<User> userList = userRepository.findAll();
        assertThat(userList.get(0).getUsername()).isEqualTo(username);
        assertThat(userList.get(0).getNickname()).isEqualTo(nickname);
    }
}
  • API 테스트시에는 @WebMvcTest를 사용하지 않고 @SpringBootTest와 TestRestTemplate을 이용합니다. 그 이유는 JPA 기능까지 테스트 하기 위해서입니다.

다음으로

  • 프로젝트 실행해서 웹 사이트에서 회원가입 정보 입력해서 DB에 제대로 데이터가 들어왔는지 까지 확인하세요!
  • 테스트 전체 실행해주시고 체크 표시✔가 떴다면 일단 여기까지 하겠습니다.
  • 다음에는 아직 회원가입할 때 패스워드 암호화와 회원 수정해보겠습니다. 이 때는 스프링 시큐리티를 이용하려고 합니다.

References

댓글