inblog logo
|
sxias
    스프링부트

    [스프링부트] 8. 블로그 만들기 v1. Prototype

    문정준's avatar
    문정준
    Mar 18, 2025
    [스프링부트] 8. 블로그 만들기 v1. Prototype
    Contents
    1. New Project2. Design3. Controller4. 결과5. DB 연결6. 기능 추가 / 개선7. 최종 결과
     

    1. New Project

    • 새 폴더 생성, Group명 변경
      • Git으로 관리 용이하도록 변경
    notion image
     
    Dependencies
    • Lombok : Getter / Setter 생성
    • Spring Web : Dispatcher, Annotation 생성
    • Mustache : Template Engine
    • H2 Database : 가상 DB
    • Spring Data JPA : Java 코드로 SQL 데이터 저장
    notion image
     

    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

    notion image
     

    save-form

    notion image
     

    detail

    notion image
     

    update-form

    notion image
     

    5. DB 연결

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

    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을 구현하고 관리하는 데에 중요한 요소
    notion image
     
    • MVC Pattern : 데이터베이스(Model), 화면(View), 기능(Controller)의 구조로 짜여진 서버 기능
      • Dispatcher를 사용하면서 Common Logic을 활용할 수 있어 코드 절약이 가능
      • SRP를 활용 : Controller가 할당 받을 기능을 쪼개서 구현
    notion image
     
     

    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
    notion image
    Insert (save)
    notion image
    Update
    notion image
    Delete
    • 6번 글 상세 보기 → 삭제 버튼 클릭 시 데이터 삭제 후 메인 화면 이동 (글 최신화)
    notion image
    Display
    Main Page (welcome)
    notion image
    Detail
    notion image
    Save-form
    notion image
    • 데이터 입력 후 ‘글쓰기’ 버튼 클릭 시 메인 화면으로 이동 (글 최신화)
    notion image
    Update-form
    • 2번 글 상세보기 → 수정화면 이동 후 내용 수정
    notion image
    • ‘글수정’ 버튼 클릭 시 해당 글 상세 화면 이동
    notion image
    Share article
    Contents
    1. New Project2. Design3. Controller4. 결과5. DB 연결6. 기능 추가 / 개선7. 최종 결과

    Sxias ㆍ a32176740@gmail.com

    RSS·Powered by Inblog