익명 게시판 만들기

1. yml 설정

application.yml
spring:
  profiles:
    active:
      - dev #활성화할 프로필 설정
application-dev.yml
server:
  servlet:
    encoding:
      charset: utf-8
      force: true
  port: 8080

spring:
  mustache:
    servlet:
      expose-session-attributes: true  # Mustache 템플릿에서 세션 속성에 접근할 수 있도록 허용
      expose-request-attributes: true  # Mustache 템플릿에서 요청 속성에 접근할 수 있도록 허용
  datasource:
    driver-class-name: org.h2.Driver    # 데이터베이스 드라이버로 H2 DB를 사용
    url: jdbc:h2:mem:test;MODE=MySQL    # H2 인메모리 데이터베이스를 MySQL 호환 모드로 사용 (테스트용)
    username: sa                        # 데이터베이스 연결 시 기본 사용자 이름
    password:                           # 데이터베이스 기본 비밀번호 (비어 있음)
  h2:
    console:
      enabled: true   # H2 데이터베이스 콘솔을 활성화하여 브라우저에서 데이터베이스를 관리할 수 있도록 함
  sql:
    init:
      data-locations:
        - classpath:db/data.sql  # 애플리케이션 초기화 시 실행할 데이터 삽입 SQL 파일의 경로 (data.sql)
  jpa:
    hibernate:
      ddl-auto: create            # 애플리케이션이 시작될 때 데이터베이스 테이블을 자동으로 생성
    show-sql: true                # Hibernate가 실행하는 SQL 쿼리를 콘솔에 출력
    properties:
      hibernate:
        format_sql: true          # 출력되는 SQL 쿼리를 포맷팅하여 읽기 쉽게 출력
    defer-datasource-initialization: true  # 데이터베이스 초기화가 지연되도록 설정하여 JPA 설정 후에 데이터 초기화

  output:
    ansi:
      enabled: always  # 콘솔 출력 시 ANSI 색상을 항상 사용하도록 설정 (색상을 통해 로그를 더 쉽게 구분 가능)

 

2. 엔티티 클래스 만들기

Board.java
package com.tenco.blog_v1.board;

import jakarta.persistence.*;
import lombok.Data;

import java.sql.Timestamp;

@Entity
@Table(name = "board_tb")
@Data
public class Board {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 기본키 전략 db 위임
    private Integer id;
    private String title;
    private String content;

    // created_at 컬럼과 매핑하며, 이 필드는 데이터 저장시 자동으로 설정 됨
    @Column(name = "created_at", insertable = false, updatable = false)
    private Timestamp createdAt;
}

 

3. 레포지토리 클래스 작성

BoardNativeRepository.java - Native쿼리 연습
  • @Repository 애노테이션으로 스프링에게 이 클래스가 레포지토리임을 알린다.
  • EntityManager를 주입받아 Native Query를 사용하여 CRUD 메서드를 구현합니다.
  • @Transactional 애노테이션을 사용하여 데이터 변경 메서드에 트랜잭션을 관리합니다.
package com.tenco.blog_v1.board;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository // IoC
@RequiredArgsConstructor
public class BoardNativeRepository {

    // DI 처리
    private final EntityManager em;

    /**
     * 새로운 게시글 생성
     *
     * @param title
     * @param content
     */
    @Transactional
    public void save(String title, String content) {
        Query query = em.createNativeQuery(
                "INSERT INTO board_tb(title, content, created_at) VALUES (?, ?, now())"
        );
        query.setParameter(1, title);
        query.setParameter(2, content);
        // 실행
        query.executeUpdate();
    }

    /**
     * 단일 게시글 조회
     * @param id
     * @return
     */
    public Board findById(int id) {
        Query query = em.createNativeQuery("SELECT * FROM board_tb WHERE id = ?", Board.class);
        query.setParameter(1, id);
        return (Board) query.getSingleResult();
    }

    /**
     * 모든 게시글 조회
     * @return
     */
    public List<Board> findAll() {
        Query query = em.createNativeQuery("SELECT * FROM board_tb ORDER BY id DESC", Board.class);
        return query.getResultList();
    }

    /**
     * 특정 ID로 게시글을 수정하는 기능
     * @param id
     * @param title
     * @param content
     */
    @Transactional
    public void updateById(int id, String title, String content) {
        Query query = em.createNativeQuery("UPDATE board_tb SET title = ?, content = ? WHERE id = ?", Board.class);
        query.setParameter(1, title);
        query.setParameter(2, content);
        query.setParameter(3, id);
        query.executeUpdate();
    }

    public void deleteById(int id) {
        Query query = em.createNativeQuery("DELETE FROM board_tb WHERE id = ?", Board.class);
        query.setParameter(1, id);
        query.executeUpdate();
    }
}

4. 컨트롤러 클래스 작성

BoardController.java
  • @ Controller 애노테이션으로 스프링에게 이 클래스가 컨트롤러임을 알다.
  • @RequiredArgsConstructor를 사용하여 레포지토리를 주입받다.
  • 각 HTTP 요청에 대해 적절한 핸들러 메서드를 작성한다.
    • GET 요청: 데이터 조회 및 뷰 반환
    • POST 요청: 데이터 변경 및 리다이렉트
package com.tenco.blog_v1.board;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@Slf4j
@Controller
@RequiredArgsConstructor
public class BoardController {

    private final BoardNativeRepository boardNativeRepository;

    @GetMapping("/")
    public String index(Model model) {
        List<Board> boardList = boardNativeRepository.findAll();
        model.addAttribute("boardList", boardList);
        return "index";
    }

    // 게시글 작성 화면
    @GetMapping("/board/save-form")
    public String saveForm() {
        return "board/save-form";
    }

