본문 바로가기

Spring boot/Blog 프로젝트 만들기(JPA)

완성 코드 STEP 01 - 익명 게시판 만들기

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 색상을 항상 사용하도록 설정 (색상을 통해 로그를 더 쉽게 구분 가능)

 

 

 

엔티티 클래스 만들기 (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;
}

 

 

 

 

레포지토리 클래스 작성 (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.persistence.TypedQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@RequiredArgsConstructor
@Repository // IoC
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();
    }

    /**
     * 특정 ID의 게시글을 조회 합니다.
     *
     * @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 ");
        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 = ?");
        query.setParameter(1, title);
        query.setParameter(2, content);
        query.setParameter(3, id);
        query.executeUpdate();
    }

    /**
     * 특정 ID의 게시글을 삭제 합니다. 
     * @param id
     */
    @Transactional
    public void deleteById(int id) {
        Query query
                = em.createNativeQuery("DELETE FROM board_tb WHERE id = ?");
        query.setParameter(1, id);
        query.executeUpdate();
    }

}

컨트롤러 클래스 작성 (BoardController.java)

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

 

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}}

 

 

 

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}}

 

 

 

BoardController
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
@RequiredArgsConstructor
@Controller
public class BoardController {

    private final BoardNativeRepository boardNativeRepository;

    @GetMapping("/")
    public String index(Model model) {

        List<Board> boardList = boardNativeRepository.findAll();
        model.addAttribute("boardList", boardList);
        log.warn("여기까지 오니");
        return "index";
    }

    // 주소설계 - http://localhost:8080/board/save-form
    // 게시글 작성 화면
    @GetMapping("/board/save-form")
    public String saveForm() {
        return "board/save-form";
    }



    // 게시글 저장
    // 주소설계 - http://localhost:8080/board/save
    @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:/";
    }


    // 특정 게시글 요청 화면
    // 주소설계 - http://localhost:8080/board/10
    @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";
    }


    // 주소설계 - http://localhost:8080/board/10/delete ( form 활용이기 때문에 delete 선언)
    // form 태크에서는 GET, POST 방식만 지원하기 때문이다.
    @PostMapping("/board/{id}/delete")
    public String delete(@PathVariable(name = "id") Integer id) {
        boardNativeRepository.deleteById(id);
        return "redirect:/";
    }


    // 게시글 수정 화면 요청
    // board/id/update
    @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"; // src/main/resources/templates/board/update-form.mustache
    }

    // 게시글 수정 요청 기능
    // board/{id}/update
    @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;
    }

}

 

 

 


 

 

Native Query와 JPQL란 뭘까?

▶ Native Query 소개

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

 

▶ JPQL(Java Persistence Query Language)란? (앞으로 배워야 하는 부분)

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

 

 

 

템플릿 작성 (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 값이 출력된다.

 

728x90