본문 바로가기
Spring Boot

스프링 부트(Spring Boot) 게시판 - REST API 방식으로 댓글 CRUD 처리하기 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

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

이전 글에서는 REST API란 무엇이며, 어떤 방식으로 데이터에 접근해야 하는지 가볍게 알아보았습니다. 지금부터 게시판에 댓글 기능을 구현해 볼 건데요. 이번 글에서는 매퍼(Mapper)와 서비스(Service)에 댓글 CRUD 로직을 작성해 두고, 다음 글부터 @RestController와 jQuery의 Ajax를 이용해서 비동기 방식의 화면(HTML) 처리를 진행합니다.

 

1. 댓글 테이블 구조

칼럼 설명
id 댓글 테이블의 PK(Primary Key)로 댓글 번호를 의미합니다.
post_id 댓글 테이블의 FK(Foreign Key)로 댓글과 연결되는 게시글 번호를 의미합니다.
content 댓글 내용을 의미합니다.
writer 댓글 작성자를 의미합니다.
delete_yn 댓글 삭제 여부를 의미합니다.
created_date 댓글 생성일시를 의미합니다.
modified_date 댓글 최종 수정일시를 의미합니다.

 

2. 댓글 테이블 생성하기

DB에 댓글 데이터를 관리할 테이블을 생성해 보겠습니다. DBMS 툴을 실행하고, tb_comment 테이블을 생성하는 다음의 스크립트를 실행해 주세요.

create table tb_comment (
      id bigint not null auto_increment comment '댓글 번호 (PK)'
    , post_id bigint not null comment '게시글 번호 (FK)'
    , content varchar(1000) not null comment '내용'
    , writer varchar(20) not null comment '작성자'
    , delete_yn tinyint(1) not null comment '삭제 여부'
    , created_date datetime not null default CURRENT_TIMESTAMP comment '생성일시'
    , modified_date datetime comment '최종 수정일시'
    , primary key(id)
) comment '댓글';

 

3. 제약 조건(Constraint) 추가하기

게시글(tb_post)과 댓글(tb_comment)은 각각 1:N의 관계가 되어야 하며, 관계를 매핑해 주기 위해 테이블에 FK(Foreign Key) 제약 조건을 추가해야 합니다.


3-1) 제약 조건 생성

댓글 테이블의 FK(post_id)가 게시글 테이블의 PK(id)를 참조할 수 있도록 DBMS 툴에서 다음의 스크립트를 실행해 주세요.

alter table tb_comment add constraint fk_post_comment foreign key(post_id) references tb_post(id);

 

3-2) 테이블 구조 확인

이제, 테이블 구조를 확인해 보면 post_id에 추가된 제약 조건을 확인할 수 있습니다. 테이블 구조 확인은 다음의 명령어들을 이용하시면 됩니다.

show full columns from tb_comment; -- 테이블 구조 확인 1 (코멘트 포함)
desc tb_comment; -- 테이블 구조 확인 2

 

댓글 테이블 구조

 

3-3) 제약 조건 조회

마지막으로 다음의 스크립트를 실행해서 DB(스키마)에 제약 조건이 정상적으로 추가되었는지 확인해 주세요.

select *
from information_schema.table_constraints
where table_name = 'tb_comment';

댓글 테이블 제약 조건

 

추가적으로, 특정 테이블이 아닌 DB(스키마)의 모든 제약 조건을 확인하고 싶을 때는 조건절에 table_schema = '스키마 이름'을 입력해 주시면 됩니다.

 

4. 댓글 요청(Request) 클래스 생성하기

다음으로 댓글 생성(INSERT)과 수정(UPDATE)에 사용할 요청 클래스를 생성해 볼 건데요. 댓글도 게시글과 마찬가지로 요청(Request)과 응답(Response)용 클래스를 분리해서 데이터를 처리합니다.


이클립스(STS)를 사용하신다면, 코드의 간결함과 가독성을 위해 롬복 설치하기를 진행하시는 것을 권장드립니다.


package com.study.domain.comment;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CommentRequest {

    private Long id;           // 댓글 번호 (PK)
    private Long postId;       // 게시글 번호 (FK)
    private String content;    // 내용
    private String writer;     // 작성자

}

 

