2023-10-16 작성

스프링부트 개발환경 구성하기 (6) Mybatis 연동과 게시판 목록 조회

📢 2024년 2월 기준으로 포스팅 내용을 업데이트하였습니다.

포스팅 순서는 아래와 같다.

 

1. Mybatis 라이브러리 추가

기본적으로 생성되는 스프링부트 프로젝트는 Mybatis를 포함하지 않는다. 따라서 스프링부트에서 Mybatis를 설정할 수 있는 라이브러리를 설치해야 한다. 아래의 mybatis-spring-boot-autoconfigure 요구사항 표를 보면 Spring Boot, Java 버전에 따라 버전이 명시되어 있다.

내 Spring Boot 버전은 3버전, Java는 17이다. 따라서 mybatis-spring-boot-starter는 3 버전대로 추가할 것이다. pom.xml에서 아래 라이브러리를 추가한다.

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

라이브러리를 추가하면, 아래처럼 Mybatis 관련 jar파일들이 추가되는 것을 확인하자. 이렇게 스프링부트는 starter 패키지 라이브러리를 추가해서 mybatis 관련 설정들을 간편하게 가져올 수 있다.

2. DB 라이브러리 추가

이어서 테스트용 DB를 사용하기 위해 pom.xml에서 H2 Database 라이브러리도 추가하자. H2를 선택한 이유는 Oracle, MySQL처럼 서버 설치하는 등 귀찮은 작업을 하지 않아도 되기 때문에 선택했다. 다만 H2는 인메모리 방식으로 할 것이므로 휘발성 데이터라서 서버 재실행시 데이터가 초기화된다. 

즉, H2 DB는 스프링부트 어플리케이션이 실행됐을 때 메모리 내에서 동작하고 스프링부트 어플리케이션이 종료되면 그대로 데이터가 사라진다.

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope> <!-- scope가 runtime이 아닌 test이면 오류가 발생한다 -->
</dependency>

3. 설정파일 추가

application.properties 설정파일에서 아래와 같이 기입한다. H2와 mybatis 관련 설정이다.

###################################################################
# h2 database settings
###################################################################
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:test;Mode=Oracle
spring.datasource.username=sa
spring.datasource.password=

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.h2.console.settings.web-allow-others=true

spring.sql.init.schema-locations=classpath:/h2/schema.sql
spring.sql.init.data-locations=classpath:/h2/data.sql

###################################################################
# mybatis settings
###################################################################
# mapper.xml 위치 지정
mybatis.mapper-locations=classpath:mapper/**/*.xml

# xml 파일의 parameter type, result type에 패키지명 생략 가능하도록 alias 설정
mybatis.type-aliases-package=com.company.basicBoard.*.*.model

# model 프로퍼티 camel case 지정
mybatis.configuration.map-underscore-to-camel-case=true

H2 초기 설정을 위해 기본적으로 schema.sql 파일과 data.sql 파일을 사용할 수 있다.

관념적으로 데이터 정의어(DDL)는 schema.sql 파일에 작성하고 데이터 조작어(DML)은 data.sql 파일에 작성하는데, 아래 5번에서 이 파일들을 작성할 예정이다.

또한 H2의 매력적인 장점은 h2-console 이라는 콘솔 기능을 제공하는데, 프로젝트 서버를 실행하여 http://localhost:8080/h2-console 으로 접속하면 쿼리를 다룰 수 있다. 접속 방법이 궁금하다면 이 포스팅에서 확인해보자.

4. 게시판 목록 조회 기능 만들기

이제 기본 프로젝트 구조를 만들어서 게시판 목록을 조회해볼 것이다. 일반적으로 쓰이는 MVC 패턴으로 RestController, Service, Mapper로 구성했으며 흔히 말하는 VO, DTO 등 비슷한 역할을 수행하는 model으로 구성하여 기본 프로젝트 구조를 만들었다. 이때 REST API 방식으로 데이터를 전달하기 위해서 컨트롤러는 @Controller가 아닌 @RestController으로 선언했음을 기억하자.

BoardController.java

package com.company.basicBoard.domain.board.controller;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.company.basicBoard.domain.board.model.Board;
import com.company.basicBoard.domain.board.service.BoardService;

@RestController
@RequestMapping(value = "/boards")
public class BoardController {

	@Autowired
	BoardService boardService;

