본문 바로가기
Spring Boot

스프링 부트(Spring Boot) 게시판 - 게시글 등록 기능 구현하기 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

by 도뎡 2023. 4. 3.
반응형

  • 본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다.
  • DBMS 툴은 DBeaver를 이용하며, DB는 MariaDB를 이용합니다. (MariaDB 설치하기)
  • 화면 처리는 HTML5 기반의 자바 템플릿 엔진인 타임리프(Thymeleaf)를 사용합니다.

이전 글에서는 게시글 데이터를 관리할 tb_post 테이블을 생성하고, MyBatis를 이용해서 게시글 CRUD 기능을 구현해 보았습니다. JUnit 단위 테스트까지 진행해 보았으니 지금부터는 실전입니다.

 

이번 글에서는 비즈니스 로직을 담당하는 서비스(Model), 사용자가 보는 화면을 의미하는 UI(View), 마지막으로 서비스와 UI를 연결해 주는 컨트롤러(Controller)를 처리하는 방법과 개발 순서에 대해 알아보도록 하겠습니다.

 

이전 글에서 Mapper 영역을 모두 처리하였으니, 바로 서비스부터 진행합니다.

 

 

1. 게시글 서비스(Service) 클래스 생성하기

서비스는 MVC 패턴 중 M(Model)에 해당되며, 사용자(고객)의 요구사항을 처리하는 로직을 실행하는 핵심 영역입니다. 자세한 내용은 데이터 처리 과정에서 설명드리도록 하겠습니다.


<TIP> Java 파일은 모두 src/main/java 디렉터리에 위치하며, 파일 경로는 클래스(인터페이스) 최상단의 package를 통해 알 수 있습니다.


1-1) 서비스 클래스 생성 및 소스 코드 작성하기

package com.study.domain.post;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;

@Service
@RequiredArgsConstructor
public class PostService {

    private final PostMapper postMapper;

    /**
     * 게시글 저장
     * @param params - 게시글 정보
     * @return Generated PK
     */
    @Transactional
    public Long savePost(final PostRequest params) {
        postMapper.save(params);
        return params.getId();
    }

    /**
     * 게시글 상세정보 조회
     * @param id - PK
     * @return 게시글 상세정보
     */
    public PostResponse findPostById(final Long id) {
        return postMapper.findById(id);
    }

    /**
     * 게시글 수정
     * @param params - 게시글 정보
     * @return PK
     */
    @Transactional
    public Long updatePost(final PostRequest params) {
        postMapper.update(params);
        return params.getId();
    }

    /**
     * 게시글 삭제
     * @param id - PK
     * @return PK
     */
    public Long deletePost(final Long id) {
        postMapper.deleteById(id);
        return id;
    }

    /**
     * 게시글 리스트 조회
     * @return 게시글 리스트
     */
    public List<PostResponse> findAllPost() {
        return postMapper.findAll();
    }

}

 

1) @Service

PostMapper 인터페이스의 @Mapper와 유사하며, 해당 클래스가 비즈니스 로직을 담당하는 Service Layer의 클래스임을 의미합니다.

 

 

2) @RequiredArgsConstructor

과거(스프링 레거시)에는 일반적으로 @Autowired, @Inject, @Resource 등을 이용해서 빈(Bean)을 주입하고는 했었는데요. 스프링은 생성자로 빈(Bean)을 주입하는 방식을 권장한다고 합니다.

 

해당 어노테이션은 롬복(Lombok)에서 제공해 주는 기능으로, 클래스 내에 final로 선언된 모든 멤버에 대한 생성자를 만들어주는 역할을 합니다. 이런 식으로 말이죠.

 

private final PostMapper postMapper;
    
public PostService(PostMapper postMapper) {
    this.postMapper = postMapper;
}

 

 

3) postMapper

이전 글에서 처리한 게시글 CRUD 기능을 포함하고 있는 Mapper 인터페이스입니다.

 

 

4) @Transactional

스프링에서 제공해 주는 트랜잭션(Transaction) 처리 방법 중 하나로, 선언적 트랜잭션으로 불리는 기능입니다. 호출된 메서드에 해당 어노테이션이 선언되어 있으면 메서드의 실행과 동시에 트랜잭션이 시작되고, 메서드의 정상 종료 여부에 따라 Commit 또는 Rollback 됩니다.

 

해당 어노테이션에는 여러 가지 옵션이 있는데요. 추후에 JPA + Querydsl 기반으로 프로젝트를 진행할 때 다루어 보도록 하겠습니다.

 

 

5) savePost( )

게시글을 생성합니다. INSERT가 완료되면, 생성된 게시글 id를 리턴합니다.

 

 

6) findPostById( )

특정 게시글의 상세정보를 조회합니다.

 

 

7) updatePost( )

게시글을 수정합니다. UPDATE가 완료되면, 게시글 id를 리턴합니다.

 

 

