[스프링 부트/ Spring Boot] 스프링 게시판 만들기 - 부트로 쉽게 구현한 Spring 게시판
- Spring/Spring Boot
- 2019. 6. 3. 22:44
| 스프링 게시판 만들기 - 부트로 쉽게 구현한 Spring 게시판
예제 git repository는 여기를 클릭하시면 됩니다.
스프링 게시판은 스프링 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);
}
- User와 Board 정보를 영속화하기 위한 DB 인터페이스인 UserRepository, BoardRepository 인터페이스입니다.
- JpaRepository 인터페이스를 확장하며 타입을 지정해주면 스프링 JPA가 제공하는 기본적인 기능 및 추가로 메서드명을 조합함으로서 특정 데이터들을 FETCH할 수 있다. 이에 관련된 것은 아래를 참조하시면 될 것 같습니다.
[Spring/Spring JPA] - [Spring JPA #11] 스프링 데이터 리포지터리 인터페이스 정의하기(Spring Repository Interface)
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">«</a></li>
<li th:style="${boardList.first} ? 'display:none'">
<a th:href="@{/board/list(page=${boardList.number})}">‹</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)}">›</a>
</li>
<li><a aria-label="Next" th:href="@{/board/list(page=${boardList.totalPages})}">»</a></li>
</ul>
</nav>
</body>
</html>
- 위는 게시글을 동적으로 생성하는 Thymeleaf 문법으로 만든 파일입니다. 컨트롤러에서 보내는Model 객체에서 boardList 에 대한 정보를 꺼내와 페이지를 동적으로 만든 것입니다.
- nav 부분을 보면 페이징의 startNumber와 endNumber에 대한 것을 정하고 HTML 페이지에 반영하는 것을 볼 수 있습니다.
결과 화면
참고자료 : http://www.yes24.com/Product/Goods/64584833?scode=032&OzSrank=1
'Spring > Spring Boot' 카테고리의 다른 글
스프링 부트로 OAuth2 구현(페이스북, 구글, 카카오, 네이버) (12) | 2019.04.30 |
---|---|
[Spring Boot #32] 스프링 부트 Actuator, JConsole, VisualVM, 스프링 Admin (0) | 2019.01.23 |
[Spring Boot #31] 스프링 부트 RestTemplate, WebClient (0) | 2019.01.23 |
[Spring Boot #30] 스프링 부트 시큐리티 커스터마이징 (1) | 2019.01.22 |
[Spring Boot #29] 스프링 부트 시큐리티 (0) | 2019.01.12 |
이 글을 공유하기