본문 바로가기
개발/게시판 만들기

[React] 리액트 게시판 만들기 #2 - 백엔드 프로젝트 생성

by onethejay 2023. 4. 16.
728x90

안녕하세요. 원더제이입니다.
지난 시간에는 기본적인 개발환경을 세팅했습니다.

이번에는 백엔드 서버에서의 데이터 및 엔드포인트를 미리 만들어두고자 합니다.
이후에는 리액트 화면에만 집중하고, 정말 필요한 내용이 아니라면 되도록 서버쪽은 손대지 않을 예정입니다.

기존에 다른 웹 프레임워크가 더 편하신분들은 테이블 생성 및 데이터 추가 SQL문과,
엔드포인트 주소 등만 참고하셔서 구성해주시면 됩니다.

또한, 작성되는 포스팅 자료는 깃허브에 챕터마다 업로드 되어 원하시는 챕터의 내용을 Clone 하실 수 있습니다.

테이블 및 데이터 생성 SQL

테이블과 데이터만 필요하신 경우라면 아래 내용을 사용해주세요.

/* *********************
 아래 DDL은 H2 Database를 기준으로 생성되었습니다. 
 MySQL 계열 혹은 Oracle DB에서 사용 시 다를수 있으므로 주의해주세요.
 ********************* */
DROP TABLE IF EXISTS TB_BOARD CASCADE;
CREATE TABLE TB_BOARD
(
    IDX        BIGINT GENERATED BY DEFAULT AS IDENTITY,
    CONTENTS   VARCHAR(255),
    CREATED_AT TIMESTAMP,
    CREATED_BY VARCHAR(255),
    TITLE      VARCHAR(255),
    PRIMARY KEY (IDX)
);

INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (1, '게시글 제목1', '게시글 내용1', '작성자1', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (2, '게시글 제목2', '게시글 내용2', '작성자2', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (3, '게시글 제목3', '게시글 내용3', '작성자3', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (4, '게시글 제목4', '게시글 내용4', '작성자4', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (5, '게시글 제목5', '게시글 내용5', '작성자5', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (6, '게시글 제목6', '게시글 내용6', '작성자6', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (7, '게시글 제목7', '게시글 내용7', '작성자7', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (8, '게시글 제목8', '게시글 내용8', '작성자8', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (9, '게시글 제목9', '게시글 내용9', '작성자9', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (10, '게시글 제목10', '게시글 내용10', '작성자10', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (11, '게시글 제목11', '게시글 내용11', '작성자11', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (12, '게시글 제목12', '게시글 내용12', '작성자12', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (13, '게시글 제목13', '게시글 내용13', '작성자13', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (14, '게시글 제목14', '게시글 내용14', '작성자14', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (15, '게시글 제목15', '게시글 내용15', '작성자15', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (16, '게시글 제목16', '게시글 내용16', '작성자16', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (17, '게시글 제목17', '게시글 내용17', '작성자17', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (18, '게시글 제목18', '게시글 내용18', '작성자18', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (19, '게시글 제목19', '게시글 내용19', '작성자19', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (20, '게시글 제목20', '게시글 내용20', '작성자20', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (21, '게시글 제목21', '게시글 내용21', '작성자21', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (22, '게시글 제목22', '게시글 내용22', '작성자22', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (23, '게시글 제목23', '게시글 내용23', '작성자23', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (24, '게시글 제목24', '게시글 내용24', '작성자24', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (25, '게시글 제목25', '게시글 내용25', '작성자25', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (26, '게시글 제목26', '게시글 내용26', '작성자26', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (27, '게시글 제목27', '게시글 내용27', '작성자27', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (28, '게시글 제목28', '게시글 내용28', '작성자28', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (29, '게시글 제목29', '게시글 내용29', '작성자29', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (30, '게시글 제목30', '게시글 내용30', '작성자30', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (31, '게시글 제목31', '게시글 내용31', '작성자31', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (32, '게시글 제목32', '게시글 내용32', '작성자32', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (33, '게시글 제목33', '게시글 내용33', '작성자33', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (34, '게시글 제목34', '게시글 내용34', '작성자34', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (35, '게시글 제목35', '게시글 내용35', '작성자35', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (36, '게시글 제목36', '게시글 내용36', '작성자36', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (37, '게시글 제목37', '게시글 내용37', '작성자37', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (38, '게시글 제목38', '게시글 내용38', '작성자38', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (39, '게시글 제목39', '게시글 내용39', '작성자39', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (40, '게시글 제목40', '게시글 내용40', '작성자40', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (41, '게시글 제목41', '게시글 내용41', '작성자41', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (42, '게시글 제목42', '게시글 내용42', '작성자42', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (43, '게시글 제목43', '게시글 내용43', '작성자43', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (44, '게시글 제목44', '게시글 내용44', '작성자44', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (45, '게시글 제목45', '게시글 내용45', '작성자45', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (46, '게시글 제목46', '게시글 내용46', '작성자46', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (47, '게시글 제목47', '게시글 내용47', '작성자47', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (48, '게시글 제목48', '게시글 내용48', '작성자48', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (49, '게시글 제목49', '게시글 내용49', '작성자49', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (50, '게시글 제목50', '게시글 내용50', '작성자50', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (51, '게시글 제목51', '게시글 내용51', '작성자51', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (52, '게시글 제목52', '게시글 내용52', '작성자52', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (53, '게시글 제목53', '게시글 내용53', '작성자53', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (54, '게시글 제목54', '게시글 내용54', '작성자54', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (55, '게시글 제목55', '게시글 내용55', '작성자55', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (56, '게시글 제목56', '게시글 내용56', '작성자56', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (57, '게시글 제목57', '게시글 내용57', '작성자57', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (58, '게시글 제목58', '게시글 내용58', '작성자58', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (59, '게시글 제목59', '게시글 내용59', '작성자59', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (60, '게시글 제목60', '게시글 내용60', '작성자60', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (61, '게시글 제목61', '게시글 내용61', '작성자61', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (62, '게시글 제목62', '게시글 내용62', '작성자62', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (63, '게시글 제목63', '게시글 내용63', '작성자63', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (64, '게시글 제목64', '게시글 내용64', '작성자64', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (65, '게시글 제목65', '게시글 내용65', '작성자65', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (66, '게시글 제목66', '게시글 내용66', '작성자66', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (67, '게시글 제목67', '게시글 내용67', '작성자67', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (68, '게시글 제목68', '게시글 내용68', '작성자68', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (69, '게시글 제목69', '게시글 내용69', '작성자69', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (70, '게시글 제목70', '게시글 내용70', '작성자70', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (71, '게시글 제목71', '게시글 내용71', '작성자71', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (72, '게시글 제목72', '게시글 내용72', '작성자72', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (73, '게시글 제목73', '게시글 내용73', '작성자73', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (74, '게시글 제목74', '게시글 내용74', '작성자74', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (75, '게시글 제목75', '게시글 내용75', '작성자75', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (76, '게시글 제목76', '게시글 내용76', '작성자76', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (77, '게시글 제목77', '게시글 내용77', '작성자77', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (78, '게시글 제목78', '게시글 내용78', '작성자78', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (79, '게시글 제목79', '게시글 내용79', '작성자79', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (80, '게시글 제목80', '게시글 내용80', '작성자80', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (81, '게시글 제목81', '게시글 내용81', '작성자81', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (82, '게시글 제목82', '게시글 내용82', '작성자82', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (83, '게시글 제목83', '게시글 내용83', '작성자83', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (84, '게시글 제목84', '게시글 내용84', '작성자84', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (85, '게시글 제목85', '게시글 내용85', '작성자85', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (86, '게시글 제목86', '게시글 내용86', '작성자86', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (87, '게시글 제목87', '게시글 내용87', '작성자87', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (88, '게시글 제목88', '게시글 내용88', '작성자88', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (89, '게시글 제목89', '게시글 내용89', '작성자89', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (90, '게시글 제목90', '게시글 내용90', '작성자90', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (91, '게시글 제목91', '게시글 내용91', '작성자91', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (92, '게시글 제목92', '게시글 내용92', '작성자92', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (93, '게시글 제목93', '게시글 내용93', '작성자93', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (94, '게시글 제목94', '게시글 내용94', '작성자94', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (95, '게시글 제목95', '게시글 내용95', '작성자95', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (96, '게시글 제목96', '게시글 내용96', '작성자96', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (97, '게시글 제목97', '게시글 내용97', '작성자97', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (98, '게시글 제목98', '게시글 내용98', '작성자98', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (99, '게시글 제목99', '게시글 내용99', '작성자99', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (100, '게시글 제목100', '게시글 내용100', '작성자100', NOW());
INSERT INTO TB_BOARD (IDX, TITLE, CONTENTS, CREATED_BY, CREATED_AT)
VALUES (101, '게시글 제목101', '게시글 내용101', '작성자101', NOW());

백엔드 프로젝트 작업 #1 - JPA 설정

Vue 게시판 만들기 프로젝트에서 사용할 백엔드 서버를 구성합니다.
설정을 건너뛰고 엔드포인트 및 서비스 부분이 필요하신분은 백엔드-프로젝트-3---컨트롤러-생성부분부터 보시면 됩니다.

데이터베이스 기본 세팅 작업을 위해 JPA와 자바 개발에 필수인 Lombok 의존성을 추가하겠습니다.

build.gradle의 내용을 아래로 변경합니다.

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.10'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

repositories {
    mavenCentral()
}

dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'

    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.h2database:h2:1.4.193'

    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

내용을 추가하셨다면 Gradle Refresh는 잊지말고 꼭 진행해주세요!

이어서 application.properties의 내용을 수정합니다. 아래 내용으로 변경해주세요.

# H2 Database
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb
# JPA
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
spring.jpa.defer-datasource-initialization=true

application.properties와 동일한 위치에 import.sql으로 일반 파일을 하나 생성하시고
위의 INSERT SQL 부분만 모두 붙여넣습니다.

서버가 부팅될 때 위에서 추가한 JPA와 application.properties에 추가한 설정에 따라,
Entity 클래스와 import.sql 파일이 있다면 자동으로 테이블을 생성하고 데이터를 추가해줍니다.

이어서 Entity 클래스를 생성합니다. (jpa ddl-auto에 따라 엔티티 클래스와 매핑되는 테이블을 생성 또는 수정할 수 있습니다.)

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
@Table(name = "TB_BOARD")
@DynamicInsert
@DynamicUpdate
public class BoardEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idx;
    private String title;
    private String contents;
    private String createdBy;
    private Date createdAt;
}


생성이 완료되었다면, 서버를 부팅해서 확인해봅니다.
로그를 보면 테이블을 생성하고 데이터가 추가된 것 처럼 보입니다.

실제로 데이터베이스에 추가되었는지는 h2-console을 통해 확인할 수 있습니다.
서버가 실행되고 있는 상태에서 웹 브라우저에서 http://localhost:8080/h2-console 에 접속합니다.
JDBC URL은 jdbc:h2:mem:testdb로 지정되어야 합니다. User Name은 sa로 두고 Password는 입력하지 않아도 접속 가능합니다.

정상적으로 접속됐다면 아래와 같은 화면을 확인할 수 있습니다.

TB_BOARD가 생성되어있는 것 같네요. 데이터도 잘 생성됐는지 SELECT 쿼리를 실행해봅니다.

데이터가 잘 생성되어 있는걸 확인할 수 있습니다.

백엔드 프로젝트 #2 - MyBatis 설정

이번 프로젝트에서 JPA 는 단순히 서버 부팅시 데이터 초기화를 위해서만 사용하겠습니다.
데이터베이스와의 작업은 모두 MyBatis 로 진행합니다.

build.gradle의 dependencies에 아래를 추가하고 Gradle Refresh 합니다.

implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4:1.16'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.1'

※ 쿼리 로깅을 위해 DriverSpy와 log4jdbc가 설정됩니다. 해당 설정으로 서버가 부팅될 경우 h2-console에 접근할 수 없습니다. h2-console 확인이 필요하실 경우, spring.datasource.driver-class-name과 spring.datasource.url 각각의 윗 부분 주석을 해제하고 아래 부분을 주석처리해주세요.

application.properties에 MyBatis 관련 설정을 추가하기 위해 아래 내용으로 덮어쓰기 해주세요.

# H2 Database
spring.h2.console.enabled=true
spring.h2.console.settings.web-allow-others=true
#spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.driver-class-name=net.sf.log4jdbc.sql.jdbcapi.DriverSpy
#spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.url=jdbc:log4jdbc:h2:mem:testdb
# JPA
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
spring.jpa.defer-datasource-initialization=true
# Mybatis
mybatis.mapper-locations=classpath:mapper/**.xml
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.call-setters-on-nulls=false
# 쿼리 로그 관련 설정
logging.level.jdbc.sqlonly=OFF
logging.level.jdbc.sqltiming=INFO
logging.level.jdbc.resultsettable=OFF
logging.level.jdbc.audit=OFF
logging.level.jdbc.resultset=OFF
logging.level.jdbc.connection=OFF

쿼리 로그 관련 로그 설정을 위해 log4jdbc.log4j2.properties 파일을 resources 아래에 생성합니다.

생성한 파일에 아래 내용을 추가합니다.

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0

이어서 데이터베이스와의 쿼리 작업을 위한 board.xml 파일을 mapper 폴더에 생성합니다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper3.0//EN" "http://mybatis.org/schema/mybatis-3-mapper.dtd">

<mapper namespace="com.example.reactboard.db.BoardMapper">
    <select id="getBoardList" resultType="com.example.reactboard.BoardEntity">
        SELECT IDX
        , TITLE
        , CONTENTS
        , CREATED_BY
        , CREATED_AT
        FROM TB_BOARD
    </select>
</mapper>

MyBatis는 Class에서 바로 xml로 접근할 수 없습니다. 중간에 매퍼 인터페이스를 통해 접근해야 합니다.
매퍼 인터페이스는 mapper의 namespace에 입력한 위치의 해당 파일과 연결되어야 합니다.


namespace에 대응되는 위치에 BoardMapper 인터페이스를 생성합니다.

또한, mapper xml에 생성한 쿼리의 id와 인터페이스 메서드 명이 동일해야 합니다.
위에서 생성한 쿼리의 id는 getBoardList 였으므로 동일한 이름의 메서드를 생성합니다.

import com.example.reactboard.BoardEntity;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface BoardMapper {
    /*
        mapper xml파일의 resultType 해당하는 클래스에 결과를 담으며,
        N개가 되므로 MutableList로 Return 타입을 설정합니다.
    */
    List<BoardEntity> getBoardList();
}

백엔드 프로젝트 #3 - 컨트롤러 생성

이제 데이터베이스를 정상적으로 조회하는지 확인하기 위해 Controller를 생성합니다.
(Controller에서 바로 데이터베이스에 접근하는 방식은 추천하지 않습니다.
이번 데이터 확인까지만 진행한 이후에는 Service를 만들어 옮기도록 하겠습니다.)

import com.example.reactboard.db.BoardMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RequiredArgsConstructor
@RestController
public class BoardController {
    private final BoardMapper boardMapper;

    //Http Get 방식으로 주소 가장 뒤 /board로 접근
    @GetMapping("/board")
    List<BoardEntity> getBoardList() {
        return boardMapper.getBoardList();
    }
}

서버를 재시작 하고 브라우저에서 localhost:8080/board 를 입력합니다.
아래처럼 데이터가 출력된다면 데이터베이스와 통신이 성공한 것입니다.
(Chrome의 JSON VIEW 확장 프로그램을 사용하면 깔끔하게 표시됩니다.)

이제 컨트롤러 CRUD 엔드포인트를 쭉 작성하도록 하겠습니다.
Http Method에 따라 기능을 구현합니다.

  • Get : 조회 (Read)
  • Post : 생성 (Create)
  • Patch : 수정 (Update)
  • Delete : 삭제 (Delete)

※ BoardEntity는 db 패키지로 옮겨주세요. com.example.reactboard.BoardEntity -> com.example.reactboard.db.BoardEntity
아래 소스를 복사해서 BoardController에 붙여 넣어주세요.

import com.example.reactboard.db.BoardEntity;
import com.example.reactboard.dto.BoardSaveDto;
import com.example.reactboard.util.Header;
import com.example.reactboard.util.Search;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RequiredArgsConstructor
@RestController
public class BoardController {
    private final BoardService boardService;

    //Http Get 방식으로 주소 가장 뒤 /board로 접근
    @GetMapping("/board")
    Header<List<BoardEntity>> getBoardList(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "10") int size, Search search) {
        return boardService.getBoardList(page, size, search);
    }

    //idx의 데이터 1개를 조회한다.
    @GetMapping("/board/{idx}")
    Header<BoardEntity> getBoardOne(@PathVariable Long idx) {
        return boardService.getBoardOne(idx);
    }

    @PostMapping("/board")
    Header<BoardEntity> createBoard(@RequestBody BoardSaveDto boardSaveDto) {
        return boardService.insertBoard(boardSaveDto);
    }

    @PatchMapping("/board")
    Header<BoardEntity> updateBoard(@RequestBody BoardSaveDto boardSaveDto) {
        return boardService.updateBoard(boardSaveDto);
    }

    @DeleteMapping("/board/{idx}")
    Header<String> deleteBoard(@PathVariable Long idx) {
        return boardService.deleteBoard(idx);
    }
}

백엔드 프로젝트 #4 - 서비스, 유틸 클래스 생성

위에서 컨트롤러 소스 붙여넣기 이후 빨간줄로 에러가 발생해도 괜찮습니다.
이어서 소스 파일들을 생성해주세요.

dto 패키지를 생성하고 아래 파일들을 생성합니다.

BoardSaveDto.class

import com.example.reactboard.db.BoardEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BoardSaveDto {
    private Long idx;
    private String title;
    private String contents;
    private String createdBy;

    public BoardEntity toEntity() {
        return BoardEntity.builder()
                .idx(idx)
                .title(title)
                .contents(contents)
                .createdBy(createdBy)
                .build();
    }
}

이어서 util 패키지를 생성하고 아래 파일들을 생성합니다.

Header.class

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Header<T> {
    private LocalDateTime transactionTime;
    private String resultCode;
    private String description;
    private T data;
    private Pagination pagination;

    public static <T> Header<T> OK() {
        return (Header<T>) Header.builder()
                .transactionTime(LocalDateTime.now())
                .resultCode("OK")
                .description("OK")
                .build();
    }

    //DATA OK
    public static <T> Header<T> OK(T data) {
        return (Header<T>) Header.builder()
                .transactionTime(LocalDateTime.now())
                .resultCode("OK")
                .description("OK")
                .data(data)
                .build();
    }

    public static <T> Header<T> OK(T data, Pagination pagination) {
        return (Header<T>) Header.builder()
                .transactionTime(LocalDateTime.now())
                .resultCode("OK")
                .description("OK")
                .data(data)
                .pagination(pagination)
                .build();
    }

    public static <T> Header<T> ERROR(String description) {
        return (Header<T>) Header.builder()
                .transactionTime(LocalDateTime.now())
                .resultCode("ERROR")
                .description(description)
                .build();
    }
}

Pagination.class

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Pagination {
    //페이지당 보여지는 게시글 최대 개수
    private int pageSize;

    //현재 페이지
    int page;

    //현재 블럭
    int block;

    //총 게시글 수
    int totalListCnt;

    //총 페이지 수
    int totalPageCnt;

    //총 구간 수
    int totalBlockCnt;

    //시작 페이지
    int startPage;

    //마지막 페이지
    int endPage;

    // 이전 구간 마지막 페이지
    int prevBlock;

    // 다음 구간 시작 페이지
    int nextBlock;

    // 인덱스
    int startIndex;

    public Pagination(Integer totalListCnt, Integer page, Integer pageSize, Integer blockSize) {
        this.pageSize = pageSize;

        //현재 페이지
        this.page = page;

        //총 게시글 수
        this.totalListCnt = totalListCnt;

        //총 페이지 수
        totalPageCnt = (int) Math.ceil(totalListCnt * 1.0 / this.pageSize);

        //총 블럭 수
        totalBlockCnt = (int) Math.ceil(totalPageCnt * 1.0 / blockSize);

        //현재 블럭
        block = (int) Math.ceil((this.page * 1.0) / blockSize);

        //if(block < 1) block = 1

        //블럭 시작 페이지
        startPage = ((block - 1) * blockSize + 1);

        //블럭 마지막 페이지
        endPage = startPage + blockSize - 1;

        //블럭 마지막 페이지 validation
        if (endPage > totalPageCnt) endPage = totalPageCnt;

        // 이전 블럭 (클릭 시, 이전 블럭 마지막 페이지)
        prevBlock = (block * blockSize) - blockSize;

        // 이전 블럭 validation
        if (prevBlock < 1) prevBlock = 1;

        //다음 블럭 (클릭 시, 다음 블럭 첫번째 페이지)
        nextBlock = (block * blockSize + 1);

        // 다음 블럭 validation
        if (nextBlock > totalPageCnt) nextBlock = totalPageCnt;

        //if(this.page < 1) this.page = 1

        startIndex = (this.page - 1) * this.pageSize;
    }
}

Search.class

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Search {
    private String sk;  //search key
    private String sv;  //search value
}

이어서 BoardService 파일을 생성합니다. 위치는 BoardController와 동일한 위치입니다.
BoardService.class

import com.example.reactboard.db.BoardEntity;
import com.example.reactboard.db.BoardMapper;
import com.example.reactboard.dto.BoardSaveDto;
import com.example.reactboard.util.Header;
import com.example.reactboard.util.Pagination;
import com.example.reactboard.util.Search;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;

@RequiredArgsConstructor
@Service
public class BoardService {
    private final BoardMapper boardMapper;

    Header<List<BoardEntity>> getBoardList(int page, int size, Search search) {
        HashMap<String, Object> paramMap = new HashMap<>();

        if (page <= 1) {    //페이지가 1 이하로 입력되면 0으로 고정,
            paramMap.put("page", 0);
        } else {            //페이지가 2 이상
            paramMap.put("page", (page - 1) * size);
        }
        paramMap.put("size", size);
        paramMap.put("sk", search.getSk());
        paramMap.put("sv", search.getSv());

        List<BoardEntity> boardList = boardMapper.getBoardList(paramMap);
        Pagination pagination = new Pagination(
                boardMapper.getBoardTotalCount(paramMap),
                page,
                size,
                10
        );

        return Header.OK(boardList, pagination);
    }

    Header<BoardEntity> getBoardOne(Long idx) {
        return Header.OK(boardMapper.getBoardOne(idx));
    }

    Header<BoardEntity> insertBoard(BoardSaveDto boardSaveDto) {
        BoardEntity entity = boardSaveDto.toEntity();
        if (boardMapper.insertBoard(entity) > 0) {
            return Header.OK(entity);
        } else {
            return Header.ERROR("ERROR");
        }
    }

    Header<BoardEntity> updateBoard(BoardSaveDto boardSaveDto) {
        BoardEntity entity = boardSaveDto.toEntity();
        if (boardMapper.updateBoard(entity) > 0) {
            return Header.OK(entity);
        } else {
            return Header.ERROR("ERROR");
        }
    }

    Header<String> deleteBoard(Long idx) {
        if (boardMapper.deleteBoard(idx) > 0) {
            return Header.OK();
        } else {
            return Header.ERROR("ERROR");
        }
    }
}

마지막으로 BoardMapper의 내용을 변경합니다.
BoardMapper.interface

import org.apache.ibatis.annotations.Mapper;

import java.util.HashMap;
import java.util.List;

@Mapper
public interface BoardMapper {
    /*
        mapper xml파일의 resultType 해당하는 클래스에 결과를 담으며,
        N개가 되므로 MutableList로 Return 타입을 설정합니다.
    */
    List<BoardEntity> getBoardList(HashMap<String, Object> paramMap);

    int getBoardTotalCount(HashMap<String, Object> paramMap);

    BoardEntity getBoardOne(Long idx);

    int insertBoard(BoardEntity entity);

    int updateBoard(BoardEntity entity);

    int deleteBoard(Long idx);
}

BoardMapper와 대응되는 board.xml의 내용도 변경합니다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper3.0//EN" "http://mybatis.org/schema/mybatis-3-mapper.dtd">

<mapper namespace="com.example.reactboard.db.BoardMapper">
    <select id="getBoardList" parameterType="Map" resultType="com.example.reactboard.db.BoardEntity">
        SELECT IDX
        , TITLE
        , CONTENTS
        , CREATED_BY
        , CREATED_AT
        FROM TB_BOARD
        WHERE 1=1

        <if test="sk != '' || sk != null">
            <if test="sk == 'title'">
                AND TITLE LIKE CONCAT('%', #{sv}, '%')
            </if>
            <if test="sk == 'contents'">
                AND CONTENTS LIKE CONCAT('%', #{sv}, '%')
            </if>
            <if test="sk == 'createdBy'">
                AND CREATED_BY LIKE CONCAT('%', #{sv}, '%')
            </if>
        </if>
        ORDER BY IDX DESC
        LIMIT #{page}, #{size}
    </select>

    <select id="getBoardTotalCount" parameterType="Map" resultType="Int">
        SELECT COUNT(IDX)
        FROM TB_BOARD
        WHERE 1=1
        <if test="sk != '' || sk != null">
            <if test="sk == 'title'">
                AND TITLE LIKE CONCAT('%', #{sv}, '%')
            </if>
            <if test="sk == 'contents'">
                AND CONTENTS LIKE CONCAT('%', #{sv}, '%')
            </if>
            <if test="sk == 'createdBy'">
                AND CREATED_BY LIKE CONCAT('%', #{sv}, '%')
            </if>
        </if>
    </select>

    <select id="getBoardOne" parameterType="Long" resultType="com.example.reactboard.db.BoardEntity">
        SELECT IDX
        , TITLE
        , CONTENTS
        , CREATED_BY
        , CREATED_AT
        FROM TB_BOARD
        WHERE IDX = #{idx}
    </select>

    <insert id="insertBoard" parameterType="com.example.reactboard.db.BoardEntity" keyProperty="idx" useGeneratedKeys="true">
        INSERT INTO TB_BOARD
        (
        TITLE
        , CONTENTS
        , CREATED_BY
        , CREATED_AT
        ) VALUES (
        #{title}
        , #{contents}
        , #{createdBy}
        , NOW()
        )
    </insert>

    <update id="updateBoard" parameterType="com.example.reactboard.db.BoardEntity">
        UPDATE TB_BOARD
        SET TITLE = #{title}
        , CONTENTS = #{contents}
        WHERE IDX = #{idx}
    </update>

    <delete id="deleteBoard" parameterType="Long">
        DELETE FROM TB_BOARD
        WHERE IDX = #{idx}
    </delete>
</mapper>

위에 작성한 소스를 전부 만들면 아래와 같게 됩니다.

이제 서버가 정상적으로 부팅되는지 시작해서 확인해봅니다.
에러 메시지 없이 Tomcat started on port(s): 8080만 보인다면 부팅은 완료된 것 입니다.

여기까지 백엔드 서버에서 사용할 작업을 모두 마쳤습니다. 기본 CRUD와 추가적으로 페이징과 검색 기능도 같이 구현하다보니 많은 내용이 정리되었네요.

앞으로는 이 서버를 두고 프론트엔드를 하나씩 만들어보도록 하겠습니다.
긴 글 따라하시느라 고생많으셨습니다. 그럼 다음 포스팅에서 뵙겠습니다.😊

이번 챕터의 소스는 https://github.com/onethejay/react-board/tree/chap2 여기에서 받으실 수 있습니다.

728x90

댓글