8) deletePost( )

게시글을 삭제합니다. UPDATE가 완료되면, 게시글 id를 리턴합니다.

 

 

9) findAllPost( )

게시글 목록(리스트)을 조회합니다.

 

 

2. 서비스(Service) 테스트해 보기

테스트 코드 작성은 어느 정도 경험해 봤으니, 디테일한 설명은 생략하겠습니다. 테스트 클래스는 src/test/java에 위치한다는 것과, 메서드명 더블클릭 후 마우스 우클릭을 통해 실행한다는 것만 꼭 기억해 주세요.


2-1) 테스트 클래스 추가 & 코드 작성하기

package com.study;

import com.study.domain.post.PostRequest;
import com.study.domain.post.PostService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class PostServiceTest {

    @Autowired
    PostService postService;

    @Test
    void save() {
        PostRequest params = new PostRequest();
        params.setTitle("1번 게시글 제목");
        params.setContent("1번 게시글 내용");
        params.setWriter("테스터");
        params.setNoticeYn(false);
        Long id = postService.savePost(params);
        System.out.println("생성된 게시글 ID : " + id);
    }

}

 

1) postService

@Autowired를 이용해서 스프링 컨테이너에 등록된 PostService 빈(Bean)을 클래스에 주입합니다.

 

 

2) save( )

코드를 실행해보면 테스트는 성공하지만, 생성된 게시글 ID가 null로 출력됩니다. id는 auto_increment에 의해 자동으로 1씩 증가하며 데이터가 생성되는데, 이때 생성된 PK를 객체에 담아주기 위해서는 MyBatis의 "useGeneratedKeys" 기능을 이용해야 합니다.

save( ) 테스트 실행 결과

 

 

3. MyBatis useGeneratedKeys 기능 적용해 보기


3-1) PostMapper.xml 수정하기

<insert id="save" parameterType="com.study.domain.post.PostRequest" useGeneratedKeys="true" keyProperty="id">

 

useGeneratedKeys 옵션을 true로 설정하면 생성된 게시글의 PK가 parameterType에 선언된 요청 객체(params)에 저장되며, keyProperty에 선언된 id에 값이 매핑(바인딩)됩니다.

useGeneratedKeys 적용 후 요청 객체 (디버깅 모드)

 

 

3-2) 다시 테스트해 보기

다시 save( )를 실행해 보면 생성된 게시글 ID가 콘솔에 출력됩니다. 여기까지 잘 따라오셨다면 PostService의 나머지 메서드들도 테스트 코드를 작성해서 한 번씩 실행해 보시기를 권장드립니다.

save( ) 테스트 재실행 결과

 

 

4. 컨트롤러(Presentation Layer) 클래스 생성하기

컨트롤러는 MVC 패턴 중 C(Controller)에 해당되며, Model(서비스)과 View(UI == 화면)의 중간다리 역할을 하는 영역입니다. 화면에서 사용자의 요청이 들어오면 가장 먼저 컨트롤러를 경유하는데요. 컨트롤러는 사용자의 요구사항을 처리해 줄 서비스의 메서드(비즈니스 로직)를 호출하고, 그에 대한 실행 결과를 다시 화면으로 전달하는 역할을 합니다.


4-1) 컨트롤러 클래스 생성 및 소스 코드 작성하기

package com.study.domain.post;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
@RequiredArgsConstructor
public class PostController {

    private final PostService postService;

    // 게시글 작성 페이지
    @GetMapping("/post/write.do")
    public String openPostWrite(Model model) {
        return "post/write";
    }

}

 

1) @Controller

해당 클래스가 사용자의 요청과 응답을 처리(UI를 담당)하는 컨트롤러 클래스임을 의미합니다.

 

 

2) @GetMapping

과거의 스프링은 컨트롤러 메서드에 URI(주소)와 HTTP 요청 메서드를 매핑하기 위해 @RequestMapping을 이용해서 value에는 URI를, method에는 HTTP 요청 메서드를 지정(선언)해 주어야만 했습니다.

 

스프링 4.3 버전부터는 @GetMapping, @PostMapping 등 요청 메서드의 타입별로 매핑을 처리할 수 있는 어노테이션이 추가되었습니다.


과거의 URI 매핑) @RequestMapping(value = "...", method = RequestMethod.XXX)

현재의 URI 매핑) @xxxMapping("...")


 

 

3) 리턴 타입

컨트롤러 메서드는 void, String, ModelAndView, Map, List 등 어떤 타입이던 리턴 타입으로 선언할 수 있습니다. 일반적으로 사용자가 보는 화면(HTML)을 처리할 때는 리턴 타입을 String으로 선언하고, 리턴 문에 HTML 파일의 경로를 선언해 주면 됩니다.

 

리턴 문에 선언된 HTML 경로에는 접미사(suffix)로 확장자(.html)가 자동으로 연결되기 때문에 확장자를 생략할 수 있습니다.

 

 

