SpringBoot 게시판 프로젝트

스프링부트(SpringBoot) 게시판 만들기 (3) - CRUD 쿼리 작성

얼뚱인데요 2024. 7. 3. 16:04
게시글 테이블 생성
CREATE TABLE `tb_post` (
    `id`            bigint(20)    NOT NULL AUTO_INCREMENT COMMENT 'PK',
    `title`         varchar(100)  NOT NULL COMMENT '제목',
    `content`       varchar(3000) NOT NULL COMMENT '내용',
    `writer`        varchar(20)   NOT NULL COMMENT '작성자',
    `view_cnt`      int(11)       NOT NULL COMMENT '조회 수',
    `notice_yn`     tinyint(1)    NOT NULL COMMENT '공지글 여부',
    `delete_yn`     tinyint(1)    NOT NULL COMMENT '삭제 여부',
    `created_date`  datetime      NOT NULL DEFAULT current_timestamp() COMMENT '생성일시',
    `modified_date` datetime               DEFAULT NULL COMMENT '최종 수정일시',
    PRIMARY KEY (`id`)
) COMMENT '게시글';

 

 

게시글 생성과 수정에 사용될 요청 클래스 만들기
package com.study.domain.post;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class PostRequest {

    private Long id;            // pk
    private String title;       // 제목
    private String content;     // 내용
    private String writer;      // 작성자
    private Boolean noticeYn;   // 공지글 여부
}

 

Q: 왜 테이블의 모든 칼럼을 요청하지 않나요?

A: 사용자가 게시글을 등록할 때 입력한 정보들만 해당 클래스를 통해 받아올 것이기 때문! 

더보기

! 여기서 잠깐 ,, 나만 몰랐던 필드와 칼럼의 차이 !

칼럼은 테이블에서의 속성 또는 열을 말한다. 위 테이블을 예시로 들면 '제목', '내용', '작성자' 등등의 칼럼이 존재한다고 말할 수 있다.

필드는 테이블에서 특정 행과 열이 만나는 지점을 말한다. 똑같이 위 테이블을 예시로 들면 '제목' 이라는 칼럼의 특정 행에 있는 값을 필드라고 할 수 있다.

 

게시글 응답 클래스 만들기
package com.study.domain.post;

import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class PostResponse {

    private Long id;                        // pk
    private String title;                   // 제목
    private String content;                 // 내용
    private String writer;                  // 작성자
    private int ViewCnt;                    // 조회 수
    private Boolean noticeYn;               // 공지글 여부
    private Boolean deleteYn;               // 삭제 여부
    private LocalDateTime createdDate;      // 생성일시
    private LocalDateTime modifiedDate;     // 최종 수정일시
}

 

 

DB와 통신 역할을 하는 Mapper 인터페이스 만들기
package com.study.domain.post;

import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface PostMapper {
    /**
     * 게시글 저장 
     * @param params - 게시글 정보 
     */
    void save(PostRequest params);

    /**
     * 게시글 상세정보 조회
     * @param id - pk
     * @return 게시글 상세정보
     */
    PostResponse findById(Long id);

    /**
     * 게시글 수정 
     * @param params - 게시글 정보 
     */
    void update(PostRequest params);

    /**
     * 게시글 삭제 
     * @param id - pk 
     */
    void deleteById(Long id);

    /**
     * 게시글 리스트 조회
     * @return 게시글 리스트
     */
    List<PostResponse> findAll();

    /**
     * 게시글 수 카운팅
     * @return 게시글 수
     */
    int count();

}

 

MyBatis가 도입되기 전에는 일반적으로 DAO(Data Access Object) 패턴을 사용했다.

DAO 패턴이란? 

DAO 클래스에 데이터베이스 접근 로직과 SQL 쿼리를 직접 작성한 후, 해당 클래스에
@Repositroy 를 선언하여 DB와 통신하는 클래스임을 명시하는 것.

이는 여러 DAO 클래스에서 유사한 SQL 쿼리를 사용해야 하는 경우, 같은 쿼리가 여러 곳에 반복적으로 작성되어 코드 중복을 초래하며 쿼리 변경이 필요할 때 여러 군데를 수정해야 하는 번거로움이 있다.
또한 SQL 쿼리가 Java 코드 내에 직접 포함되어 데이터베이스와 상호작용하는 코드가 비즈니스 로직과 함께 혼합되기 때문에 가독성과 유지보수성이 떨어지게 된다.

 

