[Spring Boot] 5. Spring Boot Project (Blog v1)

김미숙's avatar
Mar 27, 2025
[Spring Boot] 5. Spring Boot Project (Blog v1)
notion image
notion image

 
notion image

BoardController

package com.metacoding.blogv1.board; 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; // 책임 : 요청 잘 받고, 응답 잘 하고 @Controller // 컴퍼넌트 스캔 --> DS가 활용 public class BoardController { private BoardService boardService; public BoardController(BoardService boardService) { this.boardService = boardService; } @PostMapping("/board/{id}/update") public String update(@PathVariable("id") int id, String title, String content) { // update board_tb set title=?, content=? where id=? // 주소로 받는 데이터는 전부 다 where에 걸린다 // 프라이머리키,유니크키 -> / 나머지는 -> ?쿼리스트링 System.out.println("id: " + id + " title: " + title + " content: " + content); return "redirect:/board/" + id; } @PostMapping("/board/{id}/delete") public String delete(@PathVariable("id") int id) { System.out.println("id: " + id); // id로 db가서 삭제하면 됨 return "redirect:/"; } @PostMapping("/board/save") // 수행이 끝나면 리다이렉션이 일어난다 public String save(String title, String content) { // input의 name값과 동일해야한다 boardService.게시글쓰기(title, content); return "redirect:/"; // 해당 페이지로 가는 주소가 있으면 무조건 redirection } @GetMapping("/") public String list() { return "list"; } @GetMapping("/board/{id}") // 패턴 매칭 /board/1,2,3 ... public String detail(@PathVariable("id") int id) { // 주소에 들어오는 숫자값을 id에 받을 수 있는 annotation return "detail"; } @GetMapping("/board/save-form") // 주소 (하이픈(-) 사용) public String saveForm() { return "save-form"; // viewResolver를 타기 때문에 확장자를 적지 않아도 된다 } @GetMapping("/board/{id}/update-form") public String updateForm(@PathVariable("id") int id) { return "update-form"; } }

BoardService

package com.metacoding.blogv1.board; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; // 책임: 트랜잭션 처리 @Service // IoC public class BoardService { private BoardRepository boardRepository; // DI: 의존성주입 -> IoC로 부터 들고옴 public BoardService(BoardRepository boardRepository) { this.boardRepository = boardRepository; } @Transactional // 트랜잭션이 시작 -> 함수 내부가 다 수행되면 commit, 실패하면 rollback //springframework public void 게시글쓰기(String title, String content) { boardRepository.insert(title, content); } }

Board

package com.metacoding.blogv1.board; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import java.sql.Timestamp; @Getter @AllArgsConstructor // 풀 생성자 @NoArgsConstructor // 디폴트 생성자 @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; }

BoardRepository

package com.metacoding.blogv1.board; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import org.springframework.stereotype.Repository; // 책임: DB와 소통하는 친구 @Repository // IoC(제어의역전) 컬렉션에 뜬다 public class BoardRepository { private EntityManager em; // DI -> 의존성 주입 IoC를 순회해서 타입으로 찾아서 전달해준다 public BoardRepository(EntityManager em) { System.out.println("BoardRepository new 됨"); this.em = em; } public void insert(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(); } }

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());

application.properties

# utf-8 한글 인코딩 server.servlet.encoding.charset=utf-8 server.servlet.encoding.force=true # DB 연결 코드 (EntityManager 만들어냄) 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 스캔해서 테이블 생성 spring.jpa.hibernate.ddl-auto=create # 콘솔에 쿼리 표시 spring.jpa.show-sql=true # 더미데이터 sql문 실행 spring.sql.init.data-locations=classpath:db/data.sql # ddl-auto가 실행된 후에 sql문 실행하는 법 spring.jpa.defer-datasource-initialization=true # mustache에서 request 객체 접근하게 설정하는 법 spring.mustache.servlet.expose-request-attributes=true
 

h2-console

notion image
 

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="/save-form">글쓰기</a> </li> </ul> </nav> <hr>
 