어노테이션 설명
@Getter 롬복(Lombok)이 제공해주는 기능으로, 클래스의 모든 멤버 변수에 대한 get( ) 메서드를 만들어 줍니다.
@NoArgsConstructor(access = AccessLevel.PROTECTED) 클래스의 기본 생성자를 만들어 줍니다. access 속성을 이용해서 객체 생성을 protected로 제한합니다.

게시글 요청 클래스인 PostRequest에는 @Getter와 @Setter를 선언해서 사용했는데요. 이전에 말씀드렸듯이 요청 클래스의 각 멤버 변수는 HTML의 폼(form) 태그에 선언된 필드(input, textarea 등)의 name 값을 기준으로 파라미터를 전송하며, 전송된 파라미터는 요청 클래스의 set( ) 메서드에 의해 값이 매핑됩니다.

하지만, 일반적인 REST API 방식에서는 데이터를 등록/수정 할 때 폼(form) 자체를 전송하지 않고, key-value 구조로 이루어진 JSON이라는 문자열 포맷으로 데이터를 전송하기 때문에 set( ) 메서드가 필요하지 않습니다. (파일을 전송하는 경우는 제외)

자세한 내용은 컨트롤러를 처리하는 과정에서 설명드리겠습니다. 지금은 "JSON이라는 포맷으로 데이터를 전달하는구나!" 정도로 이해해 주시면 되겠습니다.

 

5. 댓글 응답(Response) 클래스 생성하기

다음은 댓글 데이터 조회에 사용할 응답용 클래스입니다.

package com.study.domain.comment;

import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class CommentResponse {

    private Long id;                       // 댓글 번호 (PK)
    private Long postId;                   // 게시글 번호 (FK)
    private String content;                // 내용
    private String writer;                 // 작성자
    private Boolean deleteYn;              // 삭제 여부
    private LocalDateTime createdDate;     // 생성일시
    private LocalDateTime modifiedDate;    // 최종 수정일시

}

 

6. 댓글 Mapper 인터페이스 생성하기

다음은 DB와의 통신 역할을 할 Mapper 인터페이스입니다.

package com.study.domain.comment;

import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface CommentMapper {

    /**
     * 댓글 저장
     * @param params - 댓글 정보
     */
    void save(CommentRequest params);

    /**
     * 댓글 상세정보 조회
     * @param id - PK
     * @return 댓글 상세정보
     */
    CommentResponse findById(Long id);

    /**
     * 댓글 수정
     * @param params - 댓글 정보
     */
    void update(CommentRequest params);

    /**
     * 댓글 삭제
     * @param id - PK
     */
    void deleteById(Long id);

    /**
     * 댓글 리스트 조회
     * @param postId - 게시글 번호 (FK)
     * @return 댓글 리스트
     */
    List<CommentResponse> findAll(Long postId);

    /**
     * 댓글 수 카운팅
     * @param postId - 게시글 번호 (FK)
     * @return 댓글 수
     */
    int count(Long postId);

}

 

구성 요소 설명
@Mapper 해당 인터페이스가 DB와 통신하는 인터페이스임을 의미합니다. Mapper 인터페이스는 XML Mapper에서 메서드명과 동일한 ID를 가진 SQL 쿼리를 찾아 실행합니다.
save( ) 댓글을 생성하는 INSERT 쿼리를 호출합니다. 파라미터로 전달받는 params에는 저장할 댓글 정보가 담깁니다.
findById( ) id(PK)를 기준으로 특정 댓글의 상세정보를 조회하는 SELECT 쿼리를 호출합니다. 쿼리가 실행되면 응답(CommentResponse) 클래스 객체의 각 멤버 변수에 결괏값이 매핑(바인딩)됩니다.
update( ) 댓글을 수정하는 UPDATE 쿼리를 호출합니다. save( )와 마찬가지로 요청 클래스의 객체를 이용해서 댓글 정보를 업데이트합니다.
deleteById( ) id(PK)를 기준으로 특정 댓글을 삭제하는 UPDATE 쿼리를 호출합니다. 우리는 테이블에서 실제로 데이터를 DELETE 하지 않고, 삭제 여부(delete_yn) 칼럼의 상태 값을 0(false)에서 1(true)로 업데이트합니다.