    // 게시글 저장
    @PostMapping("/board/save")
    public String save(@RequestParam(name = "title") String title,
                       @RequestParam(name = "content") String content) {
        log.warn("save 실행 : 제목={}, 내용={}", title, content);
        boardNativeRepository.save(title, content);
        return "redirect:/";
    }

    // 특정 게시글 요청 화면
    @GetMapping("/board/{id}")
    public String detail(@PathVariable(name = "id") Integer id, HttpServletRequest request) {
        Board board = boardNativeRepository.findById(id);
        request.setAttribute("board", board);
        return "board/detail";
    }

    // 게시글 삭제
    // form 태그에서는 GET, POST 방식만 지원
    @PostMapping("/board/{id}/delete") // form 활용이기 때문에 delete 선언
    public String delete(@PathVariable(name = "id") Integer id, HttpServletRequest request) {
        boardNativeRepository.deleteById(id);
        return "redirect:/";
    }

    // 게시글 수정 화면 요청
    @GetMapping("board/{id}/update-form")
    public String updateForm(@PathVariable(name = "id") Integer id, HttpServletRequest request) {
        Board board = boardNativeRepository.findById(id);
        request.setAttribute("board", board);
        return "board/update-form";
    }

    // 게시글 수정 요청 기능
    @PostMapping("board/{id}/update")
    public String update(@PathVariable(name = "id") Integer id,
                         @RequestParam(name = "title") String title,
                         @RequestParam(name = "content") String content) {
        boardNativeRepository.updateById(id, title, content);
        return "redirect:/board/" + id;
    }
}

5. Mustache 파일 작성

템플릿 작성 (Mustache)

  • src/main/resources/templates/ 디렉토리에 Mustache 템플릿 파일을 생성한다.
  • 각 컨트롤러 메서드가 반환하는 뷰 이름에 맞춰 템플릿 파일을 작성한다.
    • index.mustache
    • board/save-form.mustache
    • board/detail.mustache
    • board/update-form.mustache
index.mustache
{{> layout/header}}

<div class="container p-5">
    <!-- 게시글 목록을 반복 출력 (boardList가 null이 아니고 비어 있지 않다면 출력) -->
    {{#boardList}}
        <div class="card mb-3">
            <div class="card-body">
                <h4 class="card-title mb-3">{{title}}</h4>
                <a href="/board/{{id}}" class="btn btn-primary">상세보기</a>
            </div>
        </div>
    {{/boardList}} <!-- 반드시 섹션을 닫는 태그가 필요 -->
    
    <!-- 게시글이 없을 경우 출력할 내용 -->
    {{^boardList}}
        <p>게시글이 없습니다.</p>
    {{/boardList}}

    <ul class="pagination d-flex justify-content-center">
        <li class="page-item disabled"><a class="page-link" href="#">Previous</a></li>
        <li class="page-item"><a class="page-link" href="#">Next</a></li>
    </ul>
</div>

{{> layout/footer}}

Mustache 태그 설명

  1. {{> layout/header}}: Partial(부분 템플릿) 태그이다. header.mustache 파일을 포함해 헤더를 재사용할 수 있다. 공통 레이아웃을 유지하는 데 유용하다.
  2. {{#boardList}}: Section(섹션) 태그이다. 조건부 반복 블록으로 boardList 데이터가 존재할 때만 내부 블록을 반복 처리합니다. 주로 리스트 데이터 렌더링에 사용된다.
  3. {{title}} / {{id}}: Variable(변수) 태그이다. Mustache에서 데이터 바인딩을 처리하는 구문으로, 해당 변수 값이 템플릿에서 렌더링된다. 이 경우 각 게시글의 title과 id 값이 출력된다.
static/styles.css
body {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
}

.content {
    flex: 1;
}

footer {
    position: relative;
    bottom: 0;
    width: 100%;
    background-color: #f8f9fa;
    text-align: center;
    padding: 20px;
}

 

save-form.mustache
{{> layout/header}} {{!    Partial 태그 (부분 템플릿 태그) }}

  <main class="container p-5 content">
    <article>
      <div class="card">
        <div class="card-header"><b>글쓰기 화면입니다</b></div>
        <div class="card-body">
          <form action="/board/save" method="post">
            <div class="mb-3">
              <input type="text" class="form-control" placeholder="Enter title" name="title">
            </div>
            <div class="mb-3">
              <textarea class="form-control" rows="5" name="content"></textarea>
            </div>
            <button class="btn btn-primary form-control">글쓰기완료</button>
          </form>
        </div>
      </div>
    </article>
  </main>

{{> layout/footer}}

 

6. Native Query와 JPQL란?

Native Query 소개

Native Query는 데이터베이스의 고유한 SQL 문법을 사용하여 쿼리를 작성한다. 복잡한 쿼리나 JPQL로 표현하기 어려운 특정 기능을 사용할 때 유용하다.

 

JPQL(Java Persistence Query Language)란?

JPQL은 객체 지향 쿼리 언어로, 엔티티 객체를 대상으로 쿼리를 작성한다. JPQL은 데이터베이스 독립적이며, JPA의 장점을 최대한 활용할 수 있게 해준다.

목차로 돌아가기

 

'Spring Boot > Blog 프로젝트 만들기(JPA)' 카테고리의 다른 글

연관 관계 매핑: User와 Board 엔티티  (0) 2024.10.07
사용자 관리 기본 코드 추가 및 설정  (0) 2024.10.07
프로젝트 생성  (0) 2024.10.07
Mustache 란?  (0) 2024.10.04
템플릿 엔진이란?  (1) 2024.10.04