	@GetMapping("")
	public ResponseEntity<List<Board>> getListBoard(Board board) {
		return ResponseEntity.ok(boardService.getListBoard(board));
	}

}

BoardService.java

package com.company.basicBoard.domain.board.service;

import java.util.List;
import com.company.basicBoard.domain.board.model.Board;

public interface BoardService {

	public List<Board> getListBoard(Board board);
}

BoardServiceImpl.java

package com.company.basicBoard.domain.board.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.company.basicBoard.domain.board.mapper.BoardMapper;
import com.company.basicBoard.domain.board.model.Board;

@Service
public class BoardServiceImpl implements BoardService {

	@Autowired
	BoardMapper boardMapper;

	@Override
	public List<Board> getListBoard(Board board) {
		return boardMapper.getListBoard(board);
	}
}

BoardMapper.java

package com.company.basicBoard.domain.board.mapper;

import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.company.basicBoard.domain.board.model.Board;

@Mapper
public interface BoardMapper {

	List<Board> getListBoard(Board board);
}

Board.java

package com.company.basicBoard.domain.board.model;

import lombok.Data;

@Data
public class Board {
	private int boardId;
	private String writer;
	private String title;
	private String content;
	private String regDate;
	private String updateDate;
	private String deleteDate;
}

위의 @Data처럼 쉽고 깔끔하게 쓰기 위해서 Lombok을 설치했다. 깔끔한 소스 관리를 위해 Lombok(롬복) 설치 및 활용법을 참고하여 롬복을 설치하자

지금까지 MVC 패턴으로 기본적인 틀을 만들어봤다. @RestController, @Service, @Mapper 어노테이션을 사용하면 빈(Bean)으로 등록되며 스프링 컨테이너에 의해 관리된다. 이들간의 관계는 @Autowired 방식으로 연결해서, Controller단에서 Service클래스를 Autowired하고, Service단에서 Mapper클래스를 Autowired하여 간편하게 관계를 관리할 수 있다.

5. 샘플 데이터 만들기

아래와 같이 src/main/resources에서 data.sql, schema.sql, BoardMapper.xml 파일을 생성한다. 3번에서 말했다시피, H2 데이터베이스 초기 설정을 위해 schema.sql 파일에는 테이블 create문을 작성하고, data.sql 파일에는 게시글 목록으로 보여질 샘플 데이터 insert문을 작성할 것이다.

schema.sql

-- board 테이블 생성
CREATE TABLE BOARD
(
    board_id       NUMBER            NOT NULL, 
    writer         VARCHAR2(50)      NOT NULL, 
    title          VARCHAR2(50)      NULL, 
    content        VARCHAR2(1000)    NULL, 
    reg_date       VARCHAR2(20)      NOT NULL, 
    update_date    VARCHAR2(20)      NULL, 
    delete_date    VARCHAR2(20)      NULL,
    PRIMARY KEY (board_id)
);

-- board 테이블 Comment 설정
COMMENT ON TABLE board IS '게시판 테이블';
COMMENT ON COLUMN board.board_id IS '게시글ID';
COMMENT ON COLUMN board.writer IS '작성자';
COMMENT ON COLUMN board.title IS '제목';
COMMENT ON COLUMN board.content IS '내용';
COMMENT ON COLUMN board.reg_date IS '등록 일자';
COMMENT ON COLUMN board.update_date IS '수정 일자';
COMMENT ON COLUMN board.delete_date IS '삭제 일자';

-- board 시퀀스 생성
CREATE SEQUENCE "sq_board_id" MINVALUE 1 MAXVALUE 999999999 INCREMENT BY 1 START WITH 202700 NOCACHE NOCYCLE;

data.sql

다음 포스팅에서 페이징 처리를 하기 위해 테스트 데이터를 많이 생성하였다.

테스트 데이터.txt
0.02MB

6. Mybatis Mapper 연동하기

이제 4번에서 MVC 패턴으로 만든 소스와, 5번에서 만든 샘플 데이터를 연결해보자. 예전에는 스프링에서 mybatis를 사용하려면 SqlSession, SqlSessionTemplate을 설정하고 selectOne 등의 메서드를 통해 쿼리를 사용하는 등 꽤 복잡한 과정을 거쳐야 했다.