MyBatis는 SQL 쿼리를 XML 파일에 따로 분리하여 작성하는 방식을 사용함으로써 이를 개선한다.

DAO 클래스 대신 XML Mapper 파일에 따로 SQL 쿼리를 정의하고, Mapper 인터페이스를 통해 SQL을 매핑하여 Java 코드와 연결하는 방식이다.

 

위 코드를 예를 들어, Mapper 파일에 findById() 라는 메서드가 존재하면, XML Mapper 파일에 해당 메서드명과 동일한 "findById" 라는 id를 가진 SQL 쿼리를 찾아 실행 하게 된다. 

 

XML Mapper 파일 만들기 - SQL 쿼리 작성

 

쿼리를 작성하기 전, DBConfig 클래스 기존 코드에다가 XML Mapper 경로를 통해 찾아 읽어오는 코드를 추가한다. 

 @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource());
        factoryBean.setMapperLocations(context.
                getResources("classpath:/mappers/**/*Mapper.xml")); //xml mapper 파일 읽어오는 코드 추가
        return factoryBean.getObject();
    }

 

 

※ classpath는 보통 src/main/resources 디렉터리에 매핑되어 리소스 파일들을 로드하므로, resoucres 폴더 내에서 위 코드에 입력한 경로와 동일하게 생성하면 된다.

 

생성한 Mapper 파일에 SQL 쿼리 작성하기
<?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.post.PostMapper">

    <!-- tb_post 테이블 전체 컬럼 -->
    <sql id="postColumns">
          id
        , title
        , content
        , writer
        , view_cnt
        , notice_yn
        , delete_yn
        , created_date
        , modified_date
    </sql>

    <!-- 게시글 저장 -->
    <insert id="save" parameterType="com.study.domain.post.PostRequest">
        INSERT INTO tb_post {
            <include refid="postColumns" />
            } VALUES (
                  #{id}
                , #{title}
                , #{content}
                , #{writer}
                , 0
                , #{noticeYn}
                , 0
                , NOW()
                , NULL
        )
    </insert>

    <!-- 게시글 상세정보 조회 -->
    <select id="findById" parameterType="long" resultType="com.study.domain.post.PostResponse">
        SELECT
            <include refid="postColumns" />
        FROM
            tb_post
        WHERE
            id = #{value}
    </select>

    <!-- 게시글 수정 -->
    <select id="update" parameterType="com.study.domain.post.PostRequest">
        UPDATE tb_post
        SET
              modified_date = NOW()
            , title = #{title}
            , content = #{content}
            , writer = #{writer}
            , notice_yn = #{noticeYn}
        WHERE
            id = #{id}
    </select>

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

    <!-- 게시글 리스트 조회 -->
    <select id="findAll" resultType="com.study.domain.post.PostResponse">
        SELECT
            <include refid="postColumns"></include>
        FROM
            tb_post
        WHERE
            delete_yn = 0
        ORDER BY
            id DESC
    </select>

</mapper>

 

SELECT 칼럼과 응답(Response) 클래스의 멤버 변수를 매핑(바인딩)하기

 

DB 테이블 칼럼명은 스네이크 케이스(언더스코어(_)로 연결)를 사용하는 반면,

자바 변수명은 카멜 케이스(앞 단어는 소문자로 시작하고 구분되는 단어의 첫 글자는 대문자 처리)를 사용하기 때문에

둘을 꼭 매핑시켜주어야 한다.

 

여러가지 방법이 있지만 그 중에 application.properties 설정 변경하는 것을 선택하였다. (아래 코드 추가)

# column name to camel case
mybatis.configuration.map-underscore-to-camel-case=true

 

 

그리고 해당 설정을 읽어내도록 DBConfig 클래스에 빈(Bean) 등록

@Bean
@ConfigurationProperties(prefix = "mybatis.configuration")
public org.apache.ibatis.session.Configuration mybatisConfig() {
    return new org.apache.ibatis.session.Configuration();
}

 

 

그 다음 같은 클래스 내 sqlSessionFactory 메소드에 밑줄친 코드(위에서 등록한 빈을 이용하여 옵션 설정 ) 추가 

 

