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

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


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


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


[Spring/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에 대한 소스 코드입니다.


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

결과 화면




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



이 글을 공유하기

댓글(0)

Designed by JB FACTORY