4) Model

메서드의 파라미터로 선언된 Model 인터페이스는 데이터를 화면(HTML)으로 전달하는 데 사용됩니다. Model은 화면을 처리하는 과정에서 자세히 알아보도록 하겠습니다.

 

 

5. 화면(Presentation Layer) 처리하기

Mapper, Service, Controller까지 구성이 완료되었으니, 마지막으로 화면(HTML)을 구성할 차례입니다. 우선은 게시글 작성 페이지입니다. PostController의 openPostWrite( )와 연결할 HTML을 추가해 보도록 하겠습니다.


5-1) HTML 생성하기

우리는 타임리프(Thymeleaf) 템플릿 엔진을 이용해 화면을 구성합니다. 타임리프는 HTML5 기반이기에 HTML 파일을 추가해 주시면 되는데요. 스프링 부트는 기본적으로 HTML 파일을 src/main/resources/templates 폴더에서 읽기 때문에, templates 안에 openPostWrite( )에 선언된 폴더와 HTML을 추가해 주시면 됩니다.

폴더와 HTML이 추가된 디렉터리 구조

 

 

5-2) 소스 코드 작성하기 (write.html)

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <title>글작성 페이지</title>
</head>
<body>
    <h2>welcome spring boot!</h2>
</body>
</html>

 

 

5-3) 애플리케이션 실행하기

JUnit 테스트와 마찬가지로 BoardApplication에서 마우스를 우클릭한 후 main( ) 메서드를 실행하면 스프링 부트 애플리케이션이 실행됩니다. 웹 브라우저를 실행하고 글쓰기 페이지로 접근해 보면 write.html에 해당하는 화면이 출력됩니다.

 

게시글 작성 페이지 호출 결과

 

URL 구성요소 설명
로컬 호스트(localhost) 자신의 PC를 의미하며, "127.0.0.1"과 같은 의미를 갖습니다.
8080 포트 WAS(Web Application Server) 중 하나인 톰캣(Tomcat)의 기본 포트 번호입니다. 스프링 부트에서 WAS는 기본적으로 내장되어 있는 톰캣을 사용하며, properties에서 server.port 속성을 이용하면 원하는 포트 번호로 변경할 수 있습니다.

웹은 기본적으로 80 포트를 사용하기 때문에 properties에 server.port=80을 선언해두면, 도메인(localhost) 뒤에 포트 번호를 붙이지 않아도 사이트에 접속할 수 있습니다.
/post/write.do 컨트롤러의 openPostWrite( )와 매핑된 URI를 의미합니다. 만약, 매핑되지 않은 URI를 호출하는 경우에는 "서버에서 사용자의 요청 URI를 찾을 수 없음"을 의미하는 HTTP 404 Not Found 에러가 발생합니다.

HTTP 상태 코드(Status Code)는 웹에 있어 빠질 수 없는 부분입니다. 시간적 여유가 되실 때 한 번쯤은 검색해 보시기를 권장드립니다.

 

 

 

5-4) 화면으로 데이터 전달해 보기

앞에서 말씀드린 Model 인터페이스의 addAttribute( ) 메서드를 이용하면 화면(HTML)으로 데이터를 전달할 수 있습니다. 해당 메서드는 이름(String name), 값(Object value) 두 개의 파라미터(인자)를 필요로 합니다.

 

일반적으로 이름과 값을 동일하게 지정해 주는 게 코드를 읽기 유리하며, HTML에서는 ${ } 표현식을 이용해 전달받은 데이터에 접근할 수 있습니다.

 

우선은 openPostWrite( )를 다음과 같이 변경해 주세요.

    // 게시글 작성 페이지
    @GetMapping("/post/write.do")
    public String openPostWrite(Model model) {
        String title = "제목",
               content = "내용",
               writer = "홍길동";

        model.addAttribute("t", title);
        model.addAttribute("c", content);
        model.addAttribute("w", writer);
        return "post/write";
    }

 

 

이제, 타임리프를 이용해서 전달받은 데이터를 화면에 출력해 보도록 하겠습니다. write.html을 다음과 같이 변경해 주세요.

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>The page is a write page</title>
</head>
<body>
    <h2>Welcome Spring Boot!</h2>
    <span th:text="${t}">여기는 제목입니다.</span>
    <span th:text="${c}">여기는 내용입니다.</span>
    <span>[[ ${w} ]]</span>
</body>
</html>

 

속성 설명
xmlns:th 타임리프의 th 속성을 사용하기 위해 선언된 네임스페이스입니다. 순수 HTML로만 이루어진 페이지에는 선언하지 않아도 됩니다.
th:text JSP의 EL 표현식인 ${ }와 마찬가지로 타임리프도 ${ } 표현식을 이용해 컨트롤러에서 전달받은 데이터에 접근할 수 있습니다. th:text는 데이터를 텍스트 형식으로 화면에 출력하며, JSTL의 c:forEach, c:set 등의 태그와 마찬가지로 여러 가지 속성이 존재합니다.

