1. 프로젝트 생성


프로젝트 경로

2. 프로젝트 준비
1. mustache 파일

🧔templates/layout/header.mustache
<!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>
🧔templates/detail.mustache
{{> layout/header}}
<section>
<a href="/board/{{model.id}}/update-form">수정</a>
<form action="/board/{{model.id}}/delete" method="post">
<button type="submit">삭제</button>
</form>
<div>
번호 : 1 <br>
제목 : 제목1 <br>
내용 : 내용1 <br>
작성일 : 2025.05.05 <br>
</div>
</section>
</body>
</html>
🧔templates/list.mustache
<!-- 부분 템플릿 layout폴더 안에 header파일을 넣겠다 -->
{{> layout/header}}
<section>
<table border="1">
<tr>
<th>ID</th>
<th>제목</th>
<th>내용</th>
<th>상세보기</th>
</tr>
<tr>
<td>1</td>
<td>제목1</td>
<td>내용1</td>
<td><a href="/board/1">상세보기</a></td>
</tr>
<tr>
<td>2</td>
<td>제목2</td>
<td>내용2</td>
<td><a href="/board/2">상세보기</a></td>
</tr>
</table>
</section>
</body>
</html>
🧔templates/save-form.mustache
{{> 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>
🧔templates/update-form.mustache
{{> layout/header}}
<section>
<form action="/board/1/update" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="title" value="제목"><br>
<input type="text" name="content" value="내용"><br>
<button type="submit">글수정</button>
</form>
</section>
</body>
</html>
2. db 설정
☕com.metacoding.blogv1.board
package com.metacoding.blogv1.board;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.sql.Timestamp;
// 런타임에 lombok이 어노테이션 보고 생성해줌
@Getter
@AllArgsConstructor // 풀 생성자
@NoArgsConstructor // 디폴트 생성자 어노테이션
@Entity // jpa가 관리할 수 있게 설정
@Table(name = "board_tb") // 테이블 명 설정
public class Board {
@Id // pk 설정
@GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment 설정
private Integer id;
private String title;
private String content;
private Timestamp createdAt;
}
resources/db/data.sql
insert into board_tb(title, content, created_at)
values ('제목1', '내용1', now());
insert into board_tb(title, content, created_at)
values ('제목2', '내용2', now());
insert into board_tb(title, content, created_at)
values ('제목3', '내용3', now());
insert into board_tb(title, content, created_at)
values ('제목4', '내용4', now());
insert into board_tb(title, content, created_at)
values ('제목5', '내용5', now());
resources/application.properties
# utf-8 한글 인코딩, 인코딩 강제
server.servlet.encoding.charset=utf-8
server.servlet.encoding.force=true
# DB 연결
## EntityManager 생성 후 IoC에 추가
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
## entity scan @Entity 스캔해서 테이블 생성
spring.jpa.hibernate.ddl-auto=create
# 콘솔에 쿼리 로그 표기
spring.jpa.show-sql=true
# 더미 데이터 sql문 실행
## classpath: -> resources 폴더를 가리킴
spring.sql.init.data-locations=classpath:db/data.sql
## ddl-auto가 실행된 후에 sql init 이 실행 되도록 지연 시작
spring.jpa.defer-datasource-initialization=true
# request 객체에 접근 할 수 있도록 설정
spring.mustache.servlet.expose-request-attributes=true
h2-console
- 데이터 삽입 확인

3. 프로젝트 수행
SRP
하나의 클래스(또는 모듈, 함수)가 오직 하나의 책임(변화의 이유)만 가져야 한다는 원칙

- 유지보수성(Manageability) 향상
- 코드 재사용성(Reusability) 증가
- 테스트 용이성(Testability) 증가
- 협업 시 충돌 방지

단일 책임 원칙을 사용하면 문제가 발생 했을 때 해당 문제를 발생 시킨 책임을 가지고 있는 놈을 찾아 해결하면 되기 때문에 관리하기 좋다
- 위의 원칙에 따라
- Controller
- Service
- Repository
- 를 만들어 처리한다
☕1. Controller
요청 잘 받고 응답 잘 하는 친구
아래의 코드는 js를 안쓰는 관계로 http 1.0 버전인 post방식으로 insert, update, delete를 처리한다
package com.metacoding.blogv1.board;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
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 // 컴포넌트 스캔 -> DS(디스패처)가 활용
public class BoardController {
private BoardService boardService;
public BoardController(BoardService boardService) {
this.boardService = boardService;
}
@PostMapping("/board/{id}/update") // 주소로 받는 값은 다 where절에 걸린다
public String update(@PathVariable("id") int id, String title, String content, String nickname) {
// update board_tb set title=?, content=? where id=?
// get /emp/emptno/1
// get /emp/personno/1 -> 유니크키 or 프라이머리키는 /를 사용한다
// get /emp?job=manager -> 다른 모든 where절은 ? 를 사용한다
boardService.게시글수정(id, title, content, nickname);
return "redirect:/board/" + id;
}
@PostMapping("/board/{id}/delete") // http 1.0 버전 post방식으로 insert, update, delete를 모두 처리함. 주소 끝에 동사를 작성
public String delete(@PathVariable("id") int id) {
boardService.게시글삭제(id);
return "redirect:/";
}
@PostMapping("/board/save")
public String save(String title, String content, String nickname) { // xwform으로 온 데이터는 바로 받을 수 있다
boardService.게시글쓰기(title, content, nickname);
return "redirect:/"; // 주소가 있는 파일은 무조건 주소로 리다이렉션
}
@GetMapping("/")
public String list(HttpServletRequest request) {
List<Board> boardlist = boardService.게시글목록();
request.setAttribute("models", boardlist); // req에 담기
return "list"; // forward 하기
}
@GetMapping("/board/{id}") // {} -> 패턴 매칭, /board/1 or /board/2
public String detail(@PathVariable("id") int id, HttpServletRequest request) { // 패턴 매칭 값을 @PathVariable 어노테이션으로 받아 올 수 있다
Board board = boardService.게시글상세보기(id);
request.setAttribute("model", 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("model", board);
return "update-form";
}
}
- @PathVariable → 요청 주소에 {변수} 에 들어간 변수를 받을 수 있는 어노테이션
- @PathVariable(”id”) → id는 uri의 {id}와 동일해야 한다
- 주소에서 띄어쓰기를 사용하고 싶다면 하이픈(-)를 사용해야 한다
☕2. Service
트랜잭션 처리, 비지니스 로직 처리하는 친구
package com.metacoding.blogv1.board;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
// 책임 : 트랜잭션 처리, 비지니스 로직 처리
@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) {
return boardRepository.findById(id);
}
@Transactional
public void 게시글삭제(int id) {
// 1. 게시글 존재 확인
// -> insert, update, delete는 db를 고립시키고 기다림을 만든다
// 따라서 미리 존재하는지 확인하는게 속도가 빠르다
Board board = boardRepository.findById(id);
// 2. 게시글 삭제
if (board == null) {
throw new RuntimeException("게시글이 없는데 왜 삭제 요청을...");
} else {
boardRepository.deleteById(id);
}
} // commit
@Transactional
public void 게시글수정(int id, String title, String content, String nickname) {
Board board = boardRepository.findById(id);
// 2. 게시글 삭제
if (board == null) {
throw new RuntimeException("게시글이 없는데 왜 수정 요청을...");
} else {
boardRepository.update(id, title, content, nickname);
}
}
}
- @Transactional → 이 어노테인션을 사용하면 중간에 에러가 발생하면 이전 상태로 돌아감
☕3. Repository
DB와 직접 통신하는 친구
package com.metacoding.blogv1.board;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
// 책임 : DB와 소통하는 친구
@Repository // IoC 컬렉션에 뜬다. 컴포넌트를 스프링이 new해서 담고 있는 곳. @Component가 있는 클래스를 담는다. / Repository -> 저장소라는 의미로 쓰인듯
public class BoardRepository {
private EntityManager em;
// IoC를 순회해서 type으로 찾아서 전달해줌
public BoardRepository(EntityManager em) {
this.em = em;
}
public void insert(String title, String content, String nickname) {
Query query = em.createNativeQuery("insert into board_tb(title,content,created_at,nickname) values(?,?,now(),?)");
query.setParameter(1, title);
query.setParameter(2, content);
query.setParameter(3, nickname);
query.executeUpdate(); // insert, update, delete
}
public List<Board> findAll() {
Query query = em.createNativeQuery("select * from board_tb order by id desc", Board.class);
List<Board> boardList = query.getResultList(); // select
return boardList;
}
public Board findById(int id) {
Query query = em.createNativeQuery("select * from board_tb where id=?", Board.class);
query.setParameter(1, id);
try {
return (Board) query.getSingleResult();
} catch (Exception e) {
return null;
}
}
public void deleteById(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, String nickname) {
Query query = em.createNativeQuery("update board_tb set title=?,content=?,nickname=? where id=?");
query.setParameter(1, title);
query.setParameter(2, content);
query.setParameter(3, nickname);
query.setParameter(4, id);
query.executeUpdate();
}
}
- EntityManager → spring이 만들어주는 db접근 객체
- executeUpdate() → insert, update, delete 문을 사용할 때만 사용한다
- getResultList() → 받아오는 데이터가 복수일 때
- getSingleResult() → 받아오는 데이터가 단수일 때
🧔4. mustache 문법
반복문
<!-- 부분 템플릿 layout폴더 안에 header파일을 넣겠다 -->
{{> layout/header}}
<section>
<table border="1">
<tr>
<th>ID</th>
<th>제목</th>
<th>내용</th>
<th>상세보기</th>
<th>닉네임</th>
</tr>
{{#models}}
<tr>
<td>{{id}}</td>
<td>{{title}}</td>
<td>{{content}}</td>
<td><a href="/board/{{id}}">상세보기</a></td>
<td>{{nickname}}</td>
</tr>
{{/models}}
</table>
</section>
</body>
</html>
- 반복문에선 시작은 key값을 사용 models
- 반복내용에선 각 model의 value값을 사용
변수 사용
{{> layout/header}}
<section>
<a href="/board/{{model.id}}/update-form">수정</a>
<form action="/board/{{model.id}}/delete" method="post">
<button type="submit">삭제</button>
</form>
<div>
번호 : {{model.id}} <br>
제목 : {{model.title}} <br>
내용 : {{model.content}} <br>
작성일 : {{model.createdAt}} <br>
닉네임 : {{model.nickname}} <br>
</div>
</section>
</body>
</html>
- setAttribute() 로 넣은 key값으로 접근한다
- “model” 이라고 작성했으니 model 로 접근
Share article