기본 준비
server.port = 8080
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:orcl
spring.datasource.username=sqlid
spring.datasource.password=sqlpw
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
mybatis.mapper-locations=classpath:mapper/*.xml
spring.jpa.hibernate.ddl-auto=create
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
pom.xml 추가
properties 아래 추가
<repositories>
<repository>
<id>oracle</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
새로 추가 (layout, ModelMapper)
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.9</version>
</dependency>
Controller → Service → Repository → Entity → View(Thymeleaf)
📁common
header.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<div th:fragment="top">
<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
<div class="navbar-brand">
<a th:href="'/'" style="color:white">도서 정보 페이지</a>
</div>
</nav>
</div>
</html>
footer.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8">
<div class="footer" th:fragment="bottom">
<footer class="page-footer font-small cyan darken-3">
<div class="footer-copyright text-center py-3">
Book WebSite
</div>
</footer>
</div>
</html>
mylayout.html
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>
<head>
<meta charset="UTF-8">
<title>mylayout</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<link th:href="@{/css/mylayout.css}" rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<th:block layout:fragment="script"></th:block>
<th:block layout:fragment="css"></th:block>
</head>
<body>
<div th:replace="~{common/header::top}"></div>
<div layout:fragment="content"></div>
<div th:replace="~{common/footer::bottom}"></div>
</body>
</html>
📁templates/book
1. 기본 구조
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{common/mylayout}">
- xmlns:th : Thymeleaf 네임스페이스 (th:속성 사용 가능)
- xmlns:layout : Layout Dialect 사용을 위한 네임스페이스
- layout:decorate : common/mylayout.html 템플릿을 기반으로 현재 페이지 내용을 삽입
→ 즉, 이 페이지는 공통 레이아웃을 상속받는 자식 페이지
select.html
더보기
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{common/mylayout}">
>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="content" layout:fragment="content">
<h2>도서 목록 페이지</h2>
<form th:action="@{/blist}" method="get" class="form-inline mb-3 justify-content-center">
<div class="form-group mx-sm-3 mb-2">
<input type="text" name="keyword" th:value="${keyword}" class="form-control" placeholder="제목 또는 저자입력">
<button type="submit" class="btn btn-primary mb-2">검색</button>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>저자</th>
<th>출판사</th>
<th>가격</th>
<th>입고일</th>
<th>배송비</th>
<th>구매가능서점</th>
<th>보유수량</th>
<th>수정</th>
<th>삭제</th>
</tr>
</thead>
<tbody>
<tr th:each="book : ${bookList}">
<td th:text="${book.no}"></td>
<td th:text="${book.title}"></td>
<td th:text="${book.author}"></td>
<td th:text="${book.publisher}"></td>
<td th:text="${book.price}"></td>
<td th:text="${book.buy}"></td>
<td th:text="${book.kind}"></td>
<td th:text="${book.bookstore}"></td>
<td th:text="${book.count}"></td>
<td>수정</td>
<td>삭제</td>
</tr>
</tbody>
</table>
<div style="text-align:center">
<button type="button" class="btn btn-primary"
th:onclick="|location.href='@{/book/insert}'|">추가</button>
<button type="button" class="btn btn-danger">선택항목 삭제</button>
</div>
<br>
<!--페이지 설정-->
<!--0 이상일때-->
<div th:if="${bookList.totalPages > 0}">
<nav>
<ul class="pagination justify-content-center">
<li class="page-item" th:classappend="${bookList.hasPrevious()}?'':'disabled'">
<a class="page-link" th:href="@{/blist(page=${bookList.number-1}, size=${bookList.size}, keyword=${keyword})}">Previous</a>
</li>
<li class="page-item"
th:each="i : ${#numbers.sequence(0, bookList.totalPages-1)}" th:classappend="${i == bookList.number} ? 'active' : ''">
<a class="page-link"
th:href="@{/blist(page=${i}, size=${bookList.size}, keyword=${keyword})}" th:text="${i + 1}">
</a>
</li>
<li class="page-item" th:classappend="${bookList.hasNext()}?'':'disabled'">
<a class="page-link" th:href="@{/blist(page=${bookList.number+1}, size=${bookList.size}, keyword=${keyword})}">Next</a>
</li>
</ul>
</nav>
</div>
</div>
</body>
</html>
📁BookBean
public class BookBean {
private int no;
@NotBlank(message = "제목 필수 입력")
private String title;
@NotBlank(message = "저자 필수 입력")
private String author;
@NotBlank(message = "출판사 필수 입력")
private String publisher;
@NotNull(message = "가격을 입력하세요")
@Min(value = 1, message = "가격은 1원 이상이어야 합니다.")
private int price;
@NotEmpty(message = "입고일 필수 입력")
private String buy;
@NotEmpty(message = "배송비 필수 입력")
private String kind;
@NotEmpty(message = "구입가능 서점 하나 이상 선택")
private String bookstore;
@Min(value = 1, message = "보유수량은 1개 이상이어야 합니다.")
private int count;
📁Entity
BookEntity
@Entity
@Table(name = "book")
public class BookEntity {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq_gen")
@SequenceGenerator(
name = "book_seq_gen", // 시퀀스 제너레이터 이름
sequenceName = "BOOK_SEQ", // 실제 DB 시퀀스 이름
allocationSize = 1 // 증가 단위 (보통 1)
)
private int no;
private String title;
private String author;
private String publisher;
private int price;
private String buy;
private String kind;
private String bookstore;
private int count;
- @Entity : JPA가 관리하는 엔티티 클래스
- @Table(name="book") : 실제 DB 테이블 이름 명시
- @Id : 기본키 지정
- @GeneratedValue + @SequenceGenerator : Oracle 시퀀스로 PK 자동 증가
📁Controller
BookController
더보기
@Controller
public class BookController {
// (1) BookService를 주입받음 (비즈니스 로직 처리 담당)
@Autowired
BookService bookService;
// [1️⃣ 도서 목록 조회 + 검색 + 페이징]
@GetMapping(value = {"/", "blist"}) // (2) URL의 쿼리 파라미터를 받음
public String getAllBooks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "3") int size,
@RequestParam(required = false) String keyword,
Model model
){
// (3) DB의 전체 도서 수 조회
long totalCount = bookService.count();
System.out.println("totalCount : " + totalCount);
// (4) Service를 통해 현재 페이지에 표시할 도서 목록을 가져옴 : Page Class
Page<BookEntity> bookList = bookService.getBookEntity(page,size,keyword); // 현재 페이지에 출력할 3가지
// (5) 디버깅용 로그 출력 (페이징 정보)
System.out.println("bookList.getTotalElements() :" +bookList.getTotalElements()); //전체 데이터 개수
System.out.println("bookList.getTotalPages() :" +bookList.getTotalPages()); // 전체 페이지 수
System.out.println("bookList.getNumber() :" + bookList.getNumber()); // 현재 페이지 번호(0부터 시작)
System.out.println("bookList.getNumberOfElements()" + bookList.getNumberOfElements());
// (6) 화면(view)에 전달할 데이터 등록
model.addAttribute("bookList", bookList);
model.addAttribute("page", page);
model.addAttribute("size", size);
model.addAttribute("keyword", keyword);
model.addAttribute("totalCount", totalCount);
return "book/select";
}
// [2️⃣ 도서 등록 폼으로 이동]
@GetMapping(value = "book/insert")
public String insertBook(Model model){
model.addAttribute("bookBean", new BookBean());
return "book/insert";
}
// [3️⃣ 도서 등록 처리]
@PostMapping(value = "/book/insert")
public String insertBookProc(@Valid@ModelAttribute("bookBean") BookBean bookBean, BindingResult br, Model model){
if(br.hasErrors()){
return "/book/insert";
}
//bookBean을 Entity로 바꾸는 작업
BookEntity bookEntity = bookService.beanToEntity(bookBean);
bookService.saveBook(bookEntity);
return "redirect:/blist";
}
}
🔽bookBean을 Entity로 바꾸는 작업을 하는 이유 ==> Service에서 작업
BookEntity bookEntity = bookService.beanToEntity(bookBean);
bookService.saveBook(bookEntity);
1️⃣ 배경: Bean과 Entity의 역할이 다름
- BookBean 은 폼과 화면용
- BookEntity 는 데이터베이스 저장용
2️⃣ save() 메서드는 BookEntity만 저장할 수 있음
👉 따라서 Bean → Entity 변환 과정이 필요
📁Service
BookService
📘 [BookService 클래스 설명]
Controller ↔ Service ↔ Repository 사이에서 데이터를 주고받는 중간 다리 역할
- Controller가 요청을 보내면 Service가 로직을 처리하고
- Repository를 통해 DB와 실제로 데이터를 주고받는다.
ModelMapper 객체 생성 - Bean ↔ Entity 간 자동 변환을 도와주는 라이브러리
더보기
package com.example.Ex02.service;
/*controller - BookService - repository :중간 연결다리*/
import com.example.Ex02.bean.BookBean;
import com.example.Ex02.entity.BookEntity;
import com.example.Ex02.repository.BookRepository;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
@Service
public class BookService {
@Autowired
BookRepository bookRepository;
// ModelMapper : Bean ↔ Entity 자동 변환 도구
private static ModelMapper modelMapper = new ModelMapper();
// 1️⃣ 도서 총 개수 조회
public long count() {
return bookRepository.count();
}
// 2️⃣ 도서 목록 조회 (검색 + 페이징)
// Pageable : 페이징 정보와 정렬 기준 지정 (no 기준 내림차순)
public Page<BookEntity> getBookEntity(int page, int size, String keyword) {
Pageable pageable = PageRequest.of(page,size, Sort.by("no").descending());
// 검색어(keyword)가 없으면 전체 목록 반환
if (keyword == null || keyword.isEmpty()){
return bookRepository.findAll(pageable);
}
// 검색어가 있으면 제목 또는 저자에 keyword가 포함된 결과만 반환
else{
return bookRepository.findByTitleContainingOrAuthorContaining(keyword, keyword, pageable);
}
}
// 3️⃣ Bean → Entity 변환
public BookEntity beanToEntity(BookBean bookBean) {
BookEntity bookEntity= modelMapper.map(bookBean, BookEntity.class);
return bookEntity;
}
// 4️⃣ 도서 저장 (등록 또는 수정)
public void saveBook(BookEntity bookEntity){
bookRepository.save(bookEntity);
}
}
📁Repository (인터페이스)
Controller → Service → Repository(interface) → DB
public interface BookRepository extends JpaRepository<BookEntity, Integer> {
Page<BookEntity> findByTitleContainingOrAuthorContaining(String keyword, String keyword1, Pageable pageable);
}'기초 및 언어 > ▶ Spring' 카테고리의 다른 글
| 19. Spring_SLF4J Logger (0) | 2025.12.03 |
|---|---|
| 18. Spring_외부데이터 출력(.csv/json/) (0) | 2025.10.16 |
| 16. Spring_Thymeleaf 헤더(Fragment)로 로그인 상태 표시하기 (0) | 2025.10.10 |
| 15. Spring Boot JPA + 쿼리 어노테이션(jpql) (0) | 2025.10.01 |
| 14. Spring Number, 단위마다 , 붙이기 (0) | 2025.10.01 |
