[스프링 부트/ Spring Boot] 스프링 게시판 만들기 - 부트로 쉽게 구현한 Spring 게시판

2021. 3. 25. 02:12 Spring Framework/Spring boot2

| 스프링 게시판 만들기 - 부트로 쉽게 구현한 Spring 게시판

 

예제 git repository는 여기를 클릭하시면 됩니다.

 

스프링 게시판은 스프링 MVC로 스프링 부트에서 밀고있는 툴인 Thymeleaf를 사용하여 쉽게 만들 수 있습니다. REST API + SPA( React, Vue 등 )으로 만들 수 있지만 간단한 커뮤니티 사이트 구현을 위해서는 조금 과한 기술스택을 사용하는 것이 아닌 지 생각해 봐야 합니다.

 

스프링 MVC를 사용했을 때의 데이터 흐름은 아래의 링크를 참고하여 보시면 될 것 같습니다.

 

[Spring Framework/Spring 입문 - 개념 및 핵심] - [Spring] 스프링(Spring) MVC 아키텍처/설계 구조

 

위에서 JSP를 Thymeleaf라고 생각하고 읽으시면 스프링 부트 MVC에서의 데이터가 어떻게 흘러가는 지 알 수 있습니다.

 

요구 사항

 

인텔리제이 (Intellij) ( 이클립스로도 무방 )

Gradle 4 버전

Java 1.8 이상

 

| 스프링 게시판 프로젝트 

 

프로젝트 구조

\---src
+---main
|   +---java
|   |   \---com
|   |       \---tutorial
|   |           \---springboard
|   |               |   AppRunner.java
|   |               |   SpringBoardApplication.java
|   |               |
|   |               +---config
|   |               +---controller
|   |               |       BoardController.java
|   |               |
|   |               +---domain
|   |               |   |   Board.java
|   |               |   |   User.java
|   |               |   |
|   |               |   \---enums
|   |               |           BoardType.java
|   |               |
|   |               +---repository
|   |               |       BoardRepository.java
|   |               |       UserRepository.java
|   |               |
|   |               \---service
|   |                       BoardService.java
|   |
|   \---resources
|       |   application.yml
|       |
|       +---static
|       |   +---css
|       |   +---images
|       |   \---js
|       |
|       \---templates
|           \---board
|                   list.html

 

의존성 관리

 

Gradle

plugins {
    id 'org.springframework.boot' version '2.1.5.RELEASE'
    id 'java'
}

apply plugin: 'io.spring.dependency-management'

group = 'com.tutorial'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'org.springframework.boot:spring-boot-devtools'
    compileOnly 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

 

소스 코드

import com.tutorial.springboard.domain.enums.BoardType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.LocalDateTime;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table
@Builder
public class Board {

    @Id
    @Column
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long idx;

    @Column
    private String title;

    @Column
    private String subTitle;

    @Column
    private String content;

    @Column
    @Enumerated(EnumType.STRING)
    private BoardType boardType;

    @Column
    private LocalDateTime createdDate;

    @Column
    private LocalDateTime updatedDate;

    @OneToOne(fetch = FetchType.LAZY)
    private User user;

}
package com.tutorial.springboard.domain;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;

@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table
@Builder
public class User implements Serializable {

    @Id
    @Column
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long idx;

    @Column
    public String name;

    @Column
    public String password;

    @Column
    public String email;

    @Column
    public LocalDateTime createdDate;

    @Column
    public LocalDateTime updatedDate;

}

 

  • 유저정보를 나타내는 User 클래스와 게시글을 나타내는 Board 클래스입니다.
  • @GeneratedValue는 자동적으로 idx의 값을 할당해주는 어노테이션입니다. 여기서는 전체 DB 범위로 아이디의 값을 관리하는 GenerationType.IDENTITY 옵션을 썼습니다.
import com.tutorial.springboard.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    User findByEmail(String email);
}
import com.tutorial.springboard.domain.Board;
import com.tutorial.springboard.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardRepository extends JpaRepository<Board, Long> {
    Board findByUser(User user);
}

 

