7. (실습) 스프링 V1 (기본 - CRUD) (3)

박은서's avatar
Feb 05, 2026
7. (실습) 스프링 V1 (기본 - CRUD) (3)
💡
MVC (Model + View + Controller)
외부에서 컨트롤러에 요청 → 컨트롤러(C)는 DB에서 모델(M) 받아서 뷰(V)로 뿌림

10. 게시글 목록(메인화면)

1) BoardController.java

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌 @Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점) public class BoardController { private final BoardService boardService; @GetMapping("/") public String index(HttpServletRequest req){ List<Board> list = boardService.게시글목록(); req.setAttribute("models", list); return "index"; } }
BoardController에서 먼저 service의 메서드 이름 적고, alt+enter로 메서드 만들기 하면 됨

2) BoardService.java

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service // 컴포넌트 스캔되서 IoC에 뜸 + 목적 : 트랜잭션 관리 public class BoardService { private final BoardRepository boardRepository; public List<Board> 게시글목록() { return boardRepository.findAll(); } }

3) index.mustache 수정

{{> header}} <div class="container mt-3"> <table class="table table-hover"> <thead> <tr> <th>번호</th> <th>제목</th> <th>내용</th> </tr> </thead> <tbody> {{#models}} <tr> <td>{{id}}</td> <td> <a href="/boards/{{id}}" style="color: inherit; text-decoration: none;"> {{title}} </a> </td> <td>{{content}}</td> </tr> {{/models}} </tbody> </table> </div> </body> </html>

11. 게시글 상세

1) BoardController.java

package com.example.boardv1.board; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌 @Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점) public class BoardController { private final BoardService boardService; @GetMapping("/boards/{id}") public String detail(@PathVariable("id") int id, HttpServletRequest req){ Board board = boardService.상세보기(id); req.setAttribute("model", board); return "board/detail"; // mustache 파일의 경로 } }

2) BoardService.java

package com.example.boardv1.board; import org.springframework.stereotype.Service; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service // 컴포넌트 스캔되서 IoC에 뜸 + 목적 : 트랜잭션 관리 public class BoardService { private final BoardRepository boardRepository; public Board 상세보기(int id) { return boardRepository.findById(id); } }

3) detail.mustache 수정

{{> header}} <div class="container p-5"> <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <a href = "/boards/{{model.id}}/update-form" class="btn btn-secondary me-1">수정</a> <button class="btn btn-outline-secondary">삭제</button> </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr /> <div class="d-flex justify-content-end"> 작성자 : 익명 </div> <div class="m-4 p-2"> {{model.content}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/replies/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button class="btn btn-secondary mt-1"> 댓글등록 </button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-secondary text-white rounded">익명</div> <div>댓글 내용2</div> </div> <form action="/replies/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-secondary text-white rounded">익명</div> <div>댓글 내용1</div> </div> <form action="/replies/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> </div> </div> </div> </body> </html>

12. 게시글 수정

1) BoardController.java

package com.example.boardv1.board; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌 @Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점) public class BoardController { private final BoardService boardService; // body : title=제목&content=내용 @PostMapping("/boards/{id}/update") public String update(@PathVariable("id") int id, @RequestParam("title") String title, @RequestParam("content") String content){ boardService.게시글수정(id,title,content); return "redirect:/boards/"+id; } }

2) BoardService.java

package com.example.boardv1.board; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service // 컴포넌트 스캔되서 IoC에 뜸 + 목적 : 트랜잭션 관리 public class BoardService { private final BoardRepository boardRepository; @Transactional // update, delete, insert 할 때 붙이기!! import 주의! springframework(O) 자카르타(X) public void 게시글수정(int id, String title, String content) { Board board = boardRepository.findById(id); board.setTitle(title); board.setContent(content); // 자동 flush } }
💡
@Transactional 의 역할
  1. 원자성
    1. 모든 것이 다 되면 commit, 하나라도 실패하면 rollback
  1. 자동 flush()
    1. 트랜잭션 종료 시 자동으로 flush 됨

