1. New Project
- 새 폴더 생성, Group명 변경
- Git으로 관리 용이하도록 변경

Dependencies
- Lombok : Getter / Setter 생성
- Spring Web : Dispatcher, Annotation 생성
- Mustache : Template Engine
- H2 Database : 가상 DB
- Spring Data JPA : Java 코드로 SQL 데이터 저장

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

save-form

detail

update-form

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

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을 구현하고 관리하는 데에 중요한 요소

- MVC Pattern : 데이터베이스(Model), 화면(View), 기능(Controller)의 구조로 짜여진 서버 기능
- Dispatcher를 사용하면서 Common Logic을 활용할 수 있어 코드 절약이 가능
- SRP를 활용 : Controller가 할당 받을 기능을 쪼개서 구현

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

Insert (save)

Update

Delete
- 6번 글 상세 보기 → 삭제 버튼 클릭 시 데이터 삭제 후 메인 화면 이동 (글 최신화)

Display
Main Page (welcome)

Detail

Save-form

- 데이터 입력 후 ‘글쓰기’ 버튼 클릭 시 메인 화면으로 이동 (글 최신화)

Update-form
- 2번 글 상세보기 → 수정화면 이동 후 내용 수정

- ‘글수정’ 버튼 클릭 시 해당 글 상세 화면 이동

Share article