import com.tutorial.springboard.domain.Board;
import com.tutorial.springboard.service.BoardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
@RequestMapping("/board")
public class BoardController {

    @Autowired
    BoardService boardService;

    @GetMapping({"", "/"})
    public String board(@RequestParam(value="idx", defaultValue = "0") Long idx,
                        Model model) {
        model.addAttribute("board", boardService.findBoardByIdx(idx));
        return "/board/form";
    }

    @GetMapping("/list")
    public String list(@PageableDefault Pageable pageable, Model model) {
        Page<Board> boardList = boardService.findBoardList(pageable);
        boardList.stream().forEach(e -> e.getContent());
        model.addAttribute("boardList", boardList);

        return "/board/list";
    }

}

 

  • BoardController는 /board URL을 매핑하며 /board/list 을 관리합니다. 
  • /board/list 는 게시글의 데이터를 FETCH하는 데 쓰이며 서비스 계층인 BoardService에서 해당 페이징 처리를 하게 됩니다.
  • Pageable은 페이지 요청을 받을 수 있는 인터페이스입니다. 보통 Pageable의 구현체인 PageRequest의 인스턴스를 받아 이 안에 있는 데이터를 가지고 페이징 관련 처리를 하게 됩니다.
  • Model은 View 층에서 JSP나 Thymeleaf와 같은 템플릿 엔진이 동적으로 HTML 페이지를 만드는 데 필요한 데이터를 제공해줍니다. 위와 같은 경우 boardList 속성 혹은 board 속성에 대한 데이터를 model에 추가하는 것을 볼 수 있습니다. 이 데이터는 뒤에서 보실 Thymeleaf 코드에서 게시글을 만들 때 쓰여집니다.

import com.tutorial.springboard.domain.Board;
import com.tutorial.springboard.repository.BoardRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
public class BoardService {

    @Autowired
    private BoardRepository boardRepository;

    public Page<Board> findBoardList(Pageable pageable) {
        pageable = PageRequest.of(
                pageable.getPageNumber() <= 0 ? 0 : pageable.getPageNumber()-1,
                pageable.getPageSize());
        return boardRepository.findAll(pageable);
    }

    public Board findBoardByIdx(Long idx) {
        return boardRepository.findById(idx).orElse(new Board());
    }
}

 

  • 게시글 리스트뿐만 아니라 게시글 하나에 대한 요청도 처리할 수 있게 BoardService 클래스를 작성했습니다.
  • Spring에서는 Page 클래스로 페이징 요청을 관리합니다. 이 Page 클래스는 페이징에 관련된 여러 요청을 손쉽게 처리할 수 있도록 만들어진 클래스입니다.
  • 보통 Pageable의 구현체인 PageRequest의 인스턴스를 받아 이 안에 있는 데이터를 가지고 페이징 관련 처리를 하게 됩니다. 이 정보를 JpaRepository의 구현체에게 넘겨주면 스프링에서 자동적으로 메서드 인터페이스를 해석하여 페이징 데이터를 DB에서 FETCH하여 넘겨주게 됩니다.

import com.tutorial.springboard.domain.Board;
import com.tutorial.springboard.domain.User;
import com.tutorial.springboard.domain.enums.BoardType;
import com.tutorial.springboard.repository.BoardRepository;
import com.tutorial.springboard.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.stream.IntStream;

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    UserRepository userRepository;

    @Autowired
    BoardRepository boardRepository;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        User user = userRepository.save(User.builder()
                .name("saelobi")
                .password("saelobi")
                .email("saelobi@gmail.com")
                .createdDate(LocalDateTime.now())
                .build());

        IntStream.rangeClosed(1, 200).forEach(index ->
                boardRepository.save(Board.builder()
                    .title("Content " + index)
                    .subTitle("Order " + index)
                    .content("Content Example " + index)
                    .boardType(BoardType.free)
                    .createdDate(LocalDateTime.now())
                    .updatedDate(LocalDateTime.now())
                        .user(user).build()));
    }

}
  • 위는 ApplicationRunner를 구성해서 애플리케이션이 켜질  어떻게 작동할 지를 정하는 run 메서드를 오버라이드 받아 씁니다.
  • 개발시에 테스트 데이터를 받아서 개발하는 경우가 많지만 그것이 여의치 않았을 때 위와 같이 테스트 DB에 데이터를 추가하여 개발 수도 있습니다.