list.mustache

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

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> 번호 : {{model.id}} <br> 제목 : {{model.title}} <br> 내용 : {{model.content}} <br> 작성일 : {{model.createdAt}} <br> </div> </section> </body> </html>

save-form.mustache

‼️
값을 적는 화면에는 주소에 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.mustache

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

Q1. 테이블에 nickname 필드 추가, 게시글 쓰기할때 nickname 받기, 게시글 상세보기에서 nickname 확인

BoardController

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") public String update(@PathVariable("id") int id, String title, String content) { // update board_tb set title=?, content=? where id=? // 주소로 받는 데이터는 전부 다 where에 걸린다 // 프라이머리키,유니크키 -> / 나머지는 -> ?쿼리스트링 System.out.println("id: " + id + " title: " + title + " content: " + content); return "redirect:/board/" + id; } @PostMapping("/board/{id}/delete") public String delete(@PathVariable("id") int id) { boardService.게시글삭제(id); System.out.println("id: " + id); // id로 db가서 삭제하면 됨 return "redirect:/"; } @PostMapping("/board/save") // 수행이 끝나면 리다이렉션이 일어난다 public String save(String title, String content, String nickname) { // input의 name값과 동일해야한다 boardService.게시글쓰기(title, content, nickname); return "redirect:/"; // 해당 페이지로 가는 주소가 있으면 무조건 redirection } // C -> M -> V @GetMapping("/") public String list(HttpServletRequest request) { List<Board> boardList = boardService.게시글목록(); request.setAttribute("models", boardList); // forward(request에 담기) return "list"; } @GetMapping("/board/{id}") // 패턴 매칭 /board/1,2,3 ... public String detail(@PathVariable("id") int id, HttpServletRequest request) { // 주소에 들어오는 숫자값을 id에 받을 수 있는 annotation Board board = boardService.게시글상세보기(id); request.setAttribute("model", board); return "detail"; } @GetMapping("/board/save-form") // 주소 (하이픈(-) 사용) public String saveForm() { return "save-form"; // viewResolver를 타기 때문에 확장자를 적지 않아도 된다 } @GetMapping("/board/{id}/update-form") public String updateForm(@PathVariable("id") int id) { return "update-form"; } }

BoardService

package com.metacoding.blogv1.board; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; // 책임: 트랜잭션 처리 , 비즈니스 로직 처리 (ex. 송금 전 잔액검사) @Service // IoC public class BoardService { private BoardRepository boardRepository; // DI: 의존성주입 -> IoC로 부터 들고옴 public BoardService(BoardRepository boardRepository) { this.boardRepository = boardRepository; } @Transactional // 트랜잭션이 시작 -> 함수 내부가 다 수행되면 commit, 실패하면 rollback (write일때만) //springframework public void 게시글쓰기(String title, String content, String nickname) { boardRepository.insert(title, content, nickname); } public List<Board> 게시글목록() { List<Board> boardList = boardRepository.findALl(); // boardRepository한테 위임 return boardList; } public Board 게시글상세보기(int id) { return boardRepository.findById(id); } @Transactional public void 게시글삭제(int id) { // 확실할때만 삭제해야하기 때문에 로직이 필요하다 // 1. 게시글이 존재하는지 확인 Board board = boardRepository.findById(id); // 2. 삭제 if (board == null) { throw new RuntimeException("게시글이 없는데 왜 삭제?"); } boardRepository.deleteById(id); } // commit, 터지면 rollback }

Board

package com.metacoding.blogv1.board; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import java.sql.Timestamp; @Getter @AllArgsConstructor // 풀 생성자 @NoArgsConstructor // 디폴트 생성자 @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 String nickname; private Timestamp createdAt; }