타임리프 공식 문서의 튜토리얼에 여러 가지 속성과 함수들이 설명되어 있는데요. 차차 개발을 진행하면서 필요한 속성들을 이용해 보면 금방 익숙해질 수 있습니다.

 

 

5-5) 애플리케이션 재실행하기

다시 글쓰기 페이지로 접근해 보면, 컨트롤러에서 전달받은 데이터의 값이 출력되는 것을 확인할 수 있습니다.

게시글 작성 페이지 재호출 결과

 

 

6. CSS 적용하기

"보기 좋은 떡이 먹기도 좋다"라는 말이 있듯이, 화면이 보기 좋게 꾸며져 있다면 개발이 더욱 즐거워집니다. 아인커뮤니케이션의 능력자 퍼블리셔분들이 만들어 주신 정적(static) 파일을 이용해서 화면을 예쁘게 꾸며보도록 하겠습니다.


6-1) 압축 해제 및 폴더 추가하기

우선 static.zip 파일의 압축을 풀고, 하나도 빠짐없이 src/main/resources의 static 폴더에 추가해 주세요.

static.zip
0.18MB

(해당 파일을 상용 목적으로 사용하거나

불법 복제 및 무단 배포하신 경우에는

법적 처벌을 받으실 수 있습니다.)

 

폴더가 추가된 디렉터리 구조

 

6-2) 공통 레이아웃(layout) 적용하기

다음으로 화면에서 공통으로 사용할 레이아웃(layout)을 적용해 볼 건데요. 글쓰기(write) 페이지, 게시글 상세(view) 페이지, 게시글 리스트(list) 페이지에 공통으로 적용되는 머리(header)와 몸통(body)을 만들어 보도록 하겠습니다.

 

타임리프의 레이아웃 기능을 이용하려면 라이브러리를 추가해야 하는데요. build.gradle의 dependencies에 다음의 라이브러리를 선언해 주세요. 코드의 선언 위치는 이미지와 동일해야 합니다. (implementation은 implementation끼리 모아두어야 합니다.)

implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' /* Thymeleaf Layout */

 

라이브러리가 추가된 build.gradle 구조

 

 

build.gradle이 변경되면 인텔리제이가 그레이들이 변경된 것을 감지하며, IDE 우측 상단에 코끼리(?) 아이콘이 생기는데요. 코끼리를 클릭하면 그레이들 빌드가 시작되며, 라이브러리 추가가 완료됩니다.

그레이들 빌드(build) 방법

 

 

6-3) 헤더(header), 바디(body) 생성하기

src/main/resources/templates에 fragments와 layout 폴더를 추가하고, fragments에 header와 body를 추가해 주세요.

각 폴더와 HTML이 추가된 디렉터리 구조

 

 

먼저 header.html에 다음의 코드를 작성해 주세요.

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head th:fragment="main-head">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />

    <th:block layout:fragment="title"></th:block>

    <link rel="stylesheet" th:href="@{/css/default.css}" />
    <link rel="stylesheet" th:href="@{/css/common.css}" />
    <link rel="stylesheet" th:href="@{/css/content.css}" />
    <link rel="stylesheet" th:href="@{/css/button.css}" />

    <th:block layout:fragment="add-css"></th:block>
</head>
</html>

 

속성 설명
th:fragment <head> 태그에 해당 속성을 사용해서 fragment의 이름을 지정합니다. fragment는 다른 HTML에서 include 또는 replace 해서 적용할 수 있는데요. 이는 레이아웃을 처리하는 과정에서 알아보도록 하겠습니다.
th:block layout:fragment 속성에 이름을 지정해서 실제 컨텐츠 페이지의 내용을 채우는 기능입니다. 해당 기능은 동적(Dynamic)인 처리가 필요할 때 사용되며, 마찬가지로 레이아웃을 처리하는 과정에서 알아보도록 하겠습니다.
th:href  <a> 태그의 href 속성과 동일하며, JSTL의 <c:url> 태그와 마찬가지로 웹 애플리케이션을 구분하는 콘텍스트 경로(Context Path)를 포함합니다. 콘텍스트 경로(Context Path)는 application.properties에서 변경할 수 있습니다.

 

 

 

