[스프링부트] 8. 블로그 만들기 v1. Prototype

문정준's avatar
Mar 18, 2025
[스프링부트] 8. 블로그 만들기 v1. Prototype
 

1. New Project

  • 새 폴더 생성, Group명 변경
    • Git으로 관리 용이하도록 변경
notion image
 
Dependencies
  • Lombok : Getter / Setter 생성
  • Spring Web : Dispatcher, Annotation 생성
  • Mustache : Template Engine
  • H2 Database : 가상 DB
  • Spring Data JPA : Java 코드로 SQL 데이터 저장
notion image
 

2. Design

  • Mustache → Template Engine 문법 사용

layout/header

<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>blog</title> </head> <body> <nav> <ul> <li> <a href="/"></a> </li> <li> <a href="/board/save-form">글쓰기</a> </li> </ul> </nav> <hr>
 

detail

<!-- 글 상세 화면 --> {{> layout/header}} <section> <a href="/board/1/update-form">수정화면 가기</a> <form action="/board/1/delete" method="post"> <button type="submit">삭제</button> </form> <div> 번호 : 1 <br> 제목 : 제목1 <br> 내용 : 내용1 <br> 작성일 : 2025.03.18 <br> </div> </section> </body> </html>
 

list

<!-- 글 목록 화면 --> {{> layout/header}} <section> <table border="1"> <tr> <th>번호</th> <th>제목</th> <th></th> </tr> <tr> <td>제목1</td> <td>내용1</td> <td><a href="/board/1">상세보기</a></td> </tr> <tr> <td>제목2</td> <td>내용2</td> <td><a href="/board/2">상세보기</a></td> </tr> </table> </section> </body> </html>
 

save-form

<!-- 글 쓰기 화면 --> {{> layout/header}} <section> <!-- http body : title=제목6&content=내용6 http header : application/x-www-form-urlencoded key값은 input태그의 name, value값은 input태그에 사용자가 입력하는 값--> <form action="/board/save" method="post" enctype="application/x-www-form-urlencoded"> <input type="text" name="title" placeholder="제목"><br> <input type="text" name="content" placeholder="내용"><br> <button type="submit">글쓰기</button> </form> </section> </body> </html>
 

update-form

<!-- 글 수정 화면 --> {{> layout/header}} <section> <form action="/board/1/update" method="post" enctype="application/x-www-form-urlencoded"> <input type="text" name="title" value="제목1"><br> <input type="text" name="content" value="내용1"><br> <button type="submit">글수정</button> </form> </section> </body> </html>
 

3. Controller

package com.metacoding.blogv1.board; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @Controller // Component Scan -> DS가 활용 public class BoardController { @GetMapping("/") public String list(){ return "list"; } @GetMapping("/board/{id}") // Pattern Matching (/board/1,2,3,...) public String detail(@PathVariable("id") int id){ return "detail"; } @GetMapping("/board/save-form") // 주소는 하이픈 사용 (-) public String saveForm(){ return "save-form"; } @GetMapping("/board/{id}/update-form") public String updateForm(@PathVariable("id") int id){ return "update-form"; } }
 

4. 결과

 

main

notion image
 

save-form

notion image
 

detail

notion image
 

update-form

notion image
 

5. DB 연결

  • H2 Database 사용 : Virtual Database (For development)
    • 메모리에 데이터 저장 : 서버 실행 시 마다 새로 생성
    • 가볍고, 환경 구현이 쉬움
    • 데이터를 유지할 수 없음 (서버를 끄면 다 지워짐)
      • JPA로 관리 가능
notion image
 

application.properties

  • 서버 설정 - 이후 실습에도 똑같이 사용할 것 (복사 후 붙혀넣기)
  • JPA : 자바 코드를 이용하여 데이터를 보관/생성할 수 있는 API
  • 쉬운 이해를 위한 request 객체 노출 : Model 대신 HttpServletRequest request 사용
# UTF-8 설정 (문자 인코딩) server.servlet.encoding.charset=UTF-8 server.servlet.encoding.force=true # DB 연결 spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:mem:test spring.datasource.username=sa spring.datasource.password= spring.h2.console.enabled=true # JPA 발동 spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true # 더미 실행 spring.sql.init.data-locations=classpath:db/data.sql spring.jpa.defer-datasource-initialization=true # request spring.mustache.servlet.expose-request-attributes=true
 