그렇지만 이제는 mybatis-spring-boot-autoconfigure를 이용하면 이런 일련의 복잡한 과정이 없이 @mapper 어노테이션을 이용해 인터페이스의 메서드명과 xml 파일의 id를 매핑시켜 편리하게 사용 할 수 있다. 즉, BoardMapper.java 파일의 메서드명과 BoardMapper.xml 파일의 id는 동일해야 한다.

BoardMapper.xml

<?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.company.basicBoard.domain.board.mapper.BoardMapper">
	
	<select id="getListBoard" parameterType="Board" resultType="Board">
		SELECT BOARD_ID
			 , WRITER
			 , TITLE
			 , CONTENT
			 , REG_DATE
			 , UPDATE_DATE
			 , DELETE_DATE
		  FROM BOARD
	</select>
	
</mapper>

Mapper 인터페이스의 구현체는 xml 파일이 되며 서비스단에서 mapper를 호출하면 mybatis를 편하게 사용할 수 있다. 이 때 resultType은 3번에서 alias에 패키지명을 생략할 수 있도록 설정하였으므로 vo 클래스명만 입력해서 깔끔해보인다.

만약 아래와 같이 링크에서 오류 발생한다면 F2를 누른 후 'Force download'를 실행한다. 

 

이제 서버 실행후 http://localhost:8080/boards 으로 호출하면, H2 인메모리 방식으로 설정한 데이터 샘플이 json형태로 뿌려지는 것을 확인할 수 있다. (이렇게 보이는건 @RestController 방식이기 때문에 json으로 보인다는 것을 다시한번 확인하자)  

7. 뷰에서 보여주기

게시판 목록을 json형태로 뿌려지는 것을 확인했다면 mybatis도 정상적으로 연결이 된 것이다. 이제 이 결과를 최종적으로 뷰에서 보여주기 위해 WebController.java 파일, com-ajax.js 파일, list.html 파일을 만든다. 

WebController.java

WebController.java에서는 뷰(화면)를 보여줄 것이기 때문에 @Controller로 선언하였다.

package com.company.basicBoard.common.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class WebController {

	/**
	 * 기본 경로 : 게시판 페이지 호출
	 */
	@GetMapping("")
	public String home() {
		return "redirect:board";
	}

	/**
	 * 게시판 페이지 호출
	 */
	@GetMapping("/board")
	public String board() {
		return "views/board/list";
	}
}

com-ajax.js

통신하는 부분을 별도 분리하기 위해 js 파일(com-ajax.js) 을 추가한다. 

const AJAX = {

	ajaxCall : function (method, url, data, callbackFunc) {
		const xhr = new XMLHttpRequest();
		
		if (method == 'GET') {
			xhr.open(method, url + data);
		} else {
			xhr.open(method, url);
		}
		xhr.setRequestHeader("Content-Type", "application/json");
		xhr.responseType = "json";
		xhr.send(JSON.stringify(data));
		
		xhr.onload = () => {
			if (xhr.status === 200) {
				callbackFunc(xhr.response);
			} else {
				alert(xhr.status, xhr.statusText);
			}
		}
	}
}

list.html

list.html 파일은 jQuery 라이브러리를 안쓰고 순수한 바닐라 자바스크립트로만 구현했다.

화면 접속시 getList() 함수가 실행되면서 BoardController.java 파일의 get 방식인 "boards"라는 url로 매핑되므로 게시글 목록 조회 기능이 수행된다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
	<meta charset="UTF-8">
	<title>게시판</title>
	<script type="text/javascript" src="/js/com-ajax.js"></script>
</head>
<script type="text/javascript">

	// 페이지 온로드시 게시글 목록 조회
	window.onload = function () {
		getList();
	}

	// 게시글 목록 조회
	function getList() {
		AJAX.ajaxCall("GET", "/boards", "", afterGetList);
	}
	
	// 조회 후 처리
	function afterGetList(response) {
		resultTable(response);
	}

	// 동적으로 테이블 생성
	function resultTable(response) {
		document.querySelector("#fieldListBody").innerHTML = "";
		
		if (response.length > 0) {
			response.forEach(function (result, index) {

				let element = document.querySelector("#fieldListBody");

				let template = `
					<td><p>${result.title}</p></td>
					<td><p>${result.writer}</p></td>
					<td><p>${result.regDate}</p></td>
				`;
				element.insertAdjacentHTML('beforeend', template);
			});
		}
	}
	