다음으로 body.html에 다음의 코드를 작성해 주세요.

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<body th:fragment="main-body">
    <div id="adm_wrap">
        <header>
            <div class="head">
                <h1>게시판 프로젝트</h1>
                <div class="top_menu">
                    <div class="login_user"><strong><i class="far fa-user-circle"></i> 도뎡</strong>님 반갑습니다.</div>
                    <div class="logout"><button type="button"><span class="skip_info">로그아웃</span><i class="fas fa-sign-out-alt"></i></button></div>
                </div>
            </div>
        </header>

        <div id="container">
            <div class="menu_toggle"><span></span></div>
            <!--/* 좌측 영역 */-->
            <div class="lcontent">
                <!--/* 메뉴 */-->
                <nav>
                    <ul>
                        <li class="has_sub"><a href="javascript: void(0);" class="on"><span>게시판 관리</span></a>
                            <ul>
                                <li><a href="/post/list.do" class="on">리스트형</a></li>
                                <li><a href="javascript: alert('준비 중입니다.');">갤러리형</a></li>
                                <li><a href="javascript: alert('준비 중입니다.');">캘린더형</a></li>
                            </ul>
                        </li>
                        <li><a href="javascript: alert('준비 중입니다.');"><span>회원 관리</span></a></li>
                    </ul>
                </nav>
            </div>

            <!--/* 우측 영역 */-->
            <div class="rcontent">

                <!--/* 페이지별 컨텐츠 */-->
                <th:block layout:fragment="content"></th:block>

            </div>
        </div> <!--/* // #container */-->
        <footer>Copyright(c)네임즈.All rights reserved.</footer>
    </div>

    <script th:src="@{/js/function.js}"></script>
    <script th:src="@{/js/jquery-3.6.0.min.js}"></script>
    <script th:src="@{/js/common.js}"></script>
    <script src="https://kit.fontawesome.com/79613ae794.js" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>

    <th:block layout:fragment="script"></th:block>
</body>
</html>

 

content 레이아웃 조각 (<th:block layout:fragment="content"></th:block>)

페이지별 실제 콘텐츠가 들어가는 영역입니다. 글쓰기 페이지에서는 글작성 폼을, 리스트 페이지에서는 검색 영역, 테이블, 페이지 번호를, 상세 페이지에서는 게시글 정보, 첨부파일, 댓글을 보여주게 됩니다.

 

 

 

6-4) 글쓰기 페이지에 레이아웃 적용하기

글쓰기 페이지에 헤더(header)와 바디(body)를 적용해 보도록 하겠습니다. write.html을 다음과 같이 변경해 주세요.

<!DOCTYPE html>
<html lang="ko" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head th:replace="fragments/header :: main-head"> </head>
    <body th:replace="fragments/body :: main-body"> </body>
</html>

 

th:replace

JSP의 <include> 태그와 유사한 속성으로, header.html의 main-headbody.html의 main-body 프래그먼트를 찾아 해당 코드로 치환(replace)합니다.

 

 

6-5) 애플리케이션 재실행하기

다시 글쓰기 페이지로 접근해 보면 레이아웃이 적용된 것을 확인하실 수 있습니다. (이미지를 클릭하시면 확대해 보실 수 있습니다.)

레이아웃이 적용된 글쓰기 페이지

 

 

6-6) 레이아웃 인클루드(include) 하기

헤더(header)와 바디(body)는 게시판의 모든 페이지에서 공통으로 사용되기 때문에 꼭! 레이아웃으로 처리되어야 하며, 이제 레이아웃을 인클루드 하는 방법을 알아보도록 하겠습니다. 먼저 앞에서 생성한 layout 폴더에 basic.html을 추가하고, write.html의 코드를 그대로 입력해 주세요.

HTML이 추가된 디렉터리 구조

 

 

다음으로 글쓰기 페이지에서 레이아웃 폴더에 추가된 basic.html을 인클루드 하도록 write.html을 다음과 같이 변경해 주세요. (Spring Boot 버전이 2.6 미만인 경우에는 layout:decorate 대신 layout:decorator로 선언해야 합니다.)

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="layout/basic">
    <th:block layout:fragment="title">
        <title>글작성 페이지</title>
    </th:block>
</html>

 

속성 설명
xmlns:th 타임리프의 th 속성을 사용하기 위한 선언입니다.
xmlns:layout 타임리프의 레이아웃 기능을 사용하기 위한 선언입니다.
xmlnslayout:decorate 레이아웃으로 basic.html을 사용하겠다는 의미입니다.
th:block layout:fragment header.html을 작성하는 과정에서 말씀드렸듯이, layout:fragment 속성에 이름을 지정해서 실제 컨텐츠(content) 페이지의 내용을 채우게 됩니다. 예를 들어, 글쓰기 페이지는 "write page"로, 게시글 리스트 페이지는 "list page"로, 페이지마다 타이틀을 다르게 처리하고 싶을 때 해당 속성을 이용해서 타이틀을 동적(Dynamic)으로 처리할 수 있습니다.

쉽게 말해, 페이지별로 사용자에게 보여주는 내용이 다르기 때문에, 필요한 경우 해당 속성을 이용해서 컨텐츠를 동적으로 컨트롤 해주면 됩니다.

 

 

7. 글작성 페이지 처리하기