SRP & MVC Pattern

  • 각 클래스 파일에서 관리하는 기능들은 최소화(단일화)시켜 관리하는 것이 좋음
    • 디버그 시 Unit Test, WhiteBox Test에서 오류/문제점을 발견하기 용이
    • 이를 SRP (Single Responsibility Principal, 단일 책임 원칙) 라고 함
      • MVC Pattern을 구현하고 관리하는 데에 중요한 요소
notion image
 
  • MVC Pattern : 데이터베이스(Model), 화면(View), 기능(Controller)의 구조로 짜여진 서버 기능
    • Dispatcher를 사용하면서 Common Logic을 활용할 수 있어 코드 절약이 가능
    • SRP를 활용 : Controller가 할당 받을 기능을 쪼개서 구현
notion image
 
 

6. 기능 추가 / 개선

  • DB를 연결한 SRP + MVC Pattern 추가로 동적 HTML 제작
  • data.sql 작성 → DB에 Dummy data 추가

detail

<!-- 글 상세 화면 --> {{> layout/header}} <section> <a href="/board/{{board.id}}/update-form">수정화면 가기</a> <form action="/board/{{board.id}}/delete" method="post"> <button type="submit">삭제</button> </form> <div> 번호 : {{board.id}} <br> 제목 : {{board.title}} <br> 내용 : {{board.content}} <br> 작성일 : {{board.createdAt}} <br> 작성자 : {{board.nickname}} <br> </div> </section> </body> </html>
 

list