3) update-form.mustache 수정

{{> header}} <div class="container p-5"> <div class="card"> <div class="card-header"><b>게시글 수정</b></div> <div class="card-body"> <form action="/boards/{{model.id}}/update" method="post" enctype="application/x-www-form-urlencoded"> <div class="mb-3"> <input type="text" class="form-control" placeholder="Enter title" name="title" value="{{model.title}}"> </div> <div class="mb-3"> <textarea class="form-control" rows="5" name="content">{{model.content}}</textarea> </div> <button class="btn btn-secondary form-control">글수정하기</button> </form> </div> </div> </div> </body> </html>

13. 게시글 등록

1) BoardSaveDTO.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\board\BoardSaveDTO.java
💡
컨트롤러에 save() 만들 때, 브라우저에서 입력되는 title, content를 클래스로 받아서 전달하기 위한 파일 (컨트롤러에 save만들기 전에 만들어놓기)
package com.example.boardv1.board; import lombok.Data; @Data public class BoardSaveDTO { private String title; private String content; }

2) BoardController.java

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌 @Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점) public class BoardController { private final BoardService boardService; // title : title=title7&content=content7 (x-www-form) @PostMapping("/boards/save") public String save(BoardSaveDTO reqDTO){ // new해서 넣어줌! 필드가 많을 때 상태만 추가하면 되기 때문에 매우 편함! 재사용 가능!(필드명 잘 적어야 함) boardService.게시글쓰기(reqDTO.getTitle(), reqDTO.getContent()); return "redirect:/"; } }

3) BoardService.java

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service // 컴포넌트 스캔되서 IoC에 뜸 + 목적 : 트랜잭션 관리 public class BoardService { private final BoardRepository boardRepository; // 역할 1. 원자성(모든 게 다 되면 commit, 하나라도 실패하면 rollback) // 역할 2. 트랜잭션 종료시 flush 됨 @Transactional public void 게시글쓰기(String title, String content) { // 1. 비영속 객체 Board board = new Board(); board.setTitle(title); board.setContent(content); System.out.println("before persist " + board.getId()); // 2. persist boardRepository.save(board); System.out.println("after persist " + board.getId()); } }

14. 게시글 삭제

1) BoardController.java

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌 @Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점) public class BoardController { private final BoardService boardService; @PostMapping("/boards/{id}/delete") public String delete(@PathVariable("id") int id){ boardService.게시글삭제(id); return "redirect:/"; } }

2) BoardService.java

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service // 컴포넌트 스캔되서 IoC에 뜸 + 목적 : 트랜잭션 관리 public class BoardService { private final BoardRepository boardRepository; @Transactional public void 게시글삭제(int id) { Board board = boardRepository.findById(id); // 영속화 boardRepository.delete(board); // 영속화된 객체를 삭제 } }

3) detail.mustache 수정

{{> header}} <div class="container p-5"> <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <a href = "/boards/{{model.id}}/update-form" class="btn btn-secondary me-1">수정</a> <form action = "/boards/{{model.id}}/delete" method="post"> <button class="btn btn-outline-secondary">삭제</button> </form> </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr /> <div class="d-flex justify-content-end"> 작성자 : 익명 </div> <div class="m-4 p-2"> {{model.content}} </div> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/replies/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button class="btn btn-secondary mt-1"> 댓글등록 </button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-secondary text-white rounded">익명</div> <div>댓글 내용2</div> </div> <form action="/replies/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> <!-- 댓글아이템 --> <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-secondary text-white rounded">익명</div> <div>댓글 내용1</div> </div> <form action="/replies/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> </div> </div> </div> </body> </html>

15. warning 없애기

1) application.properties 추가

C:\workspace\spring_lab\boardv1\src\main\resources\application.properties
# ===== osiv ===== spring.jpa.open-in-view=true
추가하면 warning 사라짐!

Git

spring_lab-boardv1
es0127park-dotcomUpdated Jan 28, 2026
Share article