스타필드 도서관 웹 애플리케이션의 프로젝트 구조와 전체 코드이다.
코드뿐만 아니라 구현 과정 및 구현에 사용된 기술에 대한 설명을 덧붙였다.
구현한 웹 애플리케이션의 결과를 이전 글에서 확인할 수 있다.
https://github.com/Isaac-Seungwon/suwon-starfield-library.git
위 GitHub Link에서 소스 코드와 폴더 구조를 확인할 수 있다.
🔎프로젝트 구조
project-root
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com
│ │ │ └── test
│ │ │ └── jpa
│ │ │ ├── JpaApplication.java
│ │ │ ├── MainController.java
│ │ │ ├── MybatisConfig.java
│ │ │ ├── controller
│ │ │ │ └── AuthorController.java
│ │ │ │ └── BookController.java
│ │ │ ├── domain
│ │ │ │ ├── AuthorDTO.java
│ │ │ │ └── BookDTO.java
│ │ │ ├── mapper
│ │ │ │ └── BookMapper.java
│ │ │ ├── model
│ │ │ │ ├── Author.java
│ │ │ │ ├── Book.java
│ │ │ │ └── Product.java
│ │ │ ├── repository
│ │ │ │ ├── AuthorRepository.java
│ │ │ │ └── BookRepository.java
│ │ │ └── service
│ │ │ ├── AuthorListener.java
│ │ │ ├── AuthorService.java
│ │ │ └── BookService.java
│ │ ├── resources
│ │ │ ├── database
│ │ │ │ ├── ddl.sql
│ │ │ │ └── dml.sql
│ │ │ ├── mapper
│ │ │ │ └── BookMapper.xml
│ │ │ ├── static
│ │ │ │ └── assets
│ │ │ │ ├── css
│ │ │ │ │ └── style.css
│ │ │ │ └── js
│ │ │ │ └── script.js
│ │ │ └── templates
│ │ │ └── index.html
│ │ └── application.properties
│ └── test
│ └── java
│ └── com
│ └── test
│ └── jpa
│ └── (test classes)
├── pom.xml
└── README.md
MyBatis 사용하여 Mapper를 추가할 경우, jpa 하위 패키지에 mapper 패키지를 생성하고 basePackages에 Mapper 인터페이스 패키지 위치를 작성하면 된다. 그리고 resources 폴더 내에 mapper를 생성하여 xml 파일로 쿼리를 작성하고 관리할 수 있다.
📁전체 코드
JpaApplication
package com.test.jpa;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* JpaApplication은 Spring Boot 애플리케이션의 시작점입니다.
* Spring Boot 애플리케이션을 실행하는데 사용됩니다.
* 애플리케이션을 실행할 때 Shutdown Hook을 추가하여 종료 메시지를 출력합니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@SpringBootApplication
public class JpaApplication {
public static void main(String[] args) {
SpringApplication.run(JpaApplication.class, args);
// Shutdown Hook 추가
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("애플리케이션이 종료됩니다.");
}));
}
}
JpaApplication 클래스는 Spring Boot 애플리케이션의 시작점이다. 이 클래스는 main 메서드를 포함하고 있으며, 이 메서드를 통해 Spring Boot 애플리케이션을 실행한다. 그리고 @SpringBootApplication 어노테이션은 이 클래스가 Spring Boot의 주요 구성 요소임을 나타내며, Spring Boot의 자동 구성 기능을 활성화한다.
또한, 이 클래스는 애플리케이션이 종료될 때 Shutdown Hook을 추가하여 종료 메시지를 출력하는 기능을 포함하고 있다. Shutdown Hook은 JVM이 종료되기 직전에 실행되는 스레드로, 애플리케이션이 종료될 때 추가적인 작업을 수행할 수 있다.
MainController.java
package com.test.jpa;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.test.jpa.service.BookService;
/**
* MainController는 Spring MVC에서 웹 요청을 처리하는 컨트롤러입니다.
* 웹 애플리케이션의 메인 페이지를 제공하기 위해 "/" 경로에 대한 GET 요청을 처리합니다.
*
* 작성자: 이승원
* 작성일: 2024.03.05.
*/
@Controller
public class MainController {
@Autowired
private BookService bookService;
/**
* 웹 애플리케이션의 메인 페이지를 반환합니다.
*
* @param model 뷰에 데이터를 전달하기 위한 Model 객체
* @return "index" 문자열
*/
@GetMapping("/")
public String index(Model model) {
// 전체 책 권수 가져오기
int totalCount = bookService.getTotalCount();
// 모델에 전체 책 권수 추가
model.addAttribute("totalCount", totalCount);
return "index";
}
}
MainController 클래스는 Spring MVC에서 웹 요청을 처리하는 컨트롤러로, 주로 웹 애플리케이션의 메인 페이지를 제공하기 위해 사용된다. 웹 애플리케이션의 메인 페이지와 관련된 요청을 처리하고, 그에 따른 데이터를 뷰에 전달하는 역할을 담당하고 있다.
MybatisConfig.java
package com.test.jpa;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
/*
* MyBatis 설정 파일입니다.
* Spring Boot 애플리케이션에서 MyBatis를 구성하고 매퍼 인터페이스를 검색하여 빈으로 등록합니다.
* 데이터베이스 연결에 사용되는 DataSource를 설정하고, SqlSessionFactory를 생성하여 MyBatis와 Spring을 통합합니다.
* MyBatis 매퍼 파일의 위치를 지정합니다.
*
* 작성자: 이승원
* 작성일: 2024.03.06.
*/
@Configuration
@MapperScan(basePackages = { "com.test.jpa.mapper" }, sqlSessionFactoryRef = "sqlSessionFactory")
public class MybatisConfig {
/*
* SqlSessionFactory 빈을 생성합니다.
*
* @param dataSource 데이터베이스 연결에 사용되는 DataSource
* @return SqlSessionFactory
*/
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.test.jpa.model");
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
}
MybatisConfig 클래스는 MyBatis 설정 파일이다. 이 클래스는 Spring Boot 애플리케이션에서 MyBatis를 설정하고, 매퍼 인터페이스를 검색하여 빈으로 등록하는 역할을 한다. 또한, 데이터베이스 연결에 사용되는 DataSource를 설정하고, SqlSessionFactory를 생성하여 MyBatis와 Spring을 통합한다. 또한, MyBatis 매퍼 파일의 위치를 지정한다.
@Configuration 어노테이션은 Spring Boot 애플리케이션의 설정 파일임을 나타낸다.
@MapperScan 어노테이션은 매퍼 인터페이스의 위치를 지정하고, SqlSessionFactory를 참조하는 이름을 설정한다.
@Qualifier("dataSource") 어노테이션은 dataSource라는 이름의 빈을 찾아서 주입한다.
sqlSessionFactory 메서드는 SqlSessionFactory를 빈으로 생성하고, @Bean 어노테이션을 사용하여 Spring IoC 컨테이너에 빈을 등록한다.
sqlSessionFactoryBean 객체는 SqlSessionFactory를 생성하기 위한 설정을 담고 있다.
여기에 setDataSource와 setTypeAliasesPackage로 DataSource 설정 및 TypeAlias를 패키지로 지정하고, 매퍼 파일의 위치를 설정한다.
new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml")는 classpath에 있는 mapper 디렉터리 안에 있는 XML 매퍼 파일을 검색한다는 의미이다. 그리고 return sqlSessionFactoryBean.getObject()는 SqlSessionFactoryBean에서 SqlSessionFactory 객체를 반환한다.
application.properties
# Server Port
server.port=8090
# JSP View Resolver
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
# 서블릿 컨텍스트 경로
server.servlet.context-path=/starfield
# Database connection
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=starfield_library
spring.datasource.password=suwon1234
# HikariCP
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=2000000
spring.datasource.hikari.connection-timeout=30000
# Hibernate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.OracleDialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
application.properties 파일은 Spring Boot 애플리케이션의 구성을 지정하는 데 사용되는 외부 구성 파일이다. 이 파일은 애플리케이션의 속성들을 설정하고, 데이터베이스 연결 정보, 포트 번호, 로깅 레벨, 외부 서비스의 URL 등을 정의할 수 있다.
1. Server Port
server.port=8090
이 부분은 애플리케이션의 포트를 설정하는 부분이다.
현재 설정된 포트는 8090이며, 이 포트를 통해 애플리케이션에 접속할 수 있다.
2. JSP View Resolver
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
이 부분은 Spring MVC의 뷰 리졸버(View Resolver)에 관한 설정이다.
JSP 파일이 위치한 경로와 확장자를 지정하며, 설정된 값에 따라 Spring MVC는 Controller에서 반환한 뷰 이름을 해석하여 실제 JSP 파일의 경로를 찾는다.
3. 서블릿 컨텍스트 경로
server.servlet.context-path=/starfield
이 부분은 서블릿 컨텍스트 경로를 설정하는 부분으로, 서블릿 컨텍스트 경로는 애플리케이션의 루트 경로를 의미한다.
현재 설정된 경로는 "/starfield"이며, 이 경로를 통해 애플리케이션에 접근할 수 있다.
4. Database Connection
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=starfield_library
spring.datasource.password=suwon1234
이 부분은 데이터베이스 연결에 관한 설정이다.
Oracle 데이터베이스를 사용하며, JDBC 드라이버 클래스와 연결 URL이 지정되어 있다.
데이터베이스 접속에 필요한 사용자 이름과 비밀번호가 설정된다.
5. HikariCP
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=2000000
spring.datasource.hikari.connection-timeout=30000
이 부분은 HikariCP 데이터베이스 커넥션 풀의 설정이다.
커넥션 풀의 최소 및 최대 크기, 유휴 시간, 최대 생명주기, 연결 시도 제한 시간 등이 설정되어 있다.
6. Hibernate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.OracleDialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
이 부분은 Hibernate 설정에 관한 부분이다.
Hibernate의 데이터베이스 방언, SQL 쿼리 출력 여부, 쿼리 포맷팅 여부, DDL 자동 생성 전략이 설정되어 있다.
현재 설정된 DDL 자동 생성 전략은 "update"로, 엔티티 클래스에 변경이 생길 때마다 데이터베이스의 스키마를 자동으로 업데이트한다.
controller
AuthorController.java
package com.test.jpa.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.test.jpa.model.Author;
import com.test.jpa.service.AuthorService;
/**
* 작가 컨트롤러(AuthorController)는 작가에 관련된 HTTP 요청을 처리하는 엔드포인트를 제공합니다.
* 작가를 생성하고, 작가 이름으로 작가를 조회하는 기능을 제공합니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@RestController
@RequestMapping("/authors")
public class AuthorController {
@Autowired
private AuthorService authorService;
/**
* POST 메서드를 통해 새로운 작가를 생성합니다.
* 요청 본문에는 작가의 정보가 JSON 형식으로 포함되어야 합니다.
*
* @param author 생성할 작가의 정보
* @return 생성된 작가와 함께 HTTP 상태 코드 201을 응답합니다.
*/
@PostMapping
public ResponseEntity<Author> createAuthor(@RequestBody Author author) {
Author createdAuthor = authorService.createAuthor(author);
return ResponseEntity.status(HttpStatus.CREATED).body(createdAuthor);
}
/**
* GET 메서드를 통해 작가 이름으로 작가를 조회합니다.
* 작가 이름은 URL 경로 변수로 전달되며, 경로에 포함된 이름을 기반으로 작가를 검색합니다.
*
* @param name 조회할 작가의 이름
* @return 조회된 작가와 함께 HTTP 상태 코드 200을 응답합니다.
*/
@GetMapping("/{name}")
public ResponseEntity<Author> getAuthorByName(@PathVariable String name) {
Author author = authorService.getAuthorByName(name);
return ResponseEntity.ok(author);
}
}
AuthorController 클래스는 Spring MVC를 사용하여 작가(Author)와 관련된 HTTP 요청을 처리하는 컨트롤러 클래스이다. 이 클래스는 작가를 생성하고 작가 이름으로 작가를 조회하는 엔드포인트를 제공한다.
1. @RestController
@RestController
Spring MVC의 @RestController 어노테이션을 사용하여 RESTful 한 웹 서비스에서 HTTP 요청을 처리하는 컨트롤러임을 나타내고 있다.
2. 의존성 주입
@Autowired
private AuthorService authorService;
의존성 주입 부분에서는 AuthorService 인터페이스를 구현한 빈을 주입받아 사용하고 있다. 이렇게 함으로써, 작가와 관련된 비즈니스 로직을 처리할 수 있다.
3. POST 메서드: createAuthor
@PostMapping
public ResponseEntity<Author> createAuthor(@RequestBody Author author) {
Author createdAuthor = authorService.createAuthor(author);
return ResponseEntity.status(HttpStatus.CREATED).body(createdAuthor);
}
먼저 createAuthor 메서드는 HTTP POST 요청을 통해 새로운 작가를 생성한다. 요청 본문에는 작가의 정보가 JSON 형식으로 포함되어 있어야 한다. 그러면 authorService.createAuthor(author)를 호출하여 작가를 생성하고, 생성된 작가 정보와 함께 HTTP 상태 코드 201(CREATED)을 클라이언트에게 응답한다.
4. GET 메서드: getAuthorByName
@GetMapping("/{name}")
public ResponseEntity<Author> getAuthorByName(@PathVariable String name) {
Author author = authorService.getAuthorByName(name);
return ResponseEntity.ok(author);
}
getAuthorByName 메서드는 HTTP GET 요청을 통해 작가 이름으로 작가를 조회한다. 이때, 작가 이름은 URL 경로 변수로 전달되며, 경로에 포함된 이름을 기반으로 작가를 검색한다. 그 후 authorService.getAuthorByName(name)을 호출하여 작가를 조회하고, 조회된 작가 정보와 함께 HTTP 상태 코드 200(OK)을 클라이언트에게 응답한다.
BookController.java
package com.test.jpa.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.test.jpa.domain.AuthorDTO;
import com.test.jpa.domain.BookDTO;
import com.test.jpa.model.Author;
import com.test.jpa.model.Book;
import com.test.jpa.service.BookService;
/**
* 책 컨트롤러(BookController)는 책에 관련된 HTTP 요청을 처리하는 엔드포인트를 제공합니다.
* 책을 조회하고, 생성하고, 수정하고, 삭제하는 기능을 제공합니다.
* 책은 고유한 ID를 가지며, 제목, 저자, 가격 등의 속성을 포함합니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@RestController
@RequestMapping("/books")
public class BookController {
@Autowired
private BookService bookService;
/**
* 모든 책을 조회합니다.
*
* @return 조회된 모든 책 목록과 함께 HTTP 상태 코드 200을 응답합니다.
*/
@GetMapping("/all")
public ResponseEntity<List<Book>> getAllBooks() {
List<Book> books = bookService.getAllBooks();
return ResponseEntity.ok(books);
}
/**
* 특정 ID를 가진 책을 조회합니다.
*
* @param id 조회할 책의 ID
* @return 조회된 책과 함께 HTTP 상태 코드 200을 응답합니다.
*/
@GetMapping("/{id}")
public ResponseEntity<Book> getBookById(@PathVariable Long id) {
Book book = bookService.getBookById(id);
return ResponseEntity.ok(book);
}
/**
* 새로운 책을 생성합니다.
*
* @param bookDTO 생성할 책의 정보를 담은 DTO 객체
* @return 생성된 책과 함께 HTTP 상태 코드 201을 응답합니다.
*/
@PostMapping
public ResponseEntity<Book> createBook(@RequestBody BookDTO bookDTO) {
Book book = bookService.createBook(bookDTO.getTitle(), bookDTO.getAuthor().getName(), bookDTO.getPrice());
return new ResponseEntity<>(book, HttpStatus.CREATED);
}
/**
* 특정 ID를 가진 책을 수정합니다.
*
* @param id 수정할 책의 ID
* @param bookDTO 수정할 책의 정보를 담은 DTO 객체
* @return 수정된 책과 함께 HTTP 상태 코드 200을 응답합니다.
*/
@PutMapping("/{id}")
public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody BookDTO bookDTO) {
Book existingBook = bookService.getBookById(id);
// 기존 작가 정보 사용
Author author = existingBook.getAuthor();
// 책 정보 업데이트
existingBook.setTitle(bookDTO.getTitle());
existingBook.setPrice(bookDTO.getPrice());
// 작가 정보 업데이트
AuthorDTO authorDTO = bookDTO.getAuthor();
author.setName(authorDTO.getName());
Book updatedBook = bookService.updateBook(existingBook);
return ResponseEntity.ok(updatedBook);
}
/**
* 특정 ID를 가진 책을 삭제합니다.
*
* @param id 삭제할 책의 ID
* @return HTTP 상태 코드 204를 응답합니다.
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
return ResponseEntity.noContent().build();
}
}
BookController 클래스는 Spring MVC를 사용하여 책(Book)과 관련된 HTTP 요청을 처리하는 컨트롤러 클래스이다. 주요 기능으로는 책을 조회하고, 생성하고, 수정하고, 삭제하는 엔드포인트를 제공한다.
1. @RestController 및 @RequestMapping
@RestController
@RequestMapping("/books")
@RestController는 해당 클래스가 RESTful 한 웹 서비스에서 HTTP 요청을 처리하는 컨트롤러임을 나타낸다.
@RequestMapping("/books")는 이 컨트롤러에서 처리하는 요청의 기본 경로를 "/books"로 지정한다.
2. 의존성 주입
@Autowired
private BookService bookService;
BookService 인터페이스를 구현한 빈을 주입받아 사용한다. 이를 통해 책과 관련된 비즈니스 로직을 처리할 수 있다.
3. GET 메서드: getAllBooks
@GetMapping("/all")
public ResponseEntity<List<Book>> getAllBooks() {
List<Book> books = bookService.getAllBooks();
return ResponseEntity.ok(books);
}
HTTP GET 요청을 처리한다. /books/all 엔드포인트로 모든 책을 조회하며, 조회된 책 목록과 함께 HTTP 상태 코드 200(OK)을 클라이언트에게 응답한다.
4. GET 메서드: getBookById
@GetMapping("/{id}")
public ResponseEntity<Book> getBookById(@PathVariable Long id) {
Book book = bookService.getBookById(id);
return ResponseEntity.ok(book);
}
HTTP GET 요청을 처리한다. /books/{id} 엔드포인트로 특정 ID를 가진 책을 조회하며, 조회된 책과 함께 HTTP 상태 코드 200(OK)을 클라이언트에게 응답한다.
5. POST 메서드: createBook
@PostMapping
public ResponseEntity<Book> createBook(@RequestBody BookDTO bookDTO) {
Book book = bookService.createBook(bookDTO.getTitle(), bookDTO.getAuthor().getName(), bookDTO.getPrice());
return new ResponseEntity<>(book, HttpStatus.CREATED);
}
HTTP POST 요청을 처리한다. /books 엔드포인트로 새로운 책을 생성한다.
요청 본문에는 책의 정보가 JSON 형식으로 포함되어 있어야 하며, 생성된 책과 함께 HTTP 상태 코드 201(CREATED)을 클라이언트에게 응답한다.
6. PUT 메서드: updateBook
@PutMapping("/{id}")
public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody BookDTO bookDTO) {
Book existingBook = bookService.getBookById(id);
// 기존 작가 정보 사용
Author author = existingBook.getAuthor();
// 책 정보 업데이트
existingBook.setTitle(bookDTO.getTitle());
existingBook.setPrice(bookDTO.getPrice());
// 작가 정보 업데이트
AuthorDTO authorDTO = bookDTO.getAuthor();
author.setName(authorDTO.getName());
Book updatedBook = bookService.updateBook(existingBook);
return ResponseEntity.ok(updatedBook);
}
HTTP PUT 요청을 처리한다. /books/{id} 엔드포인트로 특정 ID를 가진 책을 수정한다.
요청 본문에는 수정할 책의 정보가 JSON 형식으로 포함되어 있어야 하며, 수정된 책과 함께 HTTP 상태 코드 200(OK)을 클라이언트에게 응답한다.
7. DELETE 메서드: deleteBook
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteBook(@PathVariable Long id) {
bookService.deleteBook(id);
return ResponseEntity.noContent().build();
}
HTTP DELETE 요청을 처리한다. /books/{id} 엔드포인트로 특정 ID를 가진 책을 삭제한다.
삭제된 후에는 HTTP 상태 코드 204(NO CONTENT)를 클라이언트에게 응답한다.
domain
AuthorDTO.java
package com.test.jpa.domain;
import lombok.Data;
/**
* 작가 데이터 전송 객체(DTO)는 작가의 정보를 전달하는 데 사용됩니다.
* 작가는 고유한 ID와 이름을 가지고 있습니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@Data
public class AuthorDTO {
private Long id; // 작가의 ID
private String name; // 작가의 이름
}
BookDTO.java
package com.test.jpa.dto;
import lombok.Data;
/**
* 책 데이터 전송 객체(DTO)는 책의 정보를 전달하는 데 사용됩니다.
* 각 책은 고유한 ID, 제목, 작가 정보, 가격을 가지고 있습니다.
* 작가 정보는 작가 데이터 전송 객체(AuthorDTO)를 사용하여 표현됩니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@Data
public class BookDTO {
private Long id; // 책의 ID
private String title; // 책의 제목
private AuthorDTO author; // 책의 작가 정보
private String price; // 책의 가격
}
mapper
BookMapper.java
package com.test.jpa.mapper;
import java.util.Optional;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import com.test.jpa.domain.BookDTO;
/*
* Book 엔티티와 관련된 데이터베이스 액세스를 위한 매퍼 인터페이스입니다.
* 전체 책 권수를 조회하거나, 책의 아이디를 기반으로 책을 조회할 수 있습니다.
*
* 작성자: 이승원
* 작성일: 2024.03.06.
*/
@Mapper
public interface BookMapper {
/**
* 전체 책 권수를 반환하는 메서드입니다.
*
* @return 전체 책 권수
*/
int getTotalCount();
/**
* 책의 아이디를 기반으로 책을 조회하는 메서드입니다.
*
* @param id 조회할 책의 아이디
* @return 조회된 책 엔티티의 Optional 객체
*/
@Select("SELECT * FROM Book where id = #{id}")
public Optional<BookDTO> findByBookId(long id);
}
book-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">
<!-- book-mapper.xml 파일은 책 관련 데이터베이스 액세스를 위한 매퍼입니다. -->
<mapper namespace="com.test.jpa.mapper.BookMapper">
<!-- 결과 매핑 -->
<resultMap id="bookResultMap" type="Book">
<id column="id" property="id" />
<result column="title" property="title" />
<result column="author" property="author"
typeHandler="org.apache.ibatis.type.StringTypeHandler" />
</resultMap>
<!-- 책 권수 조회 쿼리 -->
<select id="getTotalCount" resultType="int">
SELECT COUNT(*) FROM Book
</select>
</mapper>
기존에 xml 파일은 BookMapper.xml처럼 클래스명처럼 작성해 왔는데, book-mapper.xml으로 작성하는 게 일반적인 듯하다.
결과 매핑에서 typeHandler="org.apache.ibatis.type.StringTypeHandler"를 사용하는 이유는 데이터베이스 컬럼의 타입과 자바 객체의 프로퍼티 타입이 일치하지 않을 때 발생하는 매핑 문제를 해결하기 위해서이다. 이 경우, 데이터베이스 컬럼의 타입이 문자열이 아닌 경우에도 자바 객체의 해당 프로퍼티를 문자열로 매핑할 수 있도록 지정해 준다.
예를 들어, 데이터베이스 컬럼의 타입이 VARCHAR가 아니라 CHAR 또는 CLOB 등의 문자열이 아닌 타입일 경우, MyBatis가 자동으로 해당 값을 자바 객체의 문자열 프로퍼티에 매핑하는 데 문제가 발생할 수 있다. 이런 경우, StringTypeHandler를 사용하여 명시적으로 문자열로 매핑하도록 지정함으로써 이러한 문제를 해결할 수 있다.
model
Author.java
package com.test.jpa.model;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.annotations.Immutable;
import com.test.jpa.service.AuthorListener;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.SequenceGenerator;
import lombok.Data;
/**
* 작가 엔티티(Author)는 책의 저자 정보를 나타내는데 사용됩니다.
* 각 작가는 고유한 ID와 이름을 가지고 있으며, 여러 책을 작성할 수 있습니다.
* 작가의 책 정보는 일대다 관계로 표현되며, CascadeType.ALL을 사용하여 작가가 삭제될 때 해당 작가의 책도 함께 삭제됩니다.
* 작가 엔티티는 AuthorListener 클래스를 리스너로 사용하여 업데이트 이벤트를 처리합니다.
* Hibernate의 @DynamicUpdate 어노테이션을 사용하여 엔티티가 업데이트될 때 동적으로 업데이트 쿼리를 생성합니다.
* 작가 ID는 시퀀스 생성기를 사용하여 자동으로 생성되며, 초기값은 1이며, allocationSize는 1로 설정됩니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@Data
@Entity
@EntityListeners(AuthorListener.class)
@DynamicUpdate
@SequenceGenerator(
name = "author_seq_generator",
sequenceName = "author_seq",
initialValue = 1,
allocationSize = 1
)
public class Author {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "author_seq_generator")
private Long id; // 작가의 ID
private String name; // 작가의 이름
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL)
private List<Book> books = new ArrayList<>(); // 작가의 책 목록
}
Author는 작가를 나타내는 엔티티 클래스이다.
1. 클래스 선언
@Entity는 JPA 엔티티 클래스임을 나타내는 어노테이션이다.
@Data는 Lombok 라이브러리의 @Data 어노테이션을 사용하여 getter, setter, equals, hashCode, toString 등의 메서드를 자동으로 생성한다.
2. 속성
id는 작가의 고유한 ID를 나타내고, name은 작가의 이름을 나타내는 속성이다.
books은 작가가 저술한 책 목록을 나타내는 속성이다. 일대다(OneToMany) 관계로 표현되며, 하나의 작가가 여러 책을 저술할 수 있다.
3. 어노테이션
@Id는 해당 속성이 엔티티의 식별자(primary key) 임을 나타낸다.
@GeneratedValue는 엔티티의 ID 값을 자동으로 생성하는 방법을 지정한다. 여기서는 시퀀스(generator)를 사용하여 생성한다.
@SequenceGenerator는 시퀀스 생성기를 정의하는 어노테이션이다. 시퀀스의 이름, 초기값, 증가치 등을 설정할 수 있다.
@OneToMany는 엔티티 간의 일대다 관계를 나타내는 어노테이션으로, 한 작가는 여러 책을 가질 수 있다.
@EntityListeners(AuthorListener.class)로 AuthorListener 클래스를 작가 엔티티의 리스너로 등록한다. 이 리스너는 엔티티의 변경 이벤트를 처리한다.
@DynamicUpdate는 Hibernate의 어노테이션으로, 엔티티가 업데이트될 때 동적으로 업데이트 쿼리를 생성한다.
4. 생성자
기본 생성자는 생략되어 있으며, Lombok의 @Data 어노테이션을 사용하여 기본 생성자가 자동으로 생성된다.
Book.java
package com.test.jpa.model;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 책 엔티티(Book)는 제품(Product)을 상속받아 책의 정보를 나타내는데 사용됩니다.
* 각 책은 고유한 ID, 제목, 작가 정보, 가격을 가지고 있습니다.
* 책의 작가 정보는 다대일 관계로 표현되며, 책이 삭제되어도 작가 정보는 유지됩니다.
* 책의 ID는 IDENTITY 전략을 사용하여 자동으로 생성되도록 설정되었습니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@Data
@Entity
@EqualsAndHashCode(callSuper = true)
public class Book extends Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 책의 ID
private String title; // 책의 제목
@ManyToOne
@JoinColumn(name = "author_id")
@JsonIgnoreProperties("books") // 무한루프 방지
private Author author; // 책의 작가 정보
private String price; // 책의 가격
}
Book 엔티티는 책의 정보를 나타내는 클래스이다. 책은 제품(Product)을 상속받아서 만들어졌다.
1. 클래스 선언
@Entity는 JPA 엔티티 클래스임을 나타낸다. 그리고 @Data는 Lombok 라이브러리의 @Data 어노테이션을 사용하여 getter, setter, equals, hashCode, toString 등의 메서드를 자동으로 생성한다.
@EqualsAndHashCode(callSuper = true)는 상위 클래스인 Product의 필드도 포함하여 equals와 hashCode 메서드를 생성한다.
2. 속성
id는 책의 고유한 ID를 나타내고, title은 책의 제목을 나타내며 price는 책의 가격을 나타낸다.
author는 책의 작가 정보를 나타낸다. 다대일(ManyToOne) 관계로 표현되어 하나의 책은 하나의 작가에게 속한다.
3. 어노테이션
@Id는 해당 속성이 엔티티의 식별자(primary key) 임을 나타낸다.
@GeneratedValue는 엔티티의 ID 값을 자동으로 생성하는 방법을 지정한다. 여기서는 IDENTITY 전략을 사용하여 자동으로 생성한다.
@ManyToOne는 다대일 관계를 나타내는 어노테이션으로, 여러 책이 하나의 작가에 속한다.
@JoinColumn(name = "author_id")는 외래 키를 매핑할 때 사용하는 어노테이션으로, 해당 엔티티의 필드를 참조한다.
@JsonIgnoreProperties("books")는 JSON 직렬화 시 무한루프를 방지하기 위해 books 필드를 무시한다.
4. 생성자
기본 생성자는 생략되어 있으며, Lombok의 @Data 어노테이션을 사용하여 기본 생성자가 자동으로 생성된다.
Product.java
package com.test.jpa.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
import lombok.Data;
/**
* 제품 엔티티(Product)는 책과 같은 제품들의 공통 정보를 나타내는데 사용됩니다.
* 각 제품은 고유한 ID와 이름을 가지고 있습니다.
* 추상 클래스로 정의되어 있으며, 제품에 따라 구체적인 엔티티가 상속받아 사용됩니다.
* 제품의 ID는 TABLE 전략을 사용하여 자동으로 생성되도록 설정되었습니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@Data
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Product {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private Long id; // 제품의 ID
private String name; // 제품의 이름
}
Product 엔티티는 제품의 공통 정보를 담는 추상 클래스로, 책과 같은 여러 제품들의 기본 특징을 정의한다.
1. 클래스 선언
@Entity는 JPA 엔티티 클래스임을 나타낸다. 그리고 @Data는 Lombok 라이브러리의 어노테이션으로, getter, setter, equals, hashCode, toString 등의 메서드를 자동으로 생성한다.
2. 속성
id는 제품의 고유한 ID를 나타내고, name은 제품의 이름을 나타낸다.
3. 어노테이션
@Id는 해당 속성이 엔티티의 식별자(primary key)임을 나타낸다.
@GeneratedValue는 엔티티의 ID 값을 자동으로 생성하는 방법을 지정한다. 여기서는 TABLE 전략을 사용하여 자동으로 생성된다.
@Inheritance는 상속 관계를 정의하는 어노테이션으로, 여기서는 TABLE_PER_CLASS 전략을 사용하여 자식 엔티티마다 테이블이 생성된다.
4. 추상 클래스
abstract class Product: 추상 클래스로 정의되어 있다. 이는 구체적인 제품 엔티티 클래스들이 이 클래스를 상속받아 사용한다는 것을 의미한다.
repository
AuthorRepository.java
package com.test.jpa.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.test.jpa.model.Author;
/**
* AuthorRepository 인터페이스는 작가 엔티티에 대한 데이터 액세스 기능을 제공합니다.
* Spring Data JPA의 JpaRepository를 확장하여 작가 엔티티에 대한 CRUD 기능을 지원합니다.
* 작가의 이름으로 작가를 조회하는 메서드를 추가로 정의하였습니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@Repository
public interface AuthorRepository extends JpaRepository<Author, Long> {
Author findByName(String name); // 작가의 이름으로 작가를 조회하는 메서드
}
@Repository 어노테이션은 해당 인터페이스가 데이터 액세스 계층의 리포지토리 빈임을 나타낸다.
BookRepository.java
package com.test.jpa.repository;
import com.test.jpa.model.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* BookRepository 인터페이스는 책 엔티티에 대한 데이터 액세스 기능을 제공합니다.
* Spring Data JPA의 JpaRepository를 확장하여 책 엔티티에 대한 CRUD 기능을 지원합니다.
* 작가의 이름으로 책을 조회하는 쿼리 메서드를 추가로 정의하였습니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
/**
* 작가의 이름으로 책을 조회하는 쿼리 메서드입니다.
*
* @param authorName 작가의 이름
* @return 해당 작가가 작성한 책의 목록
*/
@Query("SELECT b FROM Book b WHERE b.author.name = :authorName")
List<Book> findByAuthorName(@Param("authorName") String authorName);
}
findByAuthorName 메서드는 @Query 어노테이션을 사용하여 JPQL(QueryDSL)을 이용해 작가의 이름으로 책을 조회한다.
service
AuthorListener.java
package com.test.jpa.service;
import com.test.jpa.model.Author;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
/**
* AuthorListener 클래스는 작가 엔티티의 저장 및 업데이트 이벤트를 처리합니다.
* 작가가 저장되거나 업데이트될 때, 해당 작가의 책 목록에 있는 각 책의 작가를 자신으로 설정합니다.
* JPA 엔티티 라이프사이클 이벤트를 처리하는데 사용됩니다.
*
* @author 이승원
* @since 2024.03.05.
*/
public class AuthorListener {
/**
* 작가가 저장되거나 업데이트되기 전에 실행되는 메서드입니다.
* 작가의 책 목록에 있는 각 책의 작가를 자신으로 설정합니다.
*
* @param author 저장 또는 업데이트될 작가 엔티티
*/
@PrePersist
@PreUpdate
public void beforeSaveOrUpdate(Author author) {
author.getBooks().forEach(book -> book.setAuthor(author));
}
}
@PrePersist와 @PreUpdate 어노테이션을 사용하여 JPA 엔티티 라이프사이클 이벤트를 처리하는 데 사용된다.
@PrePersist 어노테이션은 JPA 엔티티가 영속화되기 전에 호출되는 메서드를 지정한다. 이 어노테이션이 지정된 메서드는 엔티티가 데이터베이스에 삽입되기 전에 실행된다. 주로 엔티티의 생성 전에 수행해야 할 초기화 작업이나 데이터의 유효성 검사 등을 처리하는 데 사용된다.
@PreUpdate 어노테이션은 엔티티의 업데이트가 데이터베이스에 적용되기 전에 호출되는 메서드를 지정한다. 이 어노테이션이 지정된 메서드는 엔티티의 상태가 변경되고 데이터베이스에 업데이트되기 전에 실행된다. 엔티티가 업데이트되기 전에 필요한 작업을 처리하는 데 사용되며 예를 들어, 업데이트 전에 변경된 데이터의 유효성을 검사하거나 업데이트 시간을 갱신하는 등의 작업을 수행할 수 있다.
AuthorService.java
package com.test.jpa.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.test.jpa.model.Author;
import com.test.jpa.repository.AuthorRepository;
import jakarta.transaction.Transactional;
/**
* AuthorService 클래스는 작가 관련 비즈니스 로직을 처리합니다.
* 작가 엔티티의 생성과 작가 이름으로 작가를 조회하는 기능을 제공합니다.
* 작가 엔티티의 CRUD(Create, Read) 작업을 담당합니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@Service
public class AuthorService {
@Autowired
private AuthorRepository authorRepository;
/**
* 작가를 생성하는 메서드입니다.
*
* @param author 저장할 작가 엔티티
* @return 저장된 작가 엔티티
*/
@Transactional
public Author createAuthor(Author author) {
return authorRepository.save(author);
}
/**
* 작가의 이름으로 작가를 조회하는 메서드입니다.
*
* @param name 조회할 작가의 이름
* @return 해당 이름을 가진 작가 엔티티
*/
public Author getAuthorByName(String name) {
return authorRepository.findByName(name);
}
}
AuthorService 클래스는 작가 관련 비즈니스 로직을 처리하며, 작가 엔티티의 CRUD 작업을 담당한다.
@Service 어노테이션은 해당 클래스가 서비스 빈임을 나타내며, @Autowired 어노테이션을 사용하여 AuthorRepository를 주입받아 사용한다.
BookService.java
package com.test.jpa.service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.test.jpa.domain.BookDTO;
import com.test.jpa.mapper.BookMapper;
import com.test.jpa.model.Author;
import com.test.jpa.model.Book;
import com.test.jpa.repository.AuthorRepository;
import com.test.jpa.repository.BookRepository;
/**
* BookService 클래스는 책 관련 비즈니스 로직을 처리합니다.
* 책 엔티티의 생성, 조회, 수정, 삭제 기능을 제공합니다.
* 작가의 이름을 이용하여 책을 생성할 수 있습니다.
*
* @author 이승원
* @since 2024.03.05.
*/
@Service
public class BookService {
@Autowired
private BookRepository bookRepository;
@Autowired
private AuthorRepository authorRepository;
@Autowired
private BookMapper bookMapper;
@Autowired
public BookService(BookMapper bookMapper, BookRepository bookRepository, AuthorRepository authorRepository) {
this.bookMapper = bookMapper;
this.bookRepository = bookRepository;
this.authorRepository = authorRepository;
}
/**
* ID를 기반으로 책을 조회하는 메서드입니다.
*
* @param id 조회할 책의 ID
* @return 해당 ID를 가진 책 엔티티
* @throws RuntimeException 책을 찾지 못한 경우 예외 발생
*/
public Book getBookById(Long id) {
return bookRepository.findById(id).orElseThrow(() -> new RuntimeException("Book not found"));
}
/**
* 모든 책을 조회하는 메서드입니다.
*
* @return 모든 책 엔티티의 목록
*/
public List<Book> getAllBooks() {
return bookRepository.findAll();
}
/**
* 새로운 책을 생성하는 메서드입니다.
*
* @param title 책의 제목
* @param authorName 작가의 이름
* @param price 책의 가격
* @return 생성된 책 엔티티
*/
@Transactional
public Book createBook(String title, String authorName, String price) {
Author author = authorRepository.findByName(authorName);
if (author == null) {
author = new Author();
author.setName(authorName);
author = authorRepository.save(author);
}
Book book = new Book();
book.setTitle(title);
book.setAuthor(author);
book.setPrice(price);
return bookRepository.save(book);
}
/**
* 책 정보를 업데이트하는 메서드입니다.
*
* @param updatedBook 업데이트할 책 정보
* @return 업데이트된 책 엔티티
* @throws RuntimeException 책 업데이트 실패 시 예외 발생
*/
@Transactional
public Book updateBook(Book updatedBook) {
try {
Long id = updatedBook.getId();
Book existingBook = bookRepository.findById(id).orElseThrow(() -> new RuntimeException("Book not found"));
existingBook.setTitle(updatedBook.getTitle());
existingBook.setPrice(updatedBook.getPrice());
return bookRepository.save(existingBook);
} catch (RuntimeException ex) {
System.out.println("Book update failed: " + ex.getMessage());
throw new RuntimeException("Book update failed: " + ex.getMessage());
}
}
/**
* 책을 삭제하는 메서드입니다.
*
* @param id 삭제할 책의 ID
*/
@Transactional
public void deleteBook(Long id) {
Book book = bookRepository.findById(id).orElseThrow(() -> new RuntimeException("Book not found"));
bookRepository.delete(book);
}
/**
* 전체 책 개수를 반환하는 메서드입니다.
*
* @param map 책 개수 조회에 필요한 파라미터를 담은 맵
* @return 전체 책 개수
*/
public int getTotalCount() {
return bookMapper.getTotalCount();
}
/**
* 책의 아이디를 기반으로 책을 조회하는 메서드입니다.
*
* @param id 조회할 책의 아이디
* @return 조회된 책 엔티티의 Optional 객체
*/
public Optional<BookDTO> findByBookId(long id) {
return bookMapper.findByBookId(id);
}
}
BookService 클래스는 책 관련 비즈니스 로직을 처리하며, 책 엔티티의 CRUD 작업을 담당한다.
@Service 어노테이션은 해당 클래스가 서비스 빈임을 나타내며, @Autowired 어노테이션을 사용하여 BookRepository와 AuthorRepository를 주입받아 사용한다.
database
ddl.sql
-- ddl
SELECT * FROM tabs ORDER BY table_name;
-- system
-- create user starfield_library identified by suwon1234;
-- grant connect, resource, dba to starfield_library;
-- Drop Book table
DROP TABLE Book;
-- Drop Author table
DROP TABLE Author;
-- Drop Sequences
DROP SEQUENCE author_seq;
DROP SEQUENCE book_seq;
-- Create Author table
CREATE TABLE Author (
id NUMBER PRIMARY KEY,
name VARCHAR2(100)
);
-- Create Book table with foreign key constraint
CREATE TABLE Book (
id NUMBER PRIMARY KEY,
title VARCHAR2(100),
price VARCHAR2(100),
author_id NUMBER,
CONSTRAINT fk_author FOREIGN KEY (author_id) REFERENCES Author(id)
);
-- Sequence for auto-incrementing IDs
CREATE SEQUENCE author_seq START WITH 1 INCREMENT BY 1;
CREATE SEQUENCE book_seq START WITH 1 INCREMENT BY 1;
ddl에서 테이블과 시퀀스를 생성하였다. 시퀀스를 생성할 때 1부터 시작하여 1씩 증가하도록 설정해 주어야 한다. JPA에서 시퀀스를 사용하여 ID를 자동으로 생성할 때 이를 설정하지 않으면 그 이상으로 증가할 수 있기 때문이다.
dml.sql
-- dml
-- Insert sample data into Author table
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '랑가 라오 카라남');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '오카다 다쿠미');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '박우창');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '채규태');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '홍팍');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '고재성');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '윤인성');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '한정수');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '김담형');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '송형주');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '박상헌');
INSERT INTO Author (id, name) VALUES (author_seq.NEXTVAL, '박규석');
SELECT * FROM Author;
-- Insert sample data into Book table
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '스프링 5 마스터', 40000, 1);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '모던 자바스크립트로 배우는 리액트 입문', 23000, 2);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '오라클로 배우는 데이터베이스 개론과 실습', 29000, 3);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '채쌤의 자바 프로그래밍 핵심', 27000, 4);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '채쌤의 Servlet&JSP 프로그래밍 핵심', 27000, 4);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '코딩 자율학습 스프링 부트 3 자바 백엔드 개발 입문', 33000, 5);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '자바를 부탁해', 26000, 5);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, 'IT 엔지니어를 위한 네트워크 입문', 36000, 6);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, 'HTML5 웹 프로그래밍 입문', 26000, 7);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '객체 지향 설계와 분석을 위한 UML 기초와 응용', 24000, 8);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '서비스 운영이 쉬워지는 AWS 인프라 구축 가이드', 27000, 9);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '인사이드 자바스크립트', 18000, 10);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '기초부터 다지는 ElasticSearch 운영 노하우', 34000, 11);
INSERT INTO Book (id, title, price, author_id) VALUES (book_seq.NEXTVAL, '운영체제', 24000, 12);
SELECT * FROM Book;
COMMIT;
dml에서 저자와 책의 INSERT문을 작성하였다.
한 명의 저자가 여러 책을 작성할 수 있으며, 저자 중 "채규태"와 "홍팍"은 여러 책을 가진다.
static
style.css
/* style.css */
@font-face {
font-family: 'Pretendard-Regular';
src: url('https://cdn.jsdelivr.net/gh/Project-Noonnu/noonfonts_2107@1.1/Pretendard-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
}
body {
font-family: 'Pretendard-Regular', sans-serif;
background-color: #f9f9f9;
margin: 0;
padding-bottom: 90px;
}
.container {
max-width: 800px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
text-align: center;
margin-bottom: 20px;
}
form {
display: flex;
flex-direction: column;
}
input, button {
margin: 5px 0;
padding: 10px;
font-size: 16px;
border: 1px solid #ccc;
border-radius: 5px;
}
button {
cursor: pointer;
background-color: #007bff;
color: #fff;
border: none;
}
button:hover {
background-color: #0056b3;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}
th {
background-color: #f2f2f2;
}
tr:nth-child(even) {
background-color: #f2f2f2;
}
.footer {
text-align: center;
margin-top: 20px;
font-size: 14px;
color: #666;
}
script.js
1. DOM을 사용하여 구현
/* script.js */
/* 도서 관리 폼 */
const bookForm = document.getElementById('bookForm');
/* 도서 테이블의 목록을 보여주는 테이블 */
const bookTable = document.getElementById('bookTable').getElementsByTagName('tbody')[0];
/* 데이터를 렌더링하는 함수 */
function renderBooks(books) {
// 테이블 내용 초기화
bookTable.innerHTML = '';
// 각 도서를 반복하며 테이블에 추가
books.forEach(book => {
const row = `<tr>
<td>${book.id}</td>
<td>${book.title}</td>
<td>${book.author.name}</td>
<td>${formatPrice(book.price)}</td>
<td>
<button onclick="editBook(${book.id})">편집</button>
<button onclick="deleteBook(${book.id})">삭제</button>
</td>
</tr>`;
// 행을 테이블에 추가
bookTable.innerHTML += row;
});
}
/* 폼 제출 이벤트 리스너 */
bookForm.addEventListener('submit', function(event) {
event.preventDefault(); // 폼의 기본 동작 방지
let authorId;
// 책 데이터 수집
const bookData = {
id: document.getElementById('bookId').value,
title: document.getElementById('title').value,
author: { id: authorId, name: document.getElementById('author').value }, // 작가 정보
price: document.getElementById('price').value
};
// 책 저장
saveBook(bookData);
// HTML 요소 초기화
document.getElementById('bookId').value = '';
document.getElementById('title').value = '';
document.getElementById('author').value = '';
document.getElementById('price').value = '';
this.reset(); // 폼 초기화
});
/* 책 저장하는 함수, 책 수정하는 함수 */
function saveBook(book) {
let url = '/starfield/books';
let method = 'POST';
// 기존의 책을 수정하는 경우
if (book.id) {
url += `/${book.id}`;
method = 'PUT';
}
// console.log(JSON.stringify(book));
// console.log(url);
// console.log(method);
// AJAX를 통해 책 저장 요청 전송
$.ajax({
type: method,
url: url,
contentType: 'application/json',
data: JSON.stringify(book),
success: function(data) {
// 책 저장 성공 시 목록 다시 불러오기
getAllBooks();
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
}
/* 책 수정할 때 해당 책 데이터 불러오는 함수 */
function editBook(id) {
// AJAX를 통해 책 정보 요청
$.ajax({
type: 'GET',
url: `/starfield/books/${id}`,
success: function(data) {
// 폼에 책 데이터 채우기
document.getElementById('bookId').value = data.id;
document.getElementById('title').value = data.title;
document.getElementById('author').value = data.author.name;
document.getElementById('price').value = data.price;
},
error: function(xhr, status, error) {
console.error('Error:', error);
}
});
}
/* 책 삭제하는 함수 */
function deleteBook(id) {
// 사용자에게 확인을 받기 위한 경고창 띄우기
if (confirm("정말로 삭제하시겠습니까?")) {
// 사용자가 확인을 클릭한 경우 책 삭제 요청 보내기
fetch(`/starfield/books/${id}`, {
method: 'DELETE'
})
.then(response => {
if (response.status === 204) {
// 삭제 성공 시 목록 다시 불러오기
getAllBooks();
}
})
.catch(error => console.error('Error:', error));
}
}
/* 페이지 로드 시 책 목록 가져오기 */
function getAllBooks() {
// 책 목록 요청
fetch('/starfield/books/all')
.then(response => response.json())
.then(data => {
// 책 목록 렌더링
renderBooks(data);
})
.catch(error => console.error('Error:', error));
}
/* 가격에 쉼표 추가 */
function formatPrice(price) {
return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
/* 페이지 로드 시 책 목록 초기화 */
window.onload = getAllBooks;
2. jQuery + 화살표 함수를 사용하여 코드 개선
/* script.js */
$(document).ready(() => {
const bookForm = $('#bookForm'); // 도서 관리 폼
const bookTable = $('#bookTable').find('tbody'); // 도서 목록 테이블의 tbody 요소
// 도서 목록을 렌더링하는 함수
const renderBooks = (books) => {
// 테이블 내용 초기화
bookTable.empty();
// 각 도서를 반복하며 테이블에 추가
books.forEach(book => {
const row = `<tr>
<td>${book.id}</td>
<td>${book.title}</td>
<td>${book.author.name}</td>
<td>${formatPrice(book.price)}</td>
<td>
<button class="edit-btn" data-id="${book.id}">편집</button>
<button class="delete-btn" data-id="${book.id}">삭제</button>
</td>
</tr>`;
bookTable.append(row);
});
// 각 행의 편집 및 삭제 버튼에 이벤트 리스너 추가
$('.edit-btn').click((event) => {
const id = $(event.currentTarget).data('id');
editBook(id);
});
$('.delete-btn').click((event) => {
const id = $(event.currentTarget).data('id');
deleteBook(id);
});
};
// 폼 제출 이벤트 리스너
bookForm.submit((event) => {
event.preventDefault(); // 폼의 기본 동작 방지
// 책 데이터 수집
const bookData = {
id: $('#bookId').val(),
title: $('#title').val(),
author: { id: null, name: $('#author').val() }, // 작가 정보
price: $('#price').val()
};
saveBook(bookData); // 책 저장
$('#bookId, #title, #author, #price').val(''); // HTML 요소 초기화
bookForm.trigger('reset'); // 폼 초기화
});
// 책 저장하는 함수, 책 수정하는 함수
const saveBook = (book) => {
let url = '/starfield/books';
let method = 'POST';
// 기존의 책을 수정하는 경우
if (book.id) {
url += `/${book.id}`;
method = 'PUT';
}
// AJAX를 통해 책 저장 요청 전송
$.ajax({
type: method,
url: url,
contentType: 'application/json',
data: JSON.stringify(book),
success: () => {
getAllBooks(); // 책 저장 성공 시 목록 다시 불러오기
},
error: (xhr, status, error) => {
console.error('Error saving book:', error);
alert('책 저장 중 오류가 발생했습니다.'); // 오류 메시지 표시
}
});
};
// 책 수정할 때 해당 책 데이터 불러오는 함수
const editBook = (id) => {
// AJAX를 통해 책 정보 요청
$.ajax({
type: 'GET',
url: `/starfield/books/${id}`,
success: (data) => {
// 폼에 책 데이터 채우기
$('#bookId, #title, #author, #price').val('');
$('#bookId').val(data.id);
$('#title').val(data.title);
$('#author').val(data.author.name);
$('#price').val(data.price);
},
error: (xhr, status, error) => {
console.error('Error fetching book data:', error);
alert('책 정보를 가져오는 중 오류가 발생했습니다.'); // 오류 메시지 표시
}
});
};
// 책 삭제하는 함수
const deleteBook = (id) => {
if (confirm("정말로 삭제하시겠습니까?")) {
fetch(`/starfield/books/${id}`, {
method: 'DELETE'
})
.then(response => {
if (response.status === 204) {
getAllBooks(); // 삭제 성공 시 목록 다시 불러오기
}
})
.catch(error => {
console.error('Error deleting book:', error);
alert('책 삭제 중 오류가 발생했습니다.'); // 오류 메시지 표시
});
}
};
// 페이지 로드 시 책 목록 가져오기
const getAllBooks = () => {
fetch('/starfield/books/all')
.then(response => response.json())
.then(renderBooks) // 책 목록 렌더링
.catch(error => {
console.error('Error fetching book list:', error);
alert('책 목록을 가져오는 중 오류가 발생했습니다.'); // 오류 메시지 표시
});
};
// 가격에 쉼표 추가하는 함수
const formatPrice = (price) => {
return price.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
};
getAllBooks(); // 페이지 로드 시 책 목록 초기화
});
script.js 파일을 DOM을 사용했을 때와 jQeury를 사용했을 때로 구분하여 작성해 보았다.
DOM과 jQuery
DOM(Document Object Model)은 HTML, XML 및 XHTML 문서의 프로그래밍적인 인터페이스로, 트리 구조로 이루어져 있다. 각 요소는 노드로 표현되며 이 노드에는 HTML 요소, 속성, 텍스트 등이 포함되어 있어 문서의 내용, 구조, 스타일을 동적으로 변경할 수 있다.
jQuery는 JavaScript 라이브러리로, HTML 문서를 보다 쉽게 조작하고 이벤트를 처리할 수 있도록 도와준다. jQuery는 간단하고 직관적인 API를 제공하여 DOM 조작 및 이벤트 처리를 간소화한다. CSS 선택자를 사용하여 요소를 선택하고, 이벤트 리스너를 추가하고, 요소를 동적으로 생성하거나 변경하는 등의 작업을 쉽게 할 수 있다. 단, jQuery를 사용하면 추가적인 라이브러리를 다운로드해야 하고, 이로 인해 사이트의 성능이 저하될 수 있다.
2023.10.10 - [Programming/JavaScript] - [JavaScript] DOM (Document Object Model)
2023.10.13 - [Programming/jQuery] - [jQuery] jQuery의 시작: JavaScript와 jQuery 연결
DOM과 jQuery에 대해서는 위 글을 참고한다.
addEventListener
addEventListener는 JavaScript에서 이벤트를 처리하는 메서드이다. 이 메서드를 사용하여 HTML 요소에 이벤트 리스너를 추가할 수 있다. 이벤트 리스너는 특정 이벤트(예: 클릭, 마우스 오버, 키보드 입력 등)가 발생했을 때 실행할 함수를 지정한다.
element.addEventListener(event, function, useCapture);
- element: 이벤트를 추가할 HTML 요소를 지정한다.
- event: 이벤트의 종류를 지정한다. 예를 들어, 'click', 'mouseover', 'keydown' 등이 있다.
- function: 이벤트가 발생했을 때 실행할 함수를 지정한다.
- useCapture (선택적): 이벤트 캡처링을 사용할지 여부를 지정한다. 기본값은 false이며, 일반적으로 이 값은 건들 필요가 없다.
// 버튼 요소를 가져와서 이벤트 리스너 추가
var button = document.getElementById('myButton');
button.addEventListener('click', function() {
alert('Button clicked!');
});
위의 예시 코드에서는 'Click me'라는 텍스트가 표시된 버튼을 클릭했을 때, 'Button clicked!'라는 경고창이 표시된다. 이것은 addEventListener 메서드를 사용하여 버튼에 클릭 이벤트 리스너를 추가한 결과이다.
화살표 함수
화살표 함수는 ES6(ECMAScript 2015)에서 도입된 새로운 함수 정의 방식이다. 화살표 함수를 사용하면 함수를 더 간결하게 정의할 수 있고, 함수 내부의 this 컨텍스트가 외부의 컨텍스트를 상속한다는 특징이 있다.
function greet(name) {
return 'Hello, ' + name + '!';
}
예를 들어, 기존의 함수 정의 방식으로 다음과 같은 함수를 정의할 수 있다.
const greet = (name) => 'Hello, ' + name + '!';
실행 내용이 한 줄인 경우 중괄호와 return 키워드를 생략할 수 있다.
화살표 함수의 또 다른 특징은 this 컨텍스트가 외부의 컨텍스트를 상속한다는 것이다. 이는 함수가 호출된 위치가 아닌 정의된 위치의 this 값을 참조한다는 의미이다. 따라서 화살표 함수 내부에서 this를 사용하면 외부의 this 값을 사용한다. 이 기능은 주로 객체의 메서드를 정의할 때 유용하게 활용된다.
templates
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <!-- 뷰포트 설정 -->
<title>수원 스타필드 별마당 도서관 도서 관리 시스템</title> <!-- 페이지 제목 -->
<link rel="stylesheet" href="/starfield/assets/css/style.css"> <!-- 스타일 시트 링크 -->
<style>
/* 도서 테이블의 열 너비 */
#bookTable th:nth-child(1) { width: 10%; }
#bookTable th:nth-child(2) { width: 30%; }
#bookTable th:nth-child(3) { width: 20%; }
#bookTable th:nth-child(4) { width: 10%; }
#bookTable th:nth-child(5) { width: 20%; }
/* 비치중인 책 권수 */
#totalBooks {
font-size: 18px;
text-align: left;
}
</style>
</head>
<body>
<div class="container">
<h2>수원 스타필드 별마당 도서관 도서 관리 시스템</h2>
<!-- 도서관에 비치된 책 권수 출력 -->
<p id="totalBooks">도서관에 비치된 책 <span th:text="${totalCount}"></span>권</p>
<!-- 책 추가 폼 -->
<form id="bookForm">
<input type="hidden" id="bookId"> <input type="text"
id="title" placeholder="도서명" required> <input type="text"
id="author" placeholder="저자" required> <input type="text"
id="price" placeholder="가격" required>
<button type="submit">저장</button>
</form>
<!-- 도서 테이블 -->
<table id="bookTable">
<thead>
<tr>
<th>ID</th>
<th>도서명</th>
<th>저자</th>
<th>가격</th>
<th>작업</th>
</tr>
</thead>
<!-- 도서 목록 -->
<tbody></tbody>
</table>
</div>
<!-- Footer -->
<div class="footer">© 2024 수원 스타필드 별마당 도서관. All rights reserved.</div>
<!-- jQuery 라이브러리 -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<!-- 스크립트 파일 -->
<script src="/starfield/assets/js/script.js"></script>
</body>
</html>
이외에도 테스트 코드를 추가하거나 리팩토링을 통해 코드를 개선할 수 있다.