7-1) openPostWrite( ) 수정하기

PostController의 openPostWrite( ) 메서드를 다음과 같이 변경해 주세요.

    // 게시글 작성 페이지
    @GetMapping("/post/write.do")
    public String openPostWrite(@RequestParam(value = "id", required = false) final Long id, Model model) {
        if (id != null) {
            PostResponse post = postService.findPostById(id);
            model.addAttribute("post", post);
        }
        return "post/write";
    }

 

1) @RequestParam

화면(HTML)에서 보낸 파라미터를 전달받는 데 사용됩니다. 예를 들어, 신규 게시글을 등록하는 경우에는 게시글 번호(id)가 null로 전송됩니다. 하지만, 기존 게시글을 수정하는 경우에는 수정할 게시글 번호(id)가 openPostWrite( )의 파라미터로 전송되고, 전달받은 게시글 번호(id)를 이용해 게시글 상세정보를 조회한 후 화면(HTML)으로 전달합니다.

 

신규 게시글 등록에는 게시글 번호(id)가 필요하지 않기 때문에 required 속성을 false로 지정합니다. 필수(required) 속성은 default 값이 true이며, required 속성을 false로 지정하지 않으면, id가 파라미터로 넘어오지 않았을 때 예외가 발생합니다.

 

 

2) 전체 로직

게시글 번호(id)를 파라미터로 전달받은 경우, 즉 기존 게시글을 수정하는 경우에는, 게시글 번호(id)를 이용해서 조회한 게시글 상세 정보(응답 객체)를 post라는 이름으로 해서 화면(HTML)으로 전달합니다.

 

 

7-2) 글 작성 영역 추가하기

글쓰기 페이지에 글을 작성할 수 있는 영역, 즉 실제 콘텐츠(content)를 포함시켜 보도록 하겠습니다. write.html을 다음과 같이 변경해 주세요.

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="layout/basic">
    <th:block layout:fragment="title">
        <title>글작성 페이지</title>
    </th:block>

    <th:block layout:fragment="content">
        <div class="page_tits">
            <h3>게시판 관리</h3>
            <p class="path"><strong>현재 위치 :</strong> <span>게시판 관리</span> <span>리스트형</span> <span>글작성</span></p>
        </div>

        <div class="content">
            <section>
                <form id="saveForm" method="post" autocomplete="off">
                    <!--/* 게시글 수정인 경우, 서버로 전달할 게시글 번호 (PK) */-->
                    <input type="hidden" id="id" name="id" th:if="${post != null}" th:value="${post.id}" />

                    <!--/* 서버로 전달할 공지글 여부 */-->
                    <input type="hidden" id="noticeYn" name="noticeYn" />
                    <table class="tb tb_row">
                        <colgroup>
                            <col style="width:15%;" /><col style="width:35%;" /><col style="width:15%;" /><col style="width:35%;" />
                        </colgroup>
                        <tbody>
                            <tr>
                                <th scope="row">공지글</th>
                                <td><span class="chkbox"><input type="checkbox" id="isNotice" name="isNotice" class="chk" /><i></i><label for="isNotice"> 설정</label></span></td>

                                <th scope="row">등록일</th>
                                <td colspan="3"><input type="text" id="createdDate" name="createdDate" readonly /></td>
                            </tr>

                            <tr>
                                <th>제목 <span class="es">필수 입력</span></th>
                                <td colspan="3"><input type="text" id="title" name="title" maxlength="50" placeholder="제목을 입력해 주세요." /></td>
                            </tr>

                            <tr>
                                <th>이름 <span class="es">필수 입력</span></th>
                                <td colspan="3"><input type="text" id="writer" name="writer" maxlength="10" placeholder="이름을 입력해 주세요." /></td>
                            </tr>

                            <tr>
                                <th>내용 <span class="es">필수 입력</span></th>
                                <td colspan="3"><textarea id="content" name="content" cols="50" rows="10" placeholder="내용을 입력해 주세요."></textarea></td>
                            </tr>
                        </tbody>
                    </table>
                </form>
                <p class="btn_set">
                    <button type="button" id="saveBtn" onclick="savePost();" class="btns btn_st3 btn_mid">저장</button>
                    <a th:href="@{/post/list.do}" class="btns btn_bdr3 btn_mid">뒤로</a>
                </p>
            </section>
        </div> <!--/* .content */-->
    </th:block>

    <th:block layout:fragment="script">
        <script th:inline="javascript">
        /*<![CDATA[*/

            window.onload = () => {
                initCreatedDate();
            }


            // 등록일 초기화
            function initCreatedDate() {
                document.getElementById('createdDate').value = dayjs().format('YYYY-MM-DD');
            }


            // 게시글 저장(수정)
            function savePost() {
                const form = document.getElementById('saveForm');
                const fields = [form.title, form.writer, form.content];
                const fieldNames = ['제목', '이름', '내용'];

                for (let i = 0, len = fields.length; i < len; i++) {
                    isValid(fields[i], fieldNames[i]);
                }

                document.getElementById('saveBtn').disabled = true;
                form.noticeYn.value = form.isNotice.checked;
                form.action = [[ ${post == null} ]] ? '/post/save.do' : '/post/update.do';
                form.submit();
            }

        /*]]>*/
        </script>
    </th:block>