CRUD 테스트 코드 작성 후 테스트 해보기

 

1) 게시물을 생성하는 save() 메서드 테스트

package com.study;

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

import java.util.List;

@SpringBootTest
public class PostMapperTest {
    
    @Autowired
    PostMapper postMapper;
    
    @Test
    void save() {
        PostRequest params = new PostRequest();
        params.setTitle("게시글 제목 테스트");
        params.setContent("게시글 내용 테스트");
        params.setWriter("tester");
        params.setNoticeYn(false);
        postMapper.save(params);

        List<PostResponse> posts = postMapper.findAll();
        System.out.println("전체 게시글 개수는 : " + posts.size() + "개입니다.");
        
    }
}

 

테이블의 PK인 id는 auto_increment 기능으로 자동 번호 부여, 증가가 되기 때문에 id 값 세팅은 생략 !

 

save() 테스트 결과
테이블에도 잘 저장됨

 

 

2) pk인 id를 where 조건으로 하여 특정 게시물을 조회하는 findById() 메서드 테스트 

@Test
void findById() {
    PostResponse post = postMapper.findById(1L);
    try {
    	// 게시글 응답 객체를 JSON 문자열로 변환하여 콘솔에 출력
        String postJson = new ObjectMapper().
                registerModule(new JavaTimeModule()).writeValueAsString(post);
        System.out.println(postJson);
    } catch (JsonProcessingException e) {
        throw new RuntimeException(e);
    }
}

 

디버깅 결과 첫 번째 게시물의 객체가 잘 담겨있는 것을 확인

 

콘솔에도 출력 잘 됨

 

 

3) 기존에 등록된 게시글을 수정하는 update() 메서드 테스트

@Test
void update() {
    // 1. 게시글 수정
    PostRequest params = new PostRequest();
    params.setId(1L);
    params.setTitle("게시글 제목 수정 테스트");
    params.setContent("게시글 내용 수정 테스트");
    params.setWriter("tester 수정");
    params.setNoticeYn(true);
    postMapper.update(params);

    // 2. 게시글 상세정보 조회
    PostResponse post = postMapper.findById(1L);
    try {
        String postJson = new ObjectMapper().
                registerModule(new JavaTimeModule()).writeValueAsString(post);
        System.out.println(postJson);
    } catch (JsonProcessingException e) {
        throw new RuntimeException(e);
    }
}

 

새 게시글 생성과는 달리 기존에 있는 게시글을 수정 하기 때문에 id 값을 설정 해주어야 함!

테스트 결과 특정 게시물이 수정된 버전으로 조회됨

 

※ 수정 기능을 만들 때는 아래 그림 처럼 쿼리에 꼭 WHERE 조건을 주어야 한다.

update 쿼리

만약 그러지 않을 시 모든 정보가 동일한 값으로 update가 되는 대참사를 볼 수 있음....!!! 실무에서 아주 중요 ***

 

 

4) 기존에 등록된 게시글을 삭제하는 delete() 메서드 테스트

@Test
void delete() {
    System.out.println("삭제 이전의 전체 개시글 개수는 : " + postMapper.findAll().size() + "개입니다.");
    postMapper.deleteById(1L);
    System.out.println("삭제 이후의 전체 개시글 개수는 : " + postMapper.findAll().size() + "개입니다.");
}

 

delete() 메서드가 실행이 되면 테이블에서 바로 삭제를 시키는 것이 아니라,

삭제 여부(delete_yn) 값을 0에서 1으로 변경 시킨다.

 

※ 0(false) : 삭제 되지 않은 게시글 / 1(true) : 삭제 된 게시글

 

테스트 결과 아까 생성한 2개의 게시글에서 1개로 줄어든 것을 확인

 


 

모든 코드는 아래 블로그를 참고합니다!

 

스프링 부트(Spring Boot) - 게시판 CRUD 처리하기 [Thymeleaf, MariaDB, IntelliJ, Gradle, MyBatis]

본 게시판 프로젝트는 단계별(step by step)로 진행되니, 이전 단계를 진행하시는 것을 권장드립니다. 본 포스팅은 DBeaver를 기준으로 작성된 글이며, 만약 MariaDB가 설치되어 있지 않으시다면, 선행

congsong.tistory.com