<!-- 글 목록 화면 --> {{> layout/header}} <section> <table border="1"> <tr> <th>번호</th> <th>제목</th> <th></th> </tr> {{#models}} <tr> <td>{{id}}</td> <td>{{title}}</td> <td><a href="/board/{{id}}">상세보기</a></td> </tr> {{/models}} </table> </section> </body> </html>
 

save-form

<!-- 글 쓰기 화면 --> {{> layout/header}} <section> <!-- http body : title=제목6&content=내용6 http header : application/x-www-form-urlencoded key값은 input태그의 name, value값은 input태그에 사용자가 입력하는 값--> <form action="/board/save" method="post" enctype="application/x-www-form-urlencoded"> <input type="text" name="title" placeholder="제목"><br> <input type="text" name="content" placeholder="내용"><br> <input type="text" name="nickname" placeholder="작성자"><br> <button type="submit">글쓰기</button> </form> </section> </body> </html>
 

update-form

<!-- 글 수정 화면 --> {{> layout/header}} <section> <form action="/board/{{board.id}}/update" method="post" enctype="application/x-www-form-urlencoded"> <input type="text" name="title" value={{board.title}}><br> <input type="text" name="content" value={{board.content}}><br> <button type="submit">글수정</button> </form> </section> </body> </html>
 

Controllers : DI (Dependency Injection)

BoardController

package com.metacoding.blogv1.board; import jakarta.persistence.EntityManager; import jakarta.servlet.http.HttpServletRequest; 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 java.util.List; // 책임 : 요청 잘 받고 응답 잘 하고 @Controller // Component Scan -> DS가 활용 public class BoardController { private BoardService boardService; public BoardController(BoardService boardService) { this.boardService = boardService; } @PostMapping("/board/save") public String save(String title, String content, String nickname) { boardService.게시글쓰기(title, content, nickname); return "redirect:/"; // 주소가 이미 만들어져 있으면 리다이렉션 } @PostMapping("/board/{id}/delete") public String delete(@PathVariable("id") int id) { boardService.게시글삭제(id); return "redirect:/"; } @PostMapping("/board/{id}/update") public String update(@PathVariable("id") int id, String title, String content) { // update board_tb set title=?, content=? where id=? // 주소로 받는 값은 where에 걸린다 boardService.게시글수정(id, title, content); return "redirect:/board/" + id; } @GetMapping("/") public String list(HttpServletRequest request) { List<Board> boardList = boardService.게시글목록(); request.setAttribute("models", boardList); // request 담기 return "list"; // forward } @GetMapping("/board/{id}") // Pattern Matching (/board/1,2,3,...) public String detail(@PathVariable("id") int id, HttpServletRequest request) { Board board = boardService.게시글상세보기(id); request.setAttribute("board", board); return "detail"; } @GetMapping("/board/save-form") // 주소는 하이픈 사용 (-) public String saveForm() { return "save-form"; } @GetMapping("/board/{id}/update-form") public String updateForm(@PathVariable("id") int id, HttpServletRequest request) { Board board = boardService.게시글수정화면(id); request.setAttribute("board", board); return "update-form"; } }
 

BoardService

package com.metacoding.blogv1.board; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; // 책임 : 트랜잭션 처리, Business Logic 처리 @Service // IoC public class BoardService { private BoardRepository boardRepository; // DI : 의존성 주입 public BoardService(BoardRepository boardRepository) { this.boardRepository = boardRepository; } @Transactional // 트랜잭션 시작 -> 함수 내부 전부 완료 시 commit, 실패 시 rollback public void 게시글쓰기(String title, String content, String nickname) { boardRepository.insert(title, content, nickname); } public List<Board> 게시글목록() { List<Board> boardList = boardRepository.findAll(); return boardList; } public Board 게시글상세보기(int id) { Board board = boardRepository.find(id); return board; } @Transactional public void 게시글삭제(int id) { // 1. 게시글 존재 확인 Board board = boardRepository.find(id); // 2. 삭제 if(board == null) { throw new RuntimeException("게시글이 존재하지 않습니다."); } else boardRepository.delete(id); } @Transactional public void 게시글수정(int id, String title, String content) { boardRepository.update(id, title, content); } public Board 게시글수정화면(int id) { return boardRepository.find(id); } }
 

BoardRepository

package com.metacoding.blogv1.board; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.util.List; // 책임 : DB와 소통 @Repository // IoC Container public class BoardRepository { private EntityManager em; // DI -> IoC 순회해서 타입으로 찾아서 전달 public BoardRepository(EntityManager em) { System.out.println("new BoardRepository constructor"); this.em = em; } public void insert(String title, String content, String nickname) { Query query = em.createNativeQuery("insert into board_tb(title, content, nickname, created_At) values (?, ?, ?, now())"); query.setParameter(1, title); query.setParameter(2, content); query.setParameter(3, nickname); query.executeUpdate(); } public List<Board> findAll() { Query query = em.createNativeQuery("select * from board_tb order by id desc", Board.class); List<Board> boardList = query.getResultList(); return boardList; } public Board find(int id) { Query query = em.createNativeQuery("select * from board_tb where id = ?", Board.class); query.setParameter(1, id); Board board = (Board) query.getSingleResult(); return board; } public void delete(int id) { Query query = em.createNativeQuery("delete from board_tb where id = ?"); query.setParameter(1, id); query.executeUpdate(); } public void update(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(); } }
 

Board : DB Data

  • JPA를 이용한 DDL, Model 동시 구현
package com.metacoding.blogv1.board; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import java.sql.Timestamp; @Getter @AllArgsConstructor // Full Constructor @NoArgsConstructor // Default Constructor @Table(name = "board_tb") // table명 설정 @Entity // JPA가 관리할 수 있게 설정 public class Board { @Id // PK 설정 @GeneratedValue(strategy = GenerationType.IDENTITY) // auto-increment 설정 private Integer id; private String title; private String content; private Timestamp createdAt; private String nickname; }
 

data.sql

  • Dummy data : 서버 실행 시 마다 초기화되어 새로 생성
insert into board_tb(title, content, nickname, created_at) values('제목1', '내용1', 'Son', now()); insert into board_tb(title, content, nickname, created_at) values('제목2', '내용2', 'Sala', now()); insert into board_tb(title, content, nickname, created_at) values('제목3', '내용3', 'Jackson', now()); insert into board_tb(title, content, nickname, created_at) values('제목4', '내용4', 'Kim', now()); insert into board_tb(title, content, nickname, created_at) values('제목5', '내용5', 'ssar', now());
 

7. 최종 결과

DB
Dummy Data
notion image
Insert (save)
notion image
Update
notion image
Delete
  • 6번 글 상세 보기 → 삭제 버튼 클릭 시 데이터 삭제 후 메인 화면 이동 (글 최신화)
notion image
Display
Main Page (welcome)
notion image
Detail
notion image
Save-form
notion image
  • 데이터 입력 후 ‘글쓰기’ 버튼 클릭 시 메인 화면으로 이동 (글 최신화)
notion image
Update-form
  • 2번 글 상세보기 → 수정화면 이동 후 내용 수정
notion image
  • ‘글수정’ 버튼 클릭 시 해당 글 상세 화면 이동
notion image
Share article

sxias