</html>

 

layout:fragment="content" 게시글 등록 페이지는 게시글 정보를 입력할 수 있는 폼이 필요하고, 게시글 리스트 페이지는 게시글 정보를 보여주는 테이블이 필요합니다. 즉, 타이틀과 마찬가지로 페이지마다 컨텐츠 영역의 형태가 다르기 때문에 layout:fragment를 이용합니다.
<form> 태그 폼은 태그 안에 선언되어 있는 <input>, <textarea> 등 </span사용자가 입력(선택)한 필드의 "name" 값을 기준으로 폼 "action"에 지정된 URI로 폼 데이터(파라미터)를 전달합니다. 여기서 action의 URI는 컨트롤러의 메서드를 의미합니다.

method 속성에는 HTTP 요청 메서드를 지정합니다. HTTP 요청 메서드는 대표적으로 GET과 POST가 사용되는데요. GET은 데이터의 조회를 의미하고, POST는 데이터의 생성을 의미합니다.

예를 들어, 데이터를 조회하는 SELECT와 같은 행위는 GET 방식으로 처리되어야 하며, 데이터의 생성, 수정, 삭제를 의미하는 INSERT, UPDATE, DELETE와 같은 행위는 POST 방식으로 처리되어야 합니다.
layout:fragment="script" 자바스크립트도 마찬가지로 페이지마다 로직이 다르기 때문에 layout:fragment를 이용합니다.
th:inline="javascript" <script> 태그에 th:inline 속성을 javascript로 선언해야 자바스크립트 내에서 타임리프 문법을 사용할 수 있습니다.
<![CDATA[]]> 타임리프는 '<', '>' 태그를 엄격하게 검사하기 때문에 자바스크립트 코드는 꼭 CDATA로 묶어줘야 한다고 합니다. CDATA는 특수문자를 전부 문자열로 치환(replace)할 때 사용합니다.
initCreatedDate( ) 함수 해당 함수는 신규 게시글을 등록할 때, 등록일에 오늘 날짜를 렌더링 해주는 역할을 합니다. dayjs는 JS 영역에서 날짜 데이터를 쉽게 컨트롤 할 수 있도록 도와주는 라이브러리인데요. body.html 하단의 <script src="https://cdn.....dayjs.min.js> 코드를 통해 dayjs 라이브러리를 import 해서 사용합니다.
savePost( ) 함수 해당 함수는 저장하기 버튼의 onclick 이벤트를 통해 실행됩니다. 76~82번 라인의 코드는 유효성 검사 코드입니다. fields에는 제목, 이름, 내용 필드를, fieldNames에는 각 필드의 이름을 담아 반복문 안에서 isValid( ) 함수를 호출해 값이 입력되지 않은 필드를 탐색합니다.

isValid( ) 함수는 앞에서 다운로드 받은 static 폴더에 있는 function.js에 선언된 함수입니다. 해당 함수는 필드의 value 값을 체크해서, 값이 비어있는 경우 해당 필드로 포커싱 해주는 역할을 하는 함수로, 여러분의 편의를 생각해서 제가 미리 추가해둔 함수입니다. 앞으로 JS 영역에서 공통으로 사용할 함수들은 /static/js/function.js에 추가해 나갈 예정입니다.

84번 라인은 데이터 중복 저장을 방지하기 위한 로직입니다. 저장 버튼을 클릭한 상태에서 한 번 더 클릭하면 같은 내용의 게시글이 두 개 저장됩니다. 이러한 상황을 방지하고자, 저장 로직이 실행되었을 때 저장 버튼이 작동하지 않도록 비활성화(disabled)합니다.

85번 라인은 공지글 여부의 값을 세팅하는 로직입니다. 공지글 설정이 체크되어 있으면 true, 아니면 false로 hidden 타입의 noticeYn 필드 값이 세팅됩니다.

86번 라인은 앞에서 말씀드렸던 폼의 action을 설정하는 로직입니다. 컨트롤러에서 전달받은 게시글 응답 객체(post)의 유무에 따라 신규 저장인지, 기존 게시글의 수정인지를 구분합니다. "/post/save.do"는 신규 저장을, "/post/update.do"는 수정을 의미합니다.

마지막으로 87번 라인의 form.submit( )을 호출해서 폼 데이터(파라미터)를 서버(컨트롤러)로 전달합니다.

 

 

8. 게시글 등록 메서드 추가하기