public enum BoardType {
    notice("공지사항"),
    free("자유게시판");

    private String value;

    BoardType(String value) {
        this.value = value;
    }

    public String getValue() {
        return this.value;
    }

}
  • BoardType에 대한 소스 코드입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Board Form</title>
    <link rel="stylesheet"
          href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
          crossorigin="anonymous">
    <style>
        /*html{position:relative;min-height:100%;}*/
        body{
            margin-bottom:60px;
        }
 
        body > .container{
            padding:60px 15px 0;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="page-header">
            <h1>게시글 목록</h1>
        </div>
        <div class="pull-right" style="width:100px;margin:10px 0;">
            <a href="/board" class="btn btn-primary btn-block">등록</a>
        </div>
        <br/><br/><br/>
 
        <div id="mainHide">
            <table class="table table-hover">
                <thead>
                <tr>
                    <th class="col-md-1">#</th>
                    <th class="col-md-2">서비스분류</th>
                    <th class="col-md-5">제목</th>
                    <th class="col-md-2">작성날짜</th>
                    <th class="col-md-2">수정날짜</th>
                </tr>
                </thead>
                <tbody>
                <tr th:each="board : ${boardList}">
                    <td th:text="${board.idx}"></td>
                    <td th:text="${board.boardType.value}"></td>
                    <td><a th:href="'/board?idx='+${board.idx}" th:text="${board.title}"></a></td>
                    <td th:text="${board.createdDate} ?
                    ${#temporals.format(board.createdDate,'yyyy-MM-dd HH:mm')} : ${board.createdDate}"></td>
                    <td th:text="${board.updatedDate} ?
                    ${#temporals.format(board.updatedDate,'yyyy-MM-dd HH:mm')} : ${board.updatedDate}"></td>
                </tr>
                </tbody>
            </table>
        </div>
    </div>
    <br/>
 
    <nav aria-label="Page navigation" style="text-align:center;">
        <ul class="pagination" th:with="startNumber=${T(Math).floor(boardList.number/10)}*10+1, 
endNumber=(${boardList.totalPages} > ${startNumber}+9) ? ${startNumber}+9 : ${boardList.totalPages}">
            <li><a aria-label="Previous" href="/board/list?page=1">&laquo;</a></li>
            <li th:style="${boardList.first} ? 'display:none'">
                <a th:href="@{/board/list(page=${boardList.number})}">&lsaquo;</a>
            </li>
 
            <li th:each="page :${#numbers.sequence(startNumber, endNumber)}" th:class="(${page} == ${boardList.number}+1) ? 'active'">
                <a th:href="@{/board/list(page=${page})}" th:text="${page}"><span class="sr-only"></span></a>
            </li>
 
            <li th:style="${boardList.last} ? 'display:none'">
                <a th:href="@{/board/list(page=${boardList.number}+2)}">&rsaquo;</a>
            </li>
            <li><a aria-label="Next" th:href="@{/board/list(page=${boardList.totalPages})}">&raquo;</a></li>
        </ul>
    </nav>
</body>
</html>
cs

 

  • 위는 게시글을 동적으로 생성하는 Thymeleaf 문법으로 만든 파일입니다. 컨트롤러에서 보내는Model 객체에서 boardList 에 대한 정보를 꺼내와 페이지를 동적으로 만든 것입니다.
  • nav 부분을 보면 페이징의 startNumber와 endNumber에 대한 것을 정하고 HTML 페이지에 반영하는 것을 볼 수 있습니다. 

결과 화면

 

 

 

참고자료 : http://www.yes24.com/Product/Goods/64584833?scode=032&OzSrank=1



출처: https://engkimbs.tistory.com/871?category=767865 [새로비]