개요
여기에서는 Spring Boot + Spring JDBC + HSQLDB + Thymeleaf를 사용하여 Spring MVC CRUD Web Application을 만드는 방법을 보여준다.
개발환경은 다음과 같다:
● Spring Tool Suite 3 (Version: 3.9.11)
● Spring Boot Version : 2.7.10
● Java : JDK 8
HSQLDB 대신 H2 DB, Apache Derby, SQLite 등의 인메모리 데이터베이스를 사용할 수도 있다.
※ 관련글 목록: http://yellow.kr/lifeView.jsp?s=spring
작성할 웹 애플리케이션 설명
만들려는 예제 Web Application은 책 관리 시스템으로 다음과 같은 기능을 구현한다:
● 모든 책 정보 리스트를 보여준다.
● 새 책 정보 등록
● 책 정보 상세 조회
● 책 정보 수정
● 책 정보 삭제
예제 Application의 Flow는 다음과 같다:
Spring Boot project 생성
● eclips에서 Spring Boot project를 생성한다.
File > New > Project… > Spring Boot > Spring Starter Project
Name, Group, Package에 적당한 내용을 입력하고 [Next] 클릭
● 필요한 Dependency들을 선택한다.
Spring Web, Thymeleaf, Spring Data JDBC, HyperSQL Database를 선택한다.
[Finish] 클릭
● Project 생성 됨
yellow-jdbc 라는 이름으로 Project가 생성되었고, YellowJdbcApplication.java 가 생성되었다.
package com.yellow.jdbc;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class YellowJdbcApplication {
public static void main(String[] args) {
SpringApplication.run(YellowJdbcApplication.class, args);
}
}
예제 애플리케이션이 완성되면 Project Structure은 다음과 같다.
Maven Dependencies
pom.xml을 보면 다음과 같은 dependency를 확인할 수 있다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
Spring JDBC
Spring JDBC는 Spring Framework에서 제공하는 JDBC 추상화 라이브러리이다. JDBC는 자바에서 데이터베이스와 통신하는 표준 API이지만, 간단한 작업도 상당한 양의 코드를 필요로 하며, 반복적인 작업이 많아서 개발자들이 자주 사용하는 SQL문 작성, Statement 생성, ResultSet 처리, 예외처리 등의 작업을 간소화하고, 코드의 가독성 및 유지보수성을 높이기 위해 Spring Framework에서는 JDBC API를 추상화한 Spring JDBC를 제공한다.
Spring JDBC는 JDBC를 직접 사용하는 것보다 코드가 더 간결하며, 예외 처리 및 리소스 해제 등을 자동으로 처리할 수 있다. 또한, 일반적인 JDBC 작업을 처리하기 위해 반복적으로 작성해야 하는 boilerplate 코드를 최소화할 수 있다.
Spring Boot에서 사용하려면 Maven의 경우 다음과 같은 의존성을 추가한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
Spring JDBC에서 제공하는 모든 클래스는 다음과 같은 주요 패키지로 분류할 수 있다:
● core
JdbcTemplate과 관련된 클래스를 제공한다. 이 패키지는 JDBC 데이터 액세스 코드 작성을 단순화하는 데 사용된다. JdbcTemplate 클래스는 데이터베이스 액세스를 단순화하기 위한 메서드를 제공하며, NamedParameterJdbcTemplate 클래스는 SQL 쿼리 파라미터 처리를 위해 사용된다. 이 패키지는 또한 RowMapper 인터페이스를 포함하여 데이터베이스에서 읽은 데이터를 객체로 변환하는 방법을 제공한다.
● datasource
DataSource 추상화와 관련된 클래스를 제공한다. 이 패키지는 Spring에서 DataSource를 생성, 구성 및 관리하는 방법을 제공한다. DriverManagerDataSource 클래스는 JDBC 드라이버를 사용하여 DataSource를 만들고, JndiObjectFactoryBean 클래스는 JNDI 기반 DataSource를 만든다.
● object
SQL 쿼리와 관련된 클래스를 제공한다. 이 패키지는 SQL 쿼리를 캡슐화하고, 다양한 SQL 쿼리를 쉽게 실행할 수 있는 방법을 제공한다. 이 패키지는 다양한 SQL 쿼리 유형에 대한 클래스를 제공하며, SQL 쿼리를 호출하는 방법과 결과를 매핑하는 방법을 정의한다.
● support
예외 변환과 관련된 클래스를 제공한다. 이 패키지는 SQLException 예외를 Spring의 DataAccessException 계층 구조로 변환하는 기능을 제공한다. 이를 통해 개발자는 데이터베이스 액세스 코드에서 예외 처리를 단순화할 수 있다.
● init
데이터베이스 초기화와 관련된 클래스를 제공한다. 이 패키지는 데이터베이스 초기화에 사용될 SQL 스크립트를 로드하고, 실행하는 방법을 제공한다. 이 패키지는 ResourceDatabasePopulator 클래스를 제공하여 SQL 스크립트를 로드하고 실행할 수 있다.
HSQLDB (HyperSQL DataBase)
HSQLDB는 Java로 작성된 관계형 데이터베이스 관리 시스템(RDBMS)이다. HSQLDB는 고성능 인메모리 데이터베이스로서, 메모리 상에 데이터를 저장하거나 파일로 저장할 수 있다. HSQLDB는 ACID(Atomicity, Consistency, Isolation, Durability)를 보장하며, SQL 표준을 지원한다. 또한 JDBC 드라이버를 제공하여 Java 언어와의 통합이 용이하다.
HSQLDB는 다양한 용도로 사용될 수 있다. 가장 일반적인 용도는 테스트를 위한 인메모리 데이터베이스이다. 테스트를 위한 데이터베이스를 별도로 설치할 필요 없이 HSQLDB를 사용하여 테스트를 수행할 수 있다. 또한 작은 규모의 애플리케이션 개발에 적합하다. HSQLDB는 경량화되어 있으며, 자바 플랫폼에 포함되어 있기 때문에 자바 애플리케이션에 쉽게 통합할 수 있다.
HSQLDB의 장점을 정리하면 다음과 같다:
● 가벼운 인메모리 데이터베이스: HSQLDB는 인메모리 데이터베이스로 동작할 수 있어서 메모리 사용량이 적다.
● 빠른 개발: Spring Boot에서 HSQLDB를 사용하면 개발자는 데이터베이스를 설치하거나 관리하지 않아도 된다. 이로 인해 개발 속도가 빨라진다.
● 테스트 용이성: HSQLDB는 인메모리 데이터베이스로 동작할 수 있기 때문에 테스트를 위한 데이터베이스를 쉽게 구축할 수 있다.
● 다양한 데이터베이스 지원: HSQLDB는 다양한 데이터베이스를 지원한다. 예를 들어 MySQL과 호환되는 SQL을 지원하며, Oracle과 PostgreSQL과 같은 데이터베이스와의 호환성도 좋다.
Spring Boot에서 인메모리 HSQLDB를 사용할 경우 application.properties 파일에 별도의 HSQLDB 관련 설정이 필요하지 않다.
Spring Boot의 의존성 관리 기능을 이용하여 다음과 같이 HSQLDB 라이브러리를 프로젝트에 추가한다.
<dependency>
<groupId>org.hsqldb</groupId>
<artifactId>hsqldb</artifactId>
<scope>runtime</scope>
</dependency>
HSQLDB 데이터베이스 초기화
일반적으로 Spring Boot에서 데이터 초기화를 위해 데이터베이스 DDL 및 DML을 실행하려면 schema.sql 또는 data.sql 파일을 사용한다.
schema.sql 파일은 데이터베이스 스키마를 생성하고 수정하는 데 사용된다. 이 파일은 스프링 부트 애플리케이션을 시작할 때 한 번 실행된다.
data.sql 파일은 데이터베이스 초기 데이터를 삽입하는 데 사용된다. 이 파일은 schema.sql이 실행된 후 실행된다.
보통 src/main/resources 폴더 내에 schema.sql 또는 data.sql 파일을 만들어놓으면 스프링 부트가 자동으로 이 파일들을 읽어서 데이터베이스 초기화 작업을 수행한다. 만약 다른 폴더에 위치한 파일을 사용하려면 application.properties에 파일 경로를 지정할 수 있다.
● schema.sql
DROP TABLE book IF EXISTS;
CREATE TABLE book (
book_id VARCHAR(4),
title VARCHAR(40),
author VARCHAR(40),
publisher VARCHAR(40),
release_date VARCHAR(8),
isbn VARCHAR(13),
PRIMARY KEY(book_id)
);
● data.sql
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1001','장기20세기','조반니 아리기','그린비','20140520','9788976827821');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1002','신의 지문','그레이엄 핸콕','까치','20170120','9788972916307');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1003','신화의 이미지','조지프 캠벨','살림출판사','20060220','9788952204776');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1004','블랙아테나 1','마틴 버낼','소나무','20060110','9788971395479');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1005','판다의 엄지','스티븐 제이 굴드','사이언스북스','20160520','9788983717788');
INSERT INTO book(book_id,title,author,publisher,release_date,isbn) VALUES('1006','이기적 유전자','리처드 도킨스','을유문화사','20181020','9788932473901');
Model, Repository, Service
● Book.java
package com.yellow.jdbc;
public class Book {
private String bookId;
private String title;
private String author;
private String publisher;
private String releaseDate;
private String isbn;
public Book() {}
public Book(String bookId, String title, String author, String publisher, String releaseDate, String isbn) {
this.setBookId(bookId);
this.setTitle(title);
this.setAuthor(author);
this.setPublisher(publisher);
this.setReleaseDate(releaseDate);
this.setIsbn(isbn);
}
public String getBookId() {
return bookId;
}
public void setBookId(String bookId) {
this.bookId = bookId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getPublisher() {
return publisher;
}
public void setPublisher(String publisher) {
this.publisher = publisher;
}
public String getReleaseDate() {
return releaseDate;
}
public void setReleaseDate(String releaseDate) {
this.releaseDate = releaseDate;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
@Override
public String toString() {
return "bookId:" + bookId + ",title:" + title + ",author:" + author
+ ",publisher:" + publisher + ",releaseDate:" + releaseDate + ",isbn:" + isbn;
}
}
● BookRepository.java
package com.yellow.jdbc;
import java.util.List;
public interface BookRepository {
public List<Book> findAll();
public Book findById(String bookId);
public void save(Book book);
public void update(Book book);
public void delete(String bookId);
}
● BookRepositoryJDBC.java
package com.yellow.jdbc;
import java.util.List;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class BookRepositoryJDBC implements BookRepository {
private final JdbcTemplate jtm;
public BookRepositoryJDBC(JdbcTemplate jtm) {
this.jtm = jtm;
}
@Override
public List<Book> findAll() {
String sql = "SELECT book_id AS bookId,title,author,publisher,release_date AS releaseDate,isbn FROM book";
return jtm.query(sql, new BeanPropertyRowMapper<>(Book.class));
}
@Override
public Book findById(String id) {
String sql = "SELECT book_id AS bookId,title,author,publisher,release_date AS releaseDate,isbn FROM book WHERE book_id=?";
return jtm.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), new Object[]{id});
}
@Override
public void save(Book book) {
String sql = "INSERT INTO book (book_id,title,author,publisher,release_date,isbn) VALUES (?,?,?,?,?,?)";
jtm.update(sql, book.getBookId(),book.getTitle(),book.getAuthor(),book.getPublisher(),book.getReleaseDate(),book.getIsbn());
}
@Override
public void update(Book book) {
String sql = "UPDATE book SET title=?,author=?,publisher=?,release_date=?,isbn=? WHERE book_id = ?";
jtm.update(sql, book.getTitle(),book.getAuthor(),book.getPublisher(),book.getReleaseDate(),book.getIsbn(),book.getBookId());
}
@Override
public void delete(String id) {
String sql = "DELETE FROM book WHERE book_id=?";
jtm.update(sql, id);
}
}
● BookService.java
package com.yellow.jdbc;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class BookService {
private final BookRepository bookRepository;
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
public Book getBookById(String bookId) {
return bookRepository.findById(bookId);
}
public void saveBook(Book book) {
bookRepository.save(book);
}
public void updateBook(Book book) {
bookRepository.update(book);
}
public void deleteBook(String bookId) {
bookRepository.delete(bookId);
}
}
Controller, View(Thymeleaf)
● BookController.java
package com.yellow.jdbc;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class BookController {
private BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/listBook")
public String viewBookList(Model model) {
List<Book> books = bookService.getAllBooks();
model.addAttribute("allBooks", books);
return "listBook";
}
@GetMapping("/viewBook/{id}")
public String viewBook(@PathVariable("id") String id, Model model) {
Book book = bookService.getBookById(id);
model.addAttribute("book", book);
return "viewBook";
}
@GetMapping("/addViewBook")
public String addViewBook() {
return "addViewBook";
}
@PostMapping("/addBook")
public String addBook(@ModelAttribute Book book) {
bookService.saveBook(book);
return "redirect:/listBook";
}
@GetMapping("/updateViewBook/{id}")
public String updateViewBook(@PathVariable("id") String id, Model model) {
Book book = bookService.getBookById(id);
model.addAttribute("book", book);
return "updateViewBook";
}
@PostMapping("/updateBook/{id}")
public String updateBook(@PathVariable("id") String id, @ModelAttribute Book book) {
bookService.updateBook(book);
return "redirect:/listBook";
}
@GetMapping("/deleteBook/{id}")
public String deleteBook(@PathVariable("id") String id) {
bookService.deleteBook(id);
return "redirect:/listBook";
}
}
● index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Main Page</title>
</head>
<body>
<h1>Main Page</h1>
<p><a href="/listBook">List Book</a></p>
<p><a href="/addViewBook">Register Book</a></p>
</body>
</html>
● listBook.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Book List</title>
</head>
<body>
<h1>Book List</h1>
<br/>
<div>
<table border="1">
<tr>
<th>ID</th>
<th>제목</th>
<th>저자</th>
<th>Action</th>
</tr>
<tr th:each ="book : ${allBooks}">
<td><a th:href="@{/viewBook/{id}(id=${book.bookId})}" th:text="${book.bookId}"></a></td>
<td th:text="${book.title}"></td>
<td th:text="${book.author}"></td>
<td>
<a th:href="@{/updateViewBook/{id}(id=${book.bookId})}" >수정</a>,
<a th:href="@{/deleteBook/{id}(id=${book.bookId})}" >삭제</a>
</td>
</tr>
</table>
</div>
<br/><br/>
<a href="/">Main Page</a>
</body>
</html>
● viewBook.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>View Book</title>
</head>
<body>
<h1>Book Detail View</h1>
<div>
<span>Book ID : </span>
<span th:text="${book.bookId}"></span>
</div>
<div>
<span>제 목 : </span>
<span th:text="${book.title}"></span>
</div>
<div>
<span>저 자 : </span>
<span th:text="${book.author}"></span>
</div>
<div>
<span>출판사 : </span>
<span th:text="${book.publisher}"></span>
</div>
<div>
<span>출판일 : </span>
<span th:text="${book.releaseDate}"></span>
</div>
<div>
<span>ISBN : </span>
<span th:text="${book.isbn}"></span>
</div>
<br>
<br>
<div>
<a th:href="@{/updateViewBook/{id}(id=${book.bookId})}" >수정</a> /
<a th:href="@{/deleteBook/{id}(id=${book.bookId})}" >삭제</a>
</div>
<p><br></p>
<p><a href="/listBook">List Book</a></p>
</body>
</html>
● addViewBook.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Add a New Book</title>
</head>
<body>
<h1>Add a New Book</h1>
<form method="POST" action="/addBook">
<div>
<label>Book ID</label>
<input type="text" id="bookId" name="bookId">
</div>
<div>
<label>제 목 </label>
<input type="text" id="title" name="title">
</div>
<div>
<label>저 자 </label>
<input type="text" id="author" name="author">
</div>
<div>
<label>출판사</label>
<input type="text" id="publisher" name="publisher">
</div>
<div>
<label>출판일</label>
<input type="text" id="releaseDate" name="releaseDate">
</div>
<div>
<label>ISBN </label>
<input type="text" id="isbn" name="isbn">
</div>
<div>
<p><br></p>
<input type="submit" id="addNew" value="Submit">
</div>
</form>
<p><br></p>
<p><a href="/">Main</a></p>
</body>
</html>
● updateViewBook.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Update a Book</title>
</head>
<body>
<h1>Update a Book</h1>
<form th:action="@{/updateBook/{id}(id=${book.bookId})}" method="POST" >
<div>
<label>Book ID :</label>
<span th:text="${book.bookId}"></span>
<input type="hidden" th:field="*{book.bookId}" >
</div>
<div>
<label>제 목 </label>
<input type="text" th:field="*{book.title}" >
</div>
<div>
<label>저 자 </label>
<input type="text" th:field="*{book.author}" >
</div>
<div>
<label>출판사</label>
<input type="text" th:field="*{book.publisher}" >
</div>
<div>
<label>출판일</label>
<input type="text" th:field="*{book.releaseDate}" >
</div>
<div>
<label>ISBN </label>
<input type="text" th:field="*{book.isbn}" >
</div>
<div>
<p><br></p>
<input type="submit" id="addNew" value="Submit">
</div>
</form>
<p><br></p>
<p><a href="/">Main</a></p>
</body>
</html>
Application 실행
Project를 선택한 후, Run AS > Spring Boot App 를 하면 내장 Tomcat 서버에 배포된다. 브라우저에서 다음의 URL을 실행한다.
● http://localhost:8080/
● http://localhost:8080/listBook
● http://localhost:8080/addViewBook
● http://localhost:8080/listBook
● http://localhost:8080/viewBook/1004
● http://localhost:8080/updateViewBook/1004
● http://localhost:8080/listBook
등등…