화면(HTML) 처리는 모두 완료되었으니, 폼의 action으로 지정된 신규 게시글 등록 URI인 "/post/save.do"와 컨트롤러 메서드를 연결해 줄 차례입니다. PostController에 다음의 메서드를 추가해 주세요.

    // 신규 게시글 생성
    @PostMapping("/post/save.do")
    public String savePost(final PostRequest params) {
        postService.savePost(params);
        return "redirect:/post/list.do";
    }

 

params

앞에서 말씀드렸듯이 폼 태그는 사용자 입력(선택) 필드의 "name" 값을 통해 컨트롤러 메서드로 파라미터를 전송합니다. 요청 객체(PostRequest)의 멤버 변수명과 사용자 입력 필드의 "name" 값이 동일하면 PostRequest 타입의 객체인 params의 각 멤버 변수에 "name" 값을 통해 전달된 필드의 value가 매핑됩니다

 

 

9. 게시글 등록 테스트해 보기

애플리케이션을 실행하고, 게시글 등록 페이지에서 제목, 이름, 내용을 입력한 후 저장 버튼을 클릭해 주세요.

(이미지 사이즈 문제로 폼 영역만 캡처했습니다.)

 

게시글 등록 폼

 

 

디버깅 모드로 실행해 보았을 때, 값이 정상적으로 매핑(바인딩)되는 것을 확인할 수 있습니다. 여기서 포인트는 게시글 등록 페이지에 입력한 필드의 "name"과 요청(Request) 클래스의 멤버 변수명이 동일해야 한다는 것입니다.

PostController - savePost( ) 디버깅 모드

 

 

데이터도 정상적으로 등록되고 있습니다.

tb_post 테이블 SELECT 결과

 

 

10. 디버깅(Debugging) 사용해 보기


10-1) 디버깅 실행방법

이번에는 개발에 있어 너무나도 필수적인 디버깅 사용 방법을 알려드리도록 하겠습니다. 보통 애플리케이션을 실행할 때 실행(▶) 버튼을 눌러 WAS를 실행하는데요. 인텔리제이 기준으로 디버깅 모드는 IDE 우측 상단에 있는 실행 버튼 우측의 초록색 벌레(?) 모양의 버튼으로 실행하거나, BoardApplication에서 마우스 우클릭 후 Debug로 main( ) 메서드를 실행해 주면 됩니다.

디버깅 실행 방법 1

 

 

디버깅 실행 방법 2

 

 

10-2) 디버깅해 보기

우리는 PostService의 savePost( ) 메서드로 디버깅을 해보도록 하겠습니다. 먼저, 디버깅을 시작하고 싶은 위치의 라인 넘버를 클릭하거나, 커서에서 Ctrl + Shift + B를 누르면 되는데요. savePost( ) 메서드에서 postMapper의 save( ) 메서드를 호출하는 22번 라인을 찍어보면 브레이크 포인트가 잡히는 것을 볼 수 있습니다.

브레이크 포인트가 적용된 모습

 

 

 

브레이크 포인트를 지정한 상태에서 메서드가 실행되면, 포인트가 지정된 라인에 백그라운드 컬러가 활성화(Active) 됩니다.

메서드가 실행되었을 때 라인이 활성화 된 모습

 

 

컨트롤러, 서비스뿐만 아니라, 모든 자바 코드에서 디버깅 모드를 사용할 수 있습니다. 디버그는 문제가 되는 로직을 찾기 위해 사용하는데요. 포인트에서 다음 라인으로 넘어가고 싶다면 F6을, 디버깅을 종료하거나, 다음 브레이크 포인트로 이동하고 싶다면 F8을 누르면 되며, 브레이크 포인트는 여러 개 설정(지정)할 수 있습니다.

 

11. 게시글 등록해 보기

마지막으로 게시글을 등록해 보고 마무리 짓도록 하겠습니다. 게시글 등록 페이지에 제목, 이름, 내용을 입력하고 저장하기를 클릭해 주세요.

공지글 등록 폼

 

 

테이블을 SELECT 해보면 데이터는 정상적으로 등록되어 있지만, 게시글 리스트 페이지의 URI인 "/post/list.do"와 매핑된 메서드가 없기 때문에 404 Not Found 응답이 내려오게 됩니다.

tb_post 테이블 SELECT 결과

 

 

"/post/list.do" - 404 Not Found 에러

 

 

마치며

이번에는 서비스와 컨트롤러, 그리고 게시글 등록 페이지까지 구현해 보았습니다. 공통 레이아웃 설정 때문에 본의 아니게 포스팅이 길어졌네요 ^^..

 

다음 글에서는 게시글 리스트 페이지를 구현해 보도록 하겠습니다.

오늘도 방문해 주신 여러분께 진심으로 감사드립니다. 좋은 하루 보내세요 :)

 

Board.zip
0.64MB

반응형

댓글