[SB] 7. 스프링부트(프레임워크)

최재원's avatar
Mar 20, 2025
[SB] 7. 스프링부트(프레임워크)

1. 프로젝트 생성

notion image
notion image

프로젝트 경로

notion image

2. 프로젝트 준비

1. mustache 파일

notion image

🧔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

  • 데이터 삽입 확인
notion image
 
 

3. 프로젝트 수행

SRP
하나의 클래스(또는 모듈, 함수)가 오직 하나의 책임(변화의 이유)만 가져야 한다는 원칙
notion image
  • 유지보수성(Manageability) 향상
  • 코드 재사용성(Reusability) 증가
  • 테스트 용이성(Testability) 증가
  • 협업 시 충돌 방지
notion image
단일 책임 원칙을 사용하면 문제가 발생 했을 때 해당 문제를 발생 시킨 책임을 가지고 있는 놈을 찾아 해결하면 되기 때문에 관리하기 좋다
  • 위의 원칙에 따라
  • 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

jjack1