BoardRepository

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(제어의역전) 컬렉션에 뜬다 public class BoardRepository { private EntityManager em; // DI -> 의존성 주입 IoC를 순회해서 타입으로 찾아서 전달해준다 public BoardRepository(EntityManager em) { System.out.println("BoardRepository new 됨"); 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(); // insert, update, delete 일때만 } public List<Board> findALl() { Query query = em.createNativeQuery("select * from board_tb order by id desc", Board.class); // Board.class 를 쓰면 while 돌면서 board 클래스로 자동 매핑 List<Board> boardList = query.getResultList(); return boardList; // service한테 return } public Board findById(int id) { Query query = em.createNativeQuery("select * from board_tb where id = ?", Board.class); query.setParameter(1, id); try { Board board = (Board) query.getSingleResult(); // 한건이면 query.getSingleResult() return board; } 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(); } }

data.sql

insert into board_tb(title, content, nickname, created_at) values ('제목1', '내용1', '닉네임1', now()); insert into board_tb(title, content, nickname, created_at) values ('제목2', '내용2', '닉네임2', now()); insert into board_tb(title, content, nickname, created_at) values ('제목3', '내용3', '닉네임3', now()); insert into board_tb(title, content, nickname, created_at) values ('제목4', '내용4', '닉네임4', now()); insert into board_tb(title, content, nickname, created_at) values ('제목5', '내용5', '닉네임5', now());

application.properties

‼️
주석에 한글 작성하면 서버 실행하면서 깨지니까
서버 실행 전 백업 먼저 하기
# utf-8 한글 인코딩 server.servlet.encoding.charset=utf-8 server.servlet.encoding.force=true # DB 연결 코드 (EntityManager 만들어냄) 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 스캔해서 테이블 생성 spring.jpa.hibernate.ddl-auto=create # 콘솔에 쿼리 표시 spring.jpa.show-sql=true # 더미데이터 sql문 실행 spring.sql.init.data-locations=classpath:db/data.sql # ddl-auto가 실행된 후에 sql문 실행하는 법 spring.jpa.defer-datasource-initialization=true # mustache에서 request 객체 접근하게 설정하는 법 spring.mustache.servlet.expose-request-attributes=true
 

list.mustache

{{> layout/header}} <section> <table border="1"> <tr> <th>번호</th> <th>제목</th> <th>닉네임</th> <th>상세보기</th> </tr> {{#models}} <tr> <td>{{id}}</td> <td>{{title}}</td> <td>{{nickname}}</td> <td><a href="/board/{{id}}">상세보기</a></td> </tr> {{/models}} </table> </section> </body> </html>

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> 번호 : {{model.id}} <br> 제목 : {{model.title}} <br> 내용 : {{model.content}} <br> 닉네임 : {{model.nickname}} <br> 작성일 : {{model.createdAt}} <br> </div> </section> </body> </html>

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> <input type="text" name="nickname" placeholder="닉네임"><br> <button type="submit">글쓰기</button> </form> </section> </body> </html>

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="제목1"><br> <input type="text" name="content" value="내용1"><br> <input type="text" name="nickname" value="닉네임"><br> <button type="submit">글수정</button> </form> </section> </body> </html>

list page

notion image

save-form page

notion image

detail page

notion image

update-form page

notion image

게시글 추가하기

➡ 6번 게시글 추가해보기
notion image
글쓰기 버튼을 누르면 list page로 redirection되면서 6번 게시글이 추가된 것을 확인할 수 있다
notion image
⬇ detail 페이지에서 제목, 내용, 닉네임 확인 가능
notion image
글쓰기 버튼을 누르면 DB에서도 동일하게 6번 게시글이 추가된 것을 확인할 수 있다
notion image
 

게시글 삭제하기

➡ 6번 게시글 삭제
notion image
삭제 버튼을 누르면 list page로 redirection되면서 6번 게시글이 사라진 것을 확인할 수 있다
notion image
삭제 버튼을 눌렀을 때 DB에서도 동일하게 삭제된 것을 확인할 수 있다
notion image
 

게시글 수정하기

update-form

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

BoardRepository

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(제어의역전) 컬렉션에 뜬다 public class BoardRepository { private EntityManager em; // DI -> 의존성 주입 IoC를 순회해서 타입으로 찾아서 전달해준다 public BoardRepository(EntityManager em) { System.out.println("BoardRepository new 됨"); 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(); // insert, update, delete 일때만 } public List<Board> findALl() { Query query = em.createNativeQuery("select * from board_tb order by id desc", Board.class); // Board.class 를 쓰면 while 돌면서 board 클래스로 자동 매핑 List<Board> boardList = query.getResultList(); return boardList; // service한테 return } public Board findById(int id) { Query query = em.createNativeQuery("select * from board_tb where id = ?", Board.class); query.setParameter(1, id); try { Board board = (Board) query.getSingleResult(); // 한건이면 query.getSingleResult() return board; } 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(); } }

BoardController

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") public String update(@PathVariable("id") int id, String title, String content, String nickname) { // update board_tb set title=?, content=?, nickname=? where id=? // 주소로 받는 데이터는 전부 다 where에 걸린다 // 프라이머리키,유니크키 -> / 나머지는 -> ?쿼리스트링 System.out.println("id: " + id + " title: " + title + " content: " + content + " nickname: " + nickname); boardService.게시글수정하기(id, title, content, nickname); return "redirect:/board/" + id; } @PostMapping("/board/{id}/delete") public String delete(@PathVariable("id") int id) { boardService.게시글삭제(id); System.out.println("id: " + id); // id로 db가서 삭제하면 됨 return "redirect:/"; } @PostMapping("/board/save") // 수행이 끝나면 리다이렉션이 일어난다 public String save(String title, String content, String nickname) { // input의 name값과 동일해야한다 boardService.게시글쓰기(title, content, nickname); return "redirect:/"; // 해당 페이지로 가는 주소가 있으면 무조건 redirection } // C -> M -> V @GetMapping("/") public String list(HttpServletRequest request) { List<Board> boardList = boardService.게시글목록(); request.setAttribute("models", boardList); // forward(request에 담기) 여러건이면 models(컨벤션) return "list"; } @GetMapping("/board/{id}") // 패턴 매칭 /board/1,2,3 ... public String detail(@PathVariable("id") int id, HttpServletRequest request) { // 주소에 들어오는 숫자값을 id에 받을 수 있는 annotation Board board = boardService.게시글상세보기(id); request.setAttribute("model", board); // 단건이면 model (컨벤션) return "detail"; } @GetMapping("/board/save-form") // 주소 (하이픈(-) 사용) public String saveForm() { return "save-form"; // viewResolver를 타기 때문에 확장자를 적지 않아도 된다 } @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"; } }

BoardService

package com.metacoding.blogv1.board; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; // 책임: 트랜잭션 처리 , 비즈니스 로직 처리 (ex. 송금 전 잔액검사) @Service // IoC public class BoardService { private BoardRepository boardRepository; // DI: 의존성주입 -> IoC로 부터 들고옴 public BoardService(BoardRepository boardRepository) { this.boardRepository = boardRepository; } @Transactional // 트랜잭션이 시작 -> 함수 내부가 다 수행되면 commit, 실패하면 rollback (write일때만) //springframework public void 게시글쓰기(String title, String content, String nickname) { boardRepository.insert(title, content, nickname); } public List<Board> 게시글목록() { List<Board> boardList = boardRepository.findALl(); // boardRepository한테 위임 return boardList; } public Board 게시글상세보기(int id) { return boardRepository.findById(id); } @Transactional public void 게시글삭제(int id) { // 확실할때만 삭제해야하기 때문에 로직이 필요하다 // 1. 게시글이 존재하는지 확인 Board board = boardRepository.findById(id); // 2. 삭제 if (board == null) { throw new RuntimeException("게시글이 없는데 왜 삭제?"); } boardRepository.deleteById(id); } // commit, 터지면 rollback @Transactional public void 게시글수정하기(int id, String title, String content, String nickname) { // 게시글이 있어야하기 때문에 로직이 필요하다 // 1. 게시글이 존재하는지 확인 Board board = boardRepository.findById(id); // 2. 수정 if (board == null) { throw new RuntimeException("게시글이 없는데 왜 수정?"); } boardRepository.update(id, title, content, nickname); } }
 
➡ 5번 게시글 수정
notion image
삭제 버튼을 누르면 list page로 redirection되면서 6번 게시글이 사라진 것을 확인할 수 있다
notion image
삭제 버튼을 눌렀을 때 DB에서도 동일하게 삭제된 것을 확인할 수 있다
notion image
 
 
 
 
Share article

parangdajavous