SELECT 쿼리의 조건절에 delete_yn = 0 조건을 주게 되면, 삭제된(delete_yn = 1) 데이터는 조회되지 않습니다. 즉, 물리 삭제가 아닌 논리 삭제 방식을 이용합니다.
findAll( ) 게시글 번호(post_id)를 기준으로 특정 게시글에 등록된 댓글 목록을 조회하는 SELECT 쿼리를 호출합니다.
count(  게시글 번호(post_id)를 기준으로 특정 게시글에 등록된 댓글 수를 조회하는 SELECT 쿼리를 호출합니다. 당장은 사용되지 않으며, 게시글 때와 마찬가지로 추후에 페이징을 적용하면서 사용합니다.

 

7. 댓글 MyBatis XML Mapper 생성하기

다음은 Mapper 인터페이스와 연결할 XML Mapper입니다. src/main/resources/mappers 폴더에 CommentMapper.xml을 추가하고, 다음의 코드를 작성해 주세요.

XML Mapper는 게시글 CRUD 처리하기에서 설명드렸으니 자세한 설명은 생략하겠습니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.study.domain.comment.CommentMapper">

    <!-- tb_comment 테이블 전체 컬럼 -->
    <sql id="commentColumns">
          id
        , post_id
        , content
        , writer
        , delete_yn
        , created_date
        , modified_date
    </sql>


    <!-- 댓글 저장 -->
    <insert id="save" parameterType="com.study.domain.comment.CommentRequest" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO tb_comment (
            <include refid="commentColumns" />
        ) VALUES (
              #{id}
            , #{postId}
            , #{content}
            , #{writer}
            , 0
            , NOW()
            , NULL
        )
    </insert>


    <!-- 댓글 상세정보 조회 -->
    <select id="findById" parameterType="long" resultType="com.study.domain.comment.CommentResponse">
        SELECT
            <include refid="commentColumns" />
        FROM
            tb_comment
        WHERE
            id = #{value}
    </select>


    <!-- 댓글 수정 -->
    <update id="update" parameterType="com.study.domain.comment.CommentRequest">
        UPDATE tb_comment
        SET
              modified_date = NOW()
            , content = #{content}
            , writer = #{writer}
        WHERE
            id = #{id}
    </update>


    <!-- 댓글 삭제 -->
    <delete id="deleteById" parameterType="long">
        UPDATE tb_comment
        SET
            delete_yn = 1
        WHERE
            id = #{id}
    </delete>


    <!-- 댓글 리스트 조회 -->
    <select id="findAll" parameterType="long" resultType="com.study.domain.comment.CommentResponse">
        SELECT
            <include refid="commentColumns" />
        FROM
            tb_comment
        WHERE
            delete_yn = 0
            AND post_id = #{value}
        ORDER BY
            id DESC
    </select>

</mapper>

 

8. 댓글 서비스(Service) 클래스 생성하기

다음은 비즈니스 로직을 담당해 주는 서비스 레이어(Service Layer)입니다. 해당 클래스의 각 어노테이션도 게시글 등록 구현하기에서 설명드렸었고, 로직 자체도 심플하기 때문에 설명은 생략하겠습니다.

package com.study.domain.comment;

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

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

@Service
@RequiredArgsConstructor
public class CommentService {

    private final CommentMapper commentMapper;

    /**
     * 댓글 저장
     * @param params - 댓글 정보
     * @return Generated PK
     */
    @Transactional
    public Long saveComment(final CommentRequest params) {
        commentMapper.save(params);
        return params.getId();
    }

    /**
     * 댓글 상세정보 조회
     * @param id - PK
     * @return 댓글 상세정보
     */
    public CommentResponse findCommentById(final Long id) {
        return commentMapper.findById(id);
    }

    /**
     * 댓글 수정
     * @param params - 댓글 정보
     * @return PK
     */
    @Transactional
    public Long updateComment(final CommentRequest params) {
        commentMapper.update(params);
        return params.getId();
    }

    /**
     * 댓글 삭제
     * @param id - PK
     * @return PK
     */
    @Transactional
    public Long deleteComment(final Long id) {
        commentMapper.deleteById(id);
        return id;
    }

    /**
     * 댓글 리스트 조회
     * @param postId - 게시글 번호 (FK)
     * @return 특정 게시글에 등록된 댓글 리스트
     */
    public List<CommentResponse> findAllComment(final Long postId) {
        return commentMapper.findAll(postId);
    }

}

 

마치며

이번에는 서비스(Service) 영역까지 댓글 CRUD 기능을 구현해 보았습니다.

다음 글에서는 @RestController와 jQuery의 Ajax를 이용해서 댓글을 등록하는 방법을 알아보겠습니다.

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

Board.zip
0.86MB

반응형

댓글