</script>

<body>
	<div>
		<h2>게시판 목록</h2>
		<table>
			<tr>
				<td><button onclick="getList()">조회</button></td>
			</tr>
		</table>
	</div>
	<div>
		<table>
			<colgroup>
				<col width="150px" />
				<col width="150px" />
				<col width="250px" />
			</colgroup>
			<thead>
				<tr>
					<th>제목</th>
					<th>작성자</th>
					<th>작성시간</th>
				</tr>
			</thead>
			<tbody id="fieldListBody">
			</tbody>
		</table>
	</div>
</body>
</html>

아래처럼 http://localhost:8080 으로 접속하면 http://localhost:8080/board 으로 리다이렉트 되어 list.html로 이동한다.

8. 검색조건 추가

이제 최종적으로 아래 모습처럼 조회조건 (제목, 작성자)을 추가해보려고 한다. (앞으로 소스 추가되는 부분은 ✅로 별도 표시하려고 한다)

BoardMapper.xml

<?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.company.basicBoard.domain.board.mapper.BoardMapper">
	
	<select id="getListBoard" parameterType="Board" resultType="Board">
		SELECT BOARD_ID
			 , WRITER
			 , TITLE
			 , CONTENT
			 , REG_DATE
			 , UPDATE_DATE
			 , DELETE_DATE
		  FROM BOARD
		  <!-- ✅ 아래 조회조건 추가 -->
		  <where>
		  <if test="title != null and title != ''">
			 AND TITLE LIKE '%' || #{title} || '%'
		  </if>
		  <if test="writer != null and writer != ''">
			 AND WRITER LIKE '%' || #{writer} || '%'
		  </if>
		  </where>
	</select>
	
</mapper>

list.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<head>
	<meta charset="UTF-8">
	<title>게시판</title>
	<script type="text/javascript" src="/js/com-ajax.js"></script>
</head>
<script type="text/javascript">

	// 페이지 온로드시 게시글 목록 조회
	window.onload = function () {
		getList();
	}

	// 게시글 목록 조회
	function getList() {
		// ✅ 조회조건 파라미터 추가
		const title = document.querySelector("#title").value;
		const writer = document.querySelector("#writer").value;
		
		AJAX.ajaxCall("GET", "/boards", "?title=" + title + "&writer=" + writer, afterGetList);
	}
	
	// 조회 후 처리
	function afterGetList(response) {
		resultTable(response);
	}

	// 동적으로 테이블 생성
	function resultTable(response) {
		document.querySelector("#fieldListBody").innerHTML = "";
		
		if (response.length > 0) {
			response.forEach(function (result, index) {

				let element = document.querySelector("#fieldListBody");

				let template = `
					<td><p>${result.title}</p></td>
					<td><p>${result.writer}</p></td>
					<td><p>${result.regDate}</p></td>
				`;
				element.insertAdjacentHTML('beforeend', template);
			});
		}
	}
	
	// ✅ 초기화 기능 추가
	function resetList() {
		document.querySelector("#title").value = "";
		document.querySelector("#writer").value = "";
		document.querySelector("#fieldListBody").innerHTML = "";
		
		getList();
	}
	
</script>

<body>
	<div>
		<h2>게시판 목록</h2>
		<!-- ✅ 조회조건 추가 -->
		<table>
			<tr>
				<th>제목</th>
				<td><input type="text" id="title"></td>
			</tr>
			<tr>
				<th>작성자</th>
				<td><input type="text" id="writer"></td>
			</tr>
			<tr>
				<td><button onclick="getList()">조회</button></td>
				<td><button onclick="resetList()">초기화</button></td>
			</tr>
		</table>
	</div>
	<div>
		<table>
			<colgroup>
				<col width="150px" />
				<col width="150px" />
				<col width="250px" />
			</colgroup>
			<thead>
				<tr>
					<th>제목</th>
					<th>작성자</th>
					<th>작성시간</th>
				</tr>
			</thead>
			<tbody id="fieldListBody">
			</tbody>
		</table>
	</div>
</body>
</html>

최종적으로 게시판 목록 조회 기능을 구현해봤다.

다음편 가기 >> 스프링부트 개발환경 구성하기 (7) 페이징 (Mybatis에서 Pageable)