MyBatis 프레임워크는 Apache에서 만든 iBatis 프레임워크에서 파생된 SQL Mapper 프레임워크이다.
MyBaits의 특징은 아래와 같이 두 가지로 요약할 수 있다.
첫째. JDBC의 반복적인 코드를 대신 처리해준다.
JDBC로 DataBase를 연동하기 위해서는 Driver Loding에서부터 Connection 연결 및 해제까지 개발자가 신경써야 하는 코드가 너무 많다. 이런 복잡하고 반복적인 작업을 프레임워크가 대신 처리해줌으로써 개발자는 비즈니스 로직에 집중할 수 있다.
둘째. JAVA 코드와 SQL을 분리한다.
MyBatis는 SQL 매퍼라는 XML 파일을 만들어서 DAO 클래스에서 사용할 SQL을 저장하고 관리한다. 이렇게 SQL 명령어를 JAVA 코드와 분리하면 SQL 명령어들을 한 곳에 모아서 관리하기 때문에 SQL을 검색하기도 쉽고, SQL을 수정했을 때 JAVA 코드를 다시 컴파일 하지 않아도 된다.
1. SpringBoot 프로젝트 구조 MyBatis 사용
$ tree 프로젝트_경로/src/main/java
├─ java
│ └─ org
│ └─ boot
│ ├─ ① config
│ ├─ ② controller
│ ├─ ③ dao
│ ├─ ④ model
│ └─ ⑤ service
└─ resources
├─ ⑥ banner
├─ ⑦ mappers
└─ ⑧ templates
└─ post
① config
• config 패키지는 Application의 설정과 관련된 클래스들을 담는다.
예) DataBase 연결정보, 보안설정, 서드파티 API 키 설정 등
② controller
• controller 패키지는 클라이언트의 요청을 받고 적절한 응답을 반환하는 컨트롤러 클래스를 담는다.
• 각 컨트롤러 클래스는 특정 엔드포인트, 또는 URL 경로에 매핑되는 메서드를 포함하고 있다.
③ dao
• DAO 패키지는 DataBase와의 상호작용을 담당하는 클래스들을 담는다.
• DAO 클래스들은 데이터베이스 CRUD(Create, Read, Update, Delete) 작업을 수행하는 메서드를 포함하고 있다.
• 이 클래스들은 주로 데이터베이스 쿼리를 실행하고, 결과를 반환하거나, 엔티티 객체를 데이터베이스에 저장하는 역할을 한다.
④ model( VO, DTO ) - 혹은 entity
• model 패키지는 Application의 핵심 비즈니스 로직에 관련된 클래스들을 담는다.
• 클래스들의 데이터의 구조를 정의하고, 비즈니스 로직을 구현한다.
• 예로 DataBase 테이블에 대응되는 엔티티 클래스가 여기에 속한다.
⑤ service
• 비즈니스 로직을 처리하는 클래스들을 담는다.
• Controller와 데이터 액세스 계층( 예 : persistence 패키지 ) 사이에서 중간 계층으로 작용하여
데이터 처리 및 로직 수행을 담당한다.
• service 패키지의 클래스들은 주로 Transaction 관리, Data 유효성 검사, 복잡한 비즈니스 로직구현을 담당한다.
⑥ banner
• SpringBoot 어플리케이션 시작 시해당 파일의 내용을 콘솔에 출력한다.
⑦ mappers
• MyBatis와 같은 ORM 프레임워크에서 사용하는 매퍼 파일을 저장하는 데 사용된다.
• 매퍼 파일은 SQL 쿼리를 포함하고 있으며, DataBase와의 상호작용을 정의합니다.
⑧ templates
• Thymeleaf와 같은 템플릿 엔진에서 사용하는 HTML 파일을 저장하는 데 사용한다.
• 이 템플릿 파일들은 서버 사이드에서 렌더링되어 클라이언트에게 제공됩니다.
2. Gradle 의존성 Library 추가
SpringBoot에서 MyBatis를 이용하여 Data를 처리하기위해서 Gradle에 아래 Library들을 추가하여 준다.
JDBC
implementation "org.springframework.boot:spring-boot-starter-jdbc:3.2.5"
※ 해당 포스팅에서는 MariaDB를 DataBase로 사용하였다.
lombok( 선택 )
// Lombok을 컴파일 타임에만 사용하고 런타임에는 포함하지 않음
compileOnly "org.projectlombok:lombok:1.18.32"
// Lombok 어노테이션 프로세서를 컴파일 시 사용
annotationProcessor "org.projectlombok:lombok:1.18.32"
// 테스트 코드를 컴파일할 때만 Lombok을 사용하고 테스트 실행 시에는 포함하지 않음
testCompileOnly "org.projectlombok:lombok:1.18.32"
// 테스트 코드의 컴파일 시 Lombok 애노테이션 프로세서를 사용
testAnnotationProcessor "org.projectlombok:lombok:1.18.32"
MyBatis
# MariaDB 데이터베이스에 연결하기 위한 JDBC 드라이버
implementation "org.mariadb.jdbc:mariadb-java-client:3.3.3"
# MyBatis와 연동을 위한 SpringBoot의 Starter 의존성 패키지
implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3"
Gson
implementation "com.google.code.gson:gson:2.10.1"
※ Gson 라이브러리는 Google에서 제공하는 JSON 라이브러리로,
Java 객체를 JSON으로 직렬화하고 JSON을 Java 객체로 역직렬화할 수 있게 해줍니다.
전체 build.gradle의 내용은 아래와 같다.
build.gradle
plugins {
id "java"
id "org.springframework.boot" version "3.2.2"
id "io.spring.dependency-management" version "1.1.4"
}
group = "org.boot"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = "17"
}
repositories {
mavenCentral()
}
dependencies {
implementation "org.springframework.boot:spring-boot-starter:3.2.2"
implementation "org.springframework.boot:spring-boot-starter-web:3.2.2"
implementation "org.springframework.boot:spring-boot-starter-web-services:3.2.2"
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:3.2.2"
implementation "org.springframework.boot:spring-boot-starter-logging:3.2.2"
implementation "org.springframework.boot:spring-boot-starter-jdbc:3.2.5"
// lombok
compileOnly "org.projectlombok:lombok:1.18.32"
annotationProcessor "org.projectlombok:lombok:1.18.32"
testCompileOnly "org.projectlombok:lombok:1.18.32"
testAnnotationProcessor "org.projectlombok:lombok:1.18.32"
// MyBatis
implementation "org.mariadb.jdbc:mariadb-java-client:3.3.3"
// MariaDB
implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3"
// Gson
implementation "com.google.code.gson:gson:2.10.1"
testImplementation "org.springframework.boot:spring-boot-starter-test:3.2.5"
}
tasks.named("test") {
useJUnitPlatform()
}
3. SpringBoot - Application 설정
MyBatis를 사용하여 Data를 다루기위한 Library들을 모두 프로젝트에 Build 하였다면 이제 application.properties에서 DataBase 연결 설정 및 MyBatis 사용에 필요한 설정을 아래와 같이 잡아준다.
Database Config
spring.datasource.url = jdbc:mariadb://localhost:3306/데이터베이스_이름
spring.datasource.username = 사용자_이름
spring.datasource.password = 사용자_비밀번호
spring.datasource.driver = org.mariadb.jdbc.Driver
MyBatis Config
# Model 패키지를 지정
mybatis.type-aliases-package = org.boot.model
# MyBatis Mapper 파일
mybatis.mapper-locations = mappers/*-mapper.xml
# DataBase 컬럼명과 Java 객체의 필드명을 자동으로 매핑
mybatis.configuration.map-underscore-to-camel-case = true
# MyBatis가 실행하는 SQL 문장을 콘솔에 출력합니다.
mybatis.configuration.log-impl = org.apache.ibatis.logging.stdout.StdOutImpl
전체 application.properties 설정은 아래와 같다.
application.properties
## Spring Application 이름 설정
spring.application.name = SpringBoot
## Web Application 타입 설정
spring.main.web-application-type = servlet
## Banner 설정
spring.main.banner-mode = console
spring.banner.location = banner/banner.txt
## Server Port 설정
server.port = 9191
## Logging Setting
logging.level.org.boot.blog = warn
logging.file.name = src/main/resources/spring_boot.log
## Thymeleaf 캐시 설정
spring.thymeleaf.cache = false
## Database Config
spring.datasource.url = jdbc:mariadb://localhost:3306/spring_boot_db
spring.datasource.username = springboot
spring.datasource.password = 1q2w3e
spring.datasource.driver = org.mariadb.jdbc.Driver
## MyBatis Config
mybatis.type-aliases-package = org.boot.model
mybatis.mapper-locations = mappers/*-mapper.xml
mybatis.configuration.map-underscore-to-camel-case = true
mybatis.configuration.log-impl = org.apache.ibatis.logging.stdout.StdOutImpl
4. DataBase에 게시글 Table 생성
MariaDB의 DataBase에 게시글 정보를 저장할 post_board 테이블을 생성하여 준다.
CREATE TABLE post_board (
post_uuid CHAR(36) NOT NULL PRIMARY KEY
, write_id VARCHAR(20) NOT NULL
, post_title VARCHAR(50) NOT NULL
, post_content TEXT NULL
, registry_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP() NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
위 SQL Query를 실행하고 Commit 하여준다.
5. DataSource 설정 클래스 생성
Config 패키지는 SpringBoot에서 설정 클래스들을 관리하는 패키지이다. 이 패키지의 역할은 Application의 설정 정보를 담고 있는 클래스들을 포함하고, 이들 설정을 Spring Application 컨텍스트에 로드하거나 필요한 곳에서 주입할 수 있도록 구성한다.
생성한 Config 패키지에 DataBase 연결을 위한 DataSource 설정 클래스인 DataSourceConfig.java 파일을 생성한다.
DataSourceConfig.java
package org.boot.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;
@Value("${spring.datasource.driver}")
private String driverClassName;
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setDriverClassName(driverClassName);
return dataSource;
}
}
6. Model 클래스 생성
필요한 설정이 마무리됐으면 본격적으로 MyBatis를 이용하여 DataBase 연동을 처리해보자. MyBatis를 사용하는 경우프로그램의 시작은 Model 클래스를 작성한다.
해당 포스팅에서는 VO( Value Object )와 DTO( Data Transfer Object )를 합쳐서 Model이라는 클래스에 VO와 DTO를 통일하여 Data의 일관성있게 유지하여 사용한다.
※ VO와 DTO를 합치는 것은 간결성과 성능 측면에서 이점이 있을 수 있지만, 객체의 기본적인 역할은 서로 다르기 때문에 프로젝트의 요구 사항과 개발 팀의 선호도에 따라 적절한 결정하여 사용한다.
PostModel.java
package org.boot.model;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.stereotype.Component;
import java.util.Date;
@Getter
@Setter
@Component("postModel")
public class PostModel {
private static final Logger logger = LoggerFactory.getLogger(PostModel.class);
private int rowNum;
private String postUuid;
private String writeId;
private String postTitle;
private String postContent;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date registryDate;
/*
* lombok 사용으로인한 Getter Setter 메서드 사용하지 않음
*/
// public int getRowNum() {
// return rowNum;
// }
// public void setRowNum(int rowNum) {
// this.rowNum = rowNum;
// }
// public String getPostUuid() {
// return postUuid;
// }
// public void setPostUuid(String postUuid) {
// this.postUuid = postUuid;
// }
// public String getWriteId() {
// return writeId;
// }
// public void setWriteId(String writeId) {
// this.writeId = writeId;
// }
// public String getPostTitle() {
// return postTitle;
// }
// public void setPostTitle(String postTitle) {
// this.postTitle = postTitle;
// }
// public String getPostContent() {
// return postContent;
// }
// public void setPostContent(String postContent) {
// this.postContent = postContent;
// }
// public Date getRegistryDate() {
// return registryDate;
// }
// public void setRegistryDate(Date registryDate) {
// this.registryDate = registryDate;
// }
public PostModel() {
logger.info("PostVO 생성자 호출");
}
}
7. Controller 클래스 생성
컨트롤러(Controller)는 웹 애플리케이션의 요청을 처리하고 응답을 반환하는 주요 구성 요소입니다. 주로 MVC (Model-View-Controller) 아키텍처 패턴에서 사용되며, 클라이언트로부터의 HTTP 요청을 받아 해당 요청을 처리하고, 그에 따른 응답을 생성합니다.
PostController.java
package org.boot.controller;
import com.google.gson.Gson;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.boot.model.PostModel;
import org.boot.service.PostService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import java.io.PrintWriter;
import java.util.UUID;
@Controller
@RequestMapping("/post")
public class PostController {
private static final Logger logger = LoggerFactory.getLogger(PostController.class);
@Resource(name="postService")
private PostService postService;
@GetMapping("/postInfo")
public ModelAndView postInfo(HttpServletRequest request) throws Exception {
ModelAndView modelAndView = new ModelAndView();
if(request.getParameter("uuid") != null && request.getParameter("uuid").isBlank() == false) {
PostModel postModel = new PostModel();
postModel.setPostUuid(request.getParameter("uuid"));
PostModel postInfo = postService.postInfo(postModel);
/** Gson - 조회한 데이터 확인을 위해 사용
* Gson dataGson = new Gson();
* String dataJson = dataGson.toJson(postInfo);
* logger.info(dataJson);
*/
modelAndView.setViewName("post/post_info");
modelAndView.addObject("writeId", postInfo.getWriteId());
modelAndView.addObject("postTitle", postInfo.getPostTitle());
modelAndView.addObject("postContent", postInfo.getPostContent());
modelAndView.addObject("registryDate", postInfo.getRegistryDate());
}
return modelAndView;
}
@GetMapping("/postList")
public ModelAndView postList(HttpServletRequest request) throws Exception {
ModelAndView modelAndView = new ModelAndView();
PostModel postModel = new PostModel();
int totalRow = postService.postCount(postModel); // 해당 테이블의 전체 갯수
int pageNum = 0; // 선택 페이지
int offset = 0; // 결과에서 가져올 첫 번째 행의 OFFSET( 0부터 시작 )
int limitRow = 10; // 가져올 행의 수를 지정
if(request.getParameter("page") != null && Integer.parseInt(request.getParameter("page")) > 0) {
pageNum = Integer.parseInt(request.getParameter("page"));
offset = (pageNum - 1) * limitRow;
} else {
pageNum = 1;
offset = 0;
}
/** Gson 조회한 데이터 확인을 위해 사용
* Gson dataGson = new Gson();
* String dataJson = dataGson.toJson(postService.postList(postModel, offset, limitRow));
* logger.info(dataJson);
*/
modelAndView.setViewName("post/post_list");
modelAndView.addObject("postList", postService.postList(postModel, offset, limitRow));
modelAndView.addObject("totalRow", totalRow);
modelAndView.addObject("pageNum", pageNum);
return modelAndView;
}
@RequestMapping(value = "/postWrite")
public ModelAndView postWrite() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("post/post_write");
return modelAndView;
}
@ResponseBody
@PostMapping("/insertPost")
public void insertPost(HttpServletRequest request, HttpServletResponse response) throws Exception {
PostModel postModel = new PostModel();
UUID uuid = UUID.randomUUID();
String postUuid = uuid.toString();
postModel.setPostUuid(postUuid);
if(request.getParameter("writeId") != null && request.getParameter("writeId").isBlank() == false) {
postModel.setWriteId(request.getParameter("writeId"));
}
if(request.getParameter("postTitle") != null && request.getParameter("postTitle").isBlank() == false) {
postModel.setPostTitle(request.getParameter("postTitle"));
}
if(request.getParameter("postContent") != null && request.getParameter("postContent").isBlank() == false) {
postModel.setPostContent(request.getParameter("postContent"));
}
int resultNumber = postService.insertPost(postModel);
if(resultNumber > 0) {
response.sendRedirect("./postList");
} else {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<script type='text/javascript'>alert('해당 글을 등록하는데 실패하였습니다.');</script>");
out.flush();
}
}
@GetMapping(value = "/postModify")
public ModelAndView postModify(HttpServletRequest request) throws Exception {
ModelAndView modelAndView = new ModelAndView();
if(request.getParameter("uuid") != null && request.getParameter("uuid").isBlank() == false) {
PostModel postVO = new PostModel();
postVO.setPostUuid(request.getParameter("uuid"));
PostModel postInfo = postService.postInfo(postVO);
/** Gson - 조회한 데이터 확인을 위해 사용
* Gson dataGson = new Gson();
* String dataJson = dataGson.toJson(postInfo);
* logger.info(dataJson);
*/
modelAndView.setViewName("post/post_modify");
modelAndView.addObject("postUuid", postInfo.getPostUuid());
modelAndView.addObject("writeId", postInfo.getWriteId());
modelAndView.addObject("postTitle", postInfo.getPostTitle());
modelAndView.addObject("postContent", postInfo.getPostContent());
modelAndView.addObject("registryDate", postInfo.getRegistryDate());
}
return modelAndView;
}
@ResponseBody
@PostMapping(value = "/updatePost")
public void updatePost(HttpServletRequest request, HttpServletResponse response) throws Exception {
PostModel postVO = new PostModel();
if(request.getParameter("postUuid") != null && request.getParameter("postUuid").isBlank() == false) {
postVO.setPostUuid(request.getParameter("postUuid"));
}
if(request.getParameter("writeId") != null && request.getParameter("writeId").isBlank() == false) {
postVO.setWriteId(request.getParameter("writeId"));
}
if(request.getParameter("postTitle") != null && request.getParameter("postTitle").isBlank() == false) {
postVO.setPostTitle(request.getParameter("postTitle"));
}
if(request.getParameter("postContent") != null && request.getParameter("postContent").isBlank() == false) {
postVO.setPostContent(request.getParameter("postContent"));
}
int resultNumber = postService.updatePost(postVO);
if(resultNumber > 0) {
response.sendRedirect("./postInfo?uuid=" + postVO.getPostUuid());
} else {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<script type='text/javascript'>");
out.println("alert('해당 글을 수정하는데 실패하였습니다.');");
out.println("window.history.back();");
out.println("</script>");
out.flush();
}
}
@GetMapping(value = "/deletePost")
public void deletePost(HttpServletRequest request, HttpServletResponse response) throws Exception {
if(request.getParameter("uuid") != null && request.getParameter("uuid").isBlank() == false) {
PostModel postVO = new PostModel();
postVO.setPostUuid(request.getParameter("uuid"));
int resultNumber = postService.deletePost(postVO);
if(resultNumber > 0) {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<script type='text/javascript'>");
out.println("alert('해당 글이 삭제되었습니다.');");
out.println("window.location.href='/post/postList';");
out.println("</script>");
out.flush();
} else {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<script type='text/javascript'>alert('해당 글을 삭제하는데 실패하였습니다.');</script>");
out.flush();
}
}
}
}
8. Service 비즈니스 로직 구성
1) Sevice 인터페이스( 추상 클래스 ) 생성
일반적으로 비즈니스 로직을 구현하는 클래스와 클라이언트(주로 컨트롤러) 사이의 추상화 계층을 제공합니다. 주로 서비스 계층에서 사용되며, 이를 통해 컨트롤러와 비즈니스 로직 구현체 사이의 결합도를 낮추고 유연성을 높이는 데 도움을 줍니다.
PostService.java
package org.boot.service;
import org.boot.model.PostModel;
import java.util.List;
public interface PostService {
PostModel postInfo(PostModel postModel) throws Exception;
int postCount(PostModel postModel) throws Exception;
List<PostModel> postList(PostModel postModel, int offset, int limitRow) throws Exception;
int insertPost(PostModel postModel) throws Exception;
int updatePost(PostModel postModel) throws Exception;
int deletePost(PostModel postModel) throws Exception;
}
2) ServiceImpl 구현 클래스 생성
ServiceImpl ( Service Implementation )는 구현 클래스는 주로 인터페이스를 구현하여 실제 비즈니스 로직을 구현하는 클래스를 말합니다. 보통 서비스 레이어에서 사용되며, 클라이언트( 예: 컨트롤러 )와 데이터 액세스 계층( Repository 등 ) 사이에서 중개자 역할을 합니다.
ServiceImpl 구현 클래스는 주로 인터페이스를 구현하여 실제 비즈니스 로직을 구현하는 클래스를 말합니다. 보통 서비스 레이어에서 사용되며, 클라이언트(예: 컨트롤러)와 데이터 액세스 계층(Repository 등) 사이에서 중개자 역할을 합니다.
PostServiceImpl.java
package org.boot.service;
import org.boot.dao.PostDAO;
import org.boot.model.PostModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service("postService")
@Transactional(propagation = Propagation.REQUIRED)
public class PostServiceImpl implements PostService {
private static final Logger logger = LoggerFactory.getLogger(PostServiceImpl.class);
@Autowired
@Qualifier("postDAO")
private PostDAO postDAO;
@Override
public PostModel postInfo(PostModel postModel) {
logger.info("게시글 상세보기");
return postDAO.selectPostInfo(postModel);
}
@Override
public int postCount(PostModel postModel) {
logger.info("게시글 개수 조회");
return postDAO.selectPostCount(postModel);
}
@Override
public List<PostModel> postList(PostModel postModel, int offset, int limitRow) {
logger.info("게시글 목록 조회");
return postDAO.selectPostList(postModel, offset, limitRow);
}
@Override
public int insertPost(PostModel postModel) throws Exception {
logger.info("게시글 작성");
return postDAO.insertPost(postModel);
}
@Override
public int updatePost(PostModel postModel) throws Exception {
logger.info("게시글 수정");
return postDAO.updatePost(postModel);
}
@Override
public int deletePost(PostModel postModel) throws Exception {
logger.info("게시글 삭제");
return postDAO.deletePost(postModel);
}
}
9. DAO 클래스 생성
Model 클래스를 작성했다면 이제 Model( VO, DTO ) 객체를 매개변수와 return 타입으로 사용하면서 실질적인 DataBase 연동을 처리할 DAO( Data Access Object ) 클래스를 작성한다.
PostDAO.java
package org.boot.dao;
import org.apache.ibatis.annotations.Mapper;
import org.boot.model.PostModel;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Repository;
import java.util.List;
// MyBatis는 @Mapper Annotation을 사용하여 interface를 Mapper로 인식하고 SQL 매핑 파일과 연결한다.
@Mapper
// DAO 클래스임을 나타내는 Annotation으로, DataBase와 상호작용을 수행하는 클래스에 사용된다.
// 지정된 이름( 예 : postDAO )을 통해 여러 DAO 클래스들 중에서 특정 DAO에 명시적으로 주입한다.
@Repository("postDAO")
public interface PostDAO {
PostModel selectPostInfo(PostModel postModel) throws DataAccessException;
int selectPostCount(PostModel postModel) throws DataAccessException;
List<PostModel> selectPostList(PostModel postModel, int offset, int limitRow) throws DataAccessException;
int insertPost(PostModel postModel) throws Exception;
int updatePost(PostModel postModel) throws Exception;
int deletePost(PostModel postModel) throws Exception;
}
10. SQL Mapper 작성
VO 클래스를 작성했다면 이제 Table과 관련된 SQL 명령어들 저장하고 조회할 SQL Mapper XML 파일을 만들어야 한다.
post-mapper.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="org.boot.dao.PostDAO">
<select id="selectPostInfo">
SELECT
post_uuid, write_id, post_title, post_content, registry_date
FROM post_board
WHERE 1 = 1
<if test="postUuid != null and postUuid.isBlank() == false">
AND post_uuid = #{postUuid}
</if>
ORDER BY registry_date DESC
LIMIT 1
</select>
<select id="selectPostCount" resultType="int">
SELECT COUNT(post_uuid) AS postCount FROM post_board WHERE 1 = 1
</select>
<select id="selectPostList" parameterType="int">
SELECT * FROM (
SELECT *, @ROWNUM := @ROWNUM + 1 AS row_num FROM (
SELECT
post_uuid, write_id, post_title, post_content, registry_date
FROM post_board
WHERE 1 = 1
ORDER BY registry_date DESC
) TMP, (SELECT @ROWNUM := 0) SUB
ORDER BY TMP.registry_date ASC
) T
ORDER BY T.row_num DESC
LIMIT ${offset}, ${limitRow}
</select>
<insert id="insertPost" useGeneratedKeys="true" keyProperty="post_uuid">
INSERT INTO post_board(post_uuid, write_id, post_title, post_content)
VALUE(#{postUuid}, #{writeId}, #{postTitle}, #{postContent})
</insert>
<update id="updatePost">
UPDATE post_board SET
post_title = #{postTitle}
, post_content = #{postContent}
WHERE 1 = 1
AND post_uuid = #{postUuid}
AND write_id = #{writeId}
</update>
<delete id="deletePost">
DELETE FROM post_board
WHERE 1 = 1
AND post_uuid = #{postUuid}
</delete>
</mapper>
SQL Mapper XML 파일에 등록된 SQL은 다른 SQL 구문들과 식별하기 위해 유니크한 아이디를 가지고 있어야 한다.
11. Thymeleaf를 통한 WEB Page 화면 구성
1) 게시글 - 목록 Page
post_list.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>게시판 - 게시글 리스트</title>
</head>
<style type="text/css">
table, thead, tbody, tfoot { border:1px solid #000000;border-collapse:collapse; }
tfoot { text-align:center; }
th, td { border:1px solid #000000;padding:10px; }
tbody > tr > td { cursor:pointer;cursor:hand; }
tbody > tr > td:first-child { text-align:center; }
button { cursor:pointer;cursor:hand; }
div#pagingBar > a { text-decoration:none;color:inherit;margin-left:5px;margin-right:5px; }
div#pagingBar > a:hover { text-decoration:underline;color:#0000FF; }
div#pagingBar > a:active { text-decoration:underline;color:#0000FF; }
div#pagingBar > a.active { font-weight:bold; }
</style>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
// 페이징 네비게이션 바 생성
document.querySelector("#pagingBar").innerHTML = createPagingNavigation(
"changePagingNavigation" // 호출함수명
, document.querySelector("#totalRow").value // 전체_데이터_ROW
, 10 // 조회할 행의 개수 제한( OFFSET )
, document.querySelector("#pageNum").value // 출력_페이지_번호
);
document.querySelectorAll(".listRow").forEach(function(element, index, array) {
element.addEventListener("click", function() {
window.location.href = "./postInfo?uuid=" + element.dataset.uuid;
});
});
document.querySelector("#btnWrite").addEventListener("click", function() {
window.location.href = "./postWrite";
});
});
function changePagingNavigation(pageNum) {
window.location.href = "./postList?page=" + pageNum;
}
function createPagingNavigation(pagingFun, totalRow, limitRow, pageNum) {
let pagingBar = "";
let prevPage = 0;
let lastPage = 0;
let totalPage = parseInt((totalRow - 1) / limitRow);
let nowPage = 0;
if(pageNum === 0) {
nowPage = 0;
} else {
nowPage = pageNum;
}
let startRec = (nowPage) * limitRow;
let endRec = 0;
if((startRec + limitRow) > totalRow) {
endRec = totalRow;
} else {
endRec = startRec + limitRow;
}
prevPage = parseInt((nowPage - 1) / 5) * 5;
if ((prevPage + 4) > totalPage) {
lastPage = totalPage;
} else {
lastPage = prevPage + 4;
}
// 시작 페이지 이동
pagingBar += "<a href='javascript:;' onClick='" + pagingFun + "(1);'><<</a>";
// 이전 단락 이동
if(prevPage == 0) {
pagingBar += "<a href='javascript:;'><</a>";
} else {
pagingBar += "<a href='javascript:;' onClick='" + pagingFun + "(" + prevPage + ");'><</a>";
}
for(let num = prevPage; num <= lastPage; num++) {
let thisPage = num + 1;
if(thisPage == nowPage) {
pagingBar += "<a href='javascript:;' class='active'>" + thisPage + "</a>";
} else {
pagingBar += "<a href='javascript:;' onClick='" + pagingFun + "(" + thisPage + ");'>" + thisPage + "</a>";
}
}
// 다음 단락 이동
if(lastPage != totalPage) {
let nextPage = prevPage + 6;
pagingBar += "<a href='javascript:;' onClick='" + pagingFun + "(" + nextPage + ");'>></a>";
} else {
pagingBar += "<a href='javascript:;'>></a>";
}
// 맨 끝 페이지 이동
pagingBar += "<a href='javascript:;' onClick='" + pagingFun + "(" + (totalPage + 1) + ");'>>></a>";
return pagingBar;
}
</script>
<body>
<input type="hidden" id="totalRow" th:value="${totalRow}">
<input type="hidden" id="pageNum" th:value="${pageNum}">
<h3>게시글 리스트</h3>
<table>
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>날짜</th>
</tr>
</thead>
<tbody>
<tr th:each="post:${postList}" th:attr="data-uuid=${post.postUuid}" class="listRow">
<td th:text="${post.rowNum}"></td>
<td th:text="${post.postTitle}"></td>
<td th:text="${post.writeId}"></td>
<td th:text="${#dates.format(post.registryDate, 'yyyy-MM-dd')}"></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="5">
<div id="pagingBar"></div>
</td>
</tr>
</tfoot>
</table>
<br/>
<button id="btnWrite" type="button">새 글쓰기</button>
</body>
</html>
2) 게시글 - 상세보기 Page
post_info.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>게시판 - 게시글</title>
</head>
<style type="text/css">
table, thead, tbody { border: 1px solid #000000;border-collapse:collapse; }
th, td { border:1px solid #000000;padding:10px; }
tfoot { text-align:right; }
</style>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("btnList").addEventListener("click", function() {
window.location.href = "./postList";
});
document.getElementById("btnRemove").addEventListener("click", function() {
if(confirm("해당 글을 삭제하시겠습니까?") == true) {
window.location.href = "./deletePost?uuid=" + getParameter("uuid");
}
});
document.getElementById("btnModify").addEventListener("click", function() {
window.location.href = "./postModify?uuid=" + getParameter("uuid");
});
});
var getParameter = function(param) {
let returnValue;
let url = location.href;
let parameters = (url.slice(url.indexOf("?") + 1, url.length)).split("&");
for(let i = 0; i < parameters.length; i++) {
let varName = parameters[i].split("=")[0];
if(varName.toUpperCase() === param.toUpperCase()) {
returnValue = parameters[i].split("=")[1];
return decodeURIComponent(returnValue);
}
}
}
</script>
<body>
<table>
<tbody>
<tr>
<th>제목</th>
<td th:text="${postTitle}"></td>
</tr>
<tr>
<th>작성자</th>
<td th:text="${writeId}"></td>
</tr>
<tr>
<th>작성일</th>
<td th:text="${#dates.format(registryDate, 'yyyy-MM-dd')}"></td>
</tr>
<tr>
<th>내용</th>
<td th:text="${postContent}"></td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<button type="button" id="btnList">리스트</button>
<button type="button" id="btnRemove">글삭제</button>
<button type="button" id="btnModify">글수정</button>
</td>
</tr>
</tfoot>
</table>
</body>
</html>
3) 게시글 - 작성 Page
post_write.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>게시판 - 신규 글 등록</title>
</head>
<style type="text/css">
table, thead, tbody, tfoot { border:1px solid #000000;border-collapse:collapse; }
tfoot { text-align:right; }
th, td { border:1px solid #000000;padding:10px; }
</style>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("btnList").addEventListener("click", function() {
history.back();
});
document.getElementById("btnRegistry").addEventListener("click", function() {
submitPostRegistry();
});
});
function submitPostRegistry() {
if(document.getElementsByName("postTitle")[0].value.replace(/\s/gi, "") === "") {
alert("제목을 입력하지 않았습니다.\n제목을 입력해 주세요.");
document.getElementsByName("exampleTitle")[0].focus();
return false;
}
if(document.getElementsByName("postContent")[0].value.replace(/\s/gi, "") === "") {
alert("내용을 입력하지 않았습니다.\n내용을 입력해 주세요.");
document.getElementsByName("postContent")[0].focus();
return false;
}
document.getElementById("formPostWrite").method = "POST";
document.getElementById("formPostWrite").action = "./insertPost";
document.getElementById("formPostWrite").submit();
}
</script>
<body>
<h3>신규글 등록</h3>
<form id="formPostWrite">
<!-- 사용자 ID는 임시로 'user'를 고정으로 사용 -->
<input type="hidden" name="writeId" value="user_id"/>
<table>
<tbody>
<tr>
<th>제목</th>
<td>
<label><input type="text" name="postTitle" value=""/></label>
</td>
</tr>
<tr>
<th>내용</th>
<td>
<label><textarea name="postContent"></textarea></label>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<button id="btnList" type="button">리스트</button>
<button id="btnRegistry" type="button">글등록</button>
</td>
</tr>
</tfoot>
</table>
</form>
</body>
</html>
4) 게시글 - 수정 Page
post_modify.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>게시판 - </title>
</head>
<style type="text/css">
table, thead, tbody, tfoot { border:1px solid #000000;border-collapse:collapse; }
tfoot { text-align:right; }
th, td { border:1px solid #000000;padding:10px; }
</style>
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("btnModify").addEventListener("click", function() {
submitPostModify();
});
});
function submitPostModify() {
if(document.getElementsByName("postTitle")[0].value.replace(/\s/gi, "") == "") {
alert("제목을 입력하지 않았습니다.\n제목을 입력해 주세요.");
document.getElementsByName("postTitle")[0].focus();
return false;
}
document.getElementById("formPostRevise").method = "POST";
document.getElementById("formPostRevise").action = "./updatePost";
document.getElementById("formPostRevise").submit();
}
</script>
<body>
<h3>글 수정</h3>
<form id="formPostRevise">
<input type="hidden" name="postUuid" th:value="${postUuid}"/>
<table>
<tbody>
<tr>
<th>ID</th>
<td>
<input type="text" name="writeId" th:value="${writeId}" readonly/>
</td>
</tr>
<tr>
<th>제목</th>
<td>
<input type="text" name="postTitle" th:value="${postTitle}"/>
</td>
</tr>
<tr>
<th>내용</th>
<td>
<textarea name="postContent" th:text="${postContent}"></textarea>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">
<button id="btnModify" type="button">글수정</button>
</td>
</tr>
</tfoot>
</table>
</form>
</body>
</html>
'Spring Web > Spring Boot' 카테고리의 다른 글
[SpringBoot] Error - Command line is too long 해결방법 (1) | 2024.06.21 |
---|---|
[SpringBoot] IntelliJ IDEA를 사용한 SpringBoot 프로젝트 생성 (0) | 2024.02.07 |