MyBatis와 JPA를 동시 사용해서 수원 스타필드 별마당 도서관 도서 관리 시스템 CRUD(Create, Read, Update, Delete)를 구현해 보도록 하자. 이렇게 만든 웹 애플리케이션의 배포까지 진행하면서 Spring MVC Pattern을 학습하는 것을 목표로 한다.
이 연습 프로젝트는 실제 스타필드 도서관 웹 애플리케이션은 아니지만, JPA를 활용하여 데이터베이스에서 책과 저자에 대한 CRUD 기능을 구현하였다.
💡JPA
JPA(Java Persistence API)는 자바의 ORM(Object-Relational Mapping) 기술을 쉽게 구현하도록 도와주는 API이다. ORM은 객체와 관계형 데이터베이스 간의 매핑을 단순화하여 개발자가 객체 지향 프로그래밍 언어로 데이터베이스를 다룰 수 있도록 한다.
데이터 액세스 계층
JPA는 객체와 테이블 간의 매핑을 설정하고, 객체의 상태를 데이터베이스에 자동으로 동기화하는 기능을 제공한다. 또한, Spring 프레임워크와 함께 사용되는 경우 Spring Data JPA를 통해 쉽게 데이터 액세스 계층을 구현할 수 있다.
JPA는 데이터베이스와의 상호 작용을 추상화하고 표준화하는 자바 플랫폼 표준이며, 이를 구현한 여러 프레임워크 중 하나가 Hibernate이다.
JPA에 대해서는 위 글을 참고한다.
💡MyBatis
MyBatis는 SQL과 저장 프로시저를 자바에서 실행하기 위한 오픈 소스 프레임워크이다. MyBatis는 JDBC 코드의 반복적인 작업을 줄여주고, SQL 쿼리를 자바 코드에서 분리하여 관리할 수 있도록 도와준다.
MyBatis는 Hibernate와는 다르게 SQL을 직접 다룬다. SQL 쿼리와 자바 객체 사이의 매핑을 XML 파일이나 어노테이션을 사용하여 정의하며, 이를 통해 개발자는 복잡한 JDBC 코드 대신 SQL 쿼리를 직접 작성하여 데이터베이스와 상호작용할 수 있다.
https://mybatis.org/mybatis-3/
2023.11.23 - [Programming/Spring] - [Spring] MyBatis: Connection Pool (Commons DBCP, HikariCP)
2023.11.24 - [Programming/Spring] - [Spring] MyBatis: 반환값과 매개변수에 따라 달라지는 쿼리
2023.12.01 - [Programming/Spring] - [Spring] MyBatis: Interface Mapper
MyBatis에 대해서는 위 글을 참고한다.
💡MyBatis와 JPA 동시 사용하기
왜 MyBatis와 JPA를 함께 사용할까?
JPA를 이용하면 간단한 CRUD 쿼리를 직관적으로 만들 수 있지만, 복잡한 쿼리를 나타내는 것에는 한계가 있다. 특히 아래와 같이 복합 조건을 가진 쿼리를 표현하기 위해서는 긴 메서드를 정의해야 한다.
List<Notice> findAllByCreateTimestampAndStatusOrCreateTimestampGreaterThanEqualAndStatus(
Timestamp createTimestamp1, String status1, Timestamp createTimestamp2, String status2);
이러한 경우에도 SQL 함수를 사용해야 하는 경우가 있다. 예를 들어, CURDATE()와 같은 SQL 함수를 사용하기 위해서는 @Query를 사용하여 직접 쿼리를 작성해야 한다.
@Query("SELECT e FROM #{#entityName} e WHERE e.createTimestamp = :createTimestamp1 AND e.status = :status1 AND DATE(e.openTimestamp) < CURDATE() " +
"OR e.createTimestamp >= :createTimestamp2 AND e.status = :status2 AND DATE(e.openTimestamp) < CURDATE()")
List<Notice> findAllByCreateTimestampAndStatusOrCreateTimestampGreaterThanEqualAndStatus(
@Param("createTimestamp1") Timestamp createTimestamp1, @Param("status1") String status1,
@Param("createTimestamp2") Timestamp createTimestamp2, @Param("status2") String status2);
복잡한 쿼리의 경우에는 JPA보다는 MyBatis을 고려해 보아야 한다. MyBatis를 사용하면 SQL을 직접 작성하므로 복잡한 쿼리를 표현을 하기에 적합하다.
따라서 일반적인 경우에는 JPA를 사용하고, 복잡한 쿼리를 조회해야 할 때에는 MyBatis를 사용하는 것을 권장한다. 이렇게 함으로써 데이터베이스 액세스에 필요한 유연성과 성능을 모두 확보할 수 있다.
프로젝트 생성
https://start.spring.io/starter.zip?name=jpa&groupId=com.test&artifactId=jpa&version=0.0.1-SNAPSHOT&description=Spring+Web+Application+for+Spring+Boot&packageName=com.test.jpa&type=maven-project&packaging=jar&javaVersion=17&language=java&bootVersion=3.2.3
Spring Boot는 프로젝트를 생성할 때 dependency(의존 라이브러리)를 간편하게 Search & Click 해서 불러올 수 있다는 장점이 있다. 또한, 이때 사용한 라이브러리는 그대로 Used List에 남기 때문에 다른 프로젝트를 생성할 때 그대로 적용할 수 있다.
하지만 이번에는 pom.xml에 직접 dependency를 등록하는 방법으로 프로젝트를 빌드해보려고 한다.
pom.xml
현재 의존성을 추가하지 않은 상태 그대로 가져왔다.
아래 사이트에서 사용할 의존 라이브러리를 찾아 pom.xml에서 의존성을 추가해 주도록 하자.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.3</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.test</groupId>
<artifactId>jpa</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>jpa</name>
<description>Spring Web Application for Spring Boot</description>
<properties>
<java.version>17</java.version>
<hibernate.version>6.2.21.Final</hibernate.version>
</properties>
<dependencies>
<!-- Spring Boot 기본 스타터 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot 테스트 스타터 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Thymeleaf 스타터 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Spring 웹 모듈 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- Spring Boot 웹 스타터 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot 톰캣 스타터 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<!-- HikariCP 커넥션 풀 사용을 위한 Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- JPA와 Hibernate를 위한 Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MyBatis 관련 라이브러리 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<!-- MyBatis 관련 라이브러리 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.15</version>
</dependency>
<!-- 스프링 부트 개발자 도구모음 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JSON 데이터를 처리하기 위한 자바 라이브러리 -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
</dependency>
<!-- class 파일로 configuration 하기 위한 라이브러리 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Oracle JDBC -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
위와 같이 pom.xml 의존성을 추가해 주었다.
Closing JPA EntityManagerFactory for persistence unit 'default'
<dependency> <!-- Oracle JDBC-->
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<scope>provided</scope>
<!--<scope>runtime</scope>-->
</dependency>
스프링부트 서버가 실행이 안되고 자꾸 멈추는 오류가 발생했다. 프로젝트가 실행되지 않고 계속해서 중단된다면 의존성 충돌을 고려해 보아야 한다.
이는 필요한 web 의존 라이브러리를 추가하지 않은 것 외에도 ojdbc scope가 잘못 설정되어 있어서 발생하는 오류일 수 있다.
스택오버플로우에서는 의존성 충돌로 인한 오류로 보고 있다.
나는 scope를 runtime에서 provided로 변경하고 몇 가지 의존성을 추가하여 문제를 해결했다.
이외에도 종속성이 중복되거나 버전이 호환되지 않으면 오류가 발생할 수 있다.
mvnrepository
mvnrepository는 Maven 전용 dependency를 모아놓은 사이트이다.
가장 최근에 나온 버전은 다른 라이브러리와의 호환성과 안정성에 있어 문제가 발생할 수 있기 때문에 버전의 다운로드 횟수를 보고 선택하는 것을 권장한다. 물론 Spring Boot에서는 가장 호환이 잘 되는 버전으로 가져와서 프로젝트를 빌드해 주므로 크게 신경 쓰지 않아도 된다.
버전을 클릭해서 들어가면 해당 버전의 라이브러리 선언문이 나온다. 이를 pom.xml로 가져오면 된다.
HikariCP
https://github.com/brettwooldridge/HikariCP#configuration-knobs-baby
HikariCP는 JDBC(Java Database Connectivity) 연결 풀링을 관리하는 데 사용되는 JDBC 커넥션 풀 라이브러리이다.
커넥션 풀이란 웹 컨테이너(WAS)가 실행되면서 JDBC 실행 과정 중에서 생성되어야 할 Connection 객체(DB를 사용하기 위해 DB와 애플리케이션 간 통신을 할 수 있는 수단)를 미리 만들어서 pool 이란 곳에서 저장을 해두는 기법을 의미한다.
2024.03.07 - [Programming/Spring] - [Spring Boot] 스타필드 도서관 웹 애플리케이션: 프로젝트 구조 및 전체 코드
프로젝트 구조와 전체 코드는 분량으로 인해 다음 글에 첨부하였다.
💡프로젝트 배포
2024.02.10 - [Programming/AWS] - [AWS] Spring Boot 웹 애플리케이션 AWS와 Mobaxterm으로 서버에 배포하기
AWS와 MobaXterm으로 가상 컴퓨터 환경에 웹 애플리케이션을 배포하였다.
조회 (Read)
배포된 웹 페이지에 들어가면 자동으로 DB에 저장된 책 데이터를 조회한다.
도서관에 비치된 책 권 수 조회
CRUD는 JPA로 구현하였고, MyBatis Mapper로 조회한 책의 총 권 수를 가져와 표시하였다.
생성 (Create)
도서명, 저자, 가격을 입력하여 도서관에 책을 등록할 수 있다.
이미 등록된 저자의 이름을 입력할 경우 해당 Author와 연결되며, 등록되지 않은 저자를 입력할 경우 새로운 Author 데이터가 생성된다.
Spring Data JPA 기능으로 Book과 Author가 생성될 때 ID가 자동으로 생성된다.
수정 (Update)
수정하려고 하는 책의 편집 버튼을 클릭하면 해당 데이터의 도서명, 저자, 가격이 입력 폼에 자동으로 입력된다.
내용을 수정하고 저장 버튼을 클릭하면 수정된 내용으로 데이터가 수정된다.
도서명을 "오라클로 배우는 데이터베이스 개론과 실습"에서 "오라클로 배우는 데이터베이스 개론과 실습 제2판"으로 수정하고, 저자를 "박우창"에서 "남송휘", 가격을 "29000"에서 "28000"으로 수정하였다.
엔티티 관계 문제 (개선점)
ID가 4번인 책의 저자 "채규태"를 "채쌤"으로 변경하였다. 그런데 ID가 5번인 책의 저자 또한 "채쌤"으로 변경되었다. 영속성 컨텍스트의 변경 감지(Dirty Checking)로 인해 하나의 데이터를 수정해도 같이 수정이 되는 것으로 보인다.
변경 감지
변경 감지는 엔티티의 상태를 지속적으로 모니터링하여 변경 사항을 감지하고, 해당 변경 사항을 데이터베이스에 자동으로 반영하는 특성이다. 따라서 하나의 엔티티를 수정하면, 해당 엔티티의 상태가 변경되어 변경 사항이 감지되고, 이 변경 사항이 데이터베이스에 저장된다.
데이터베이스에서 새로운 엔티티를 저장하거나 이미 있는 엔티티를 병합할 때도 변경 감지가 작동한다. 새로운 엔티티를 저장할 때는 새로운 레코드가 데이터베이스에 삽입되고, 이미 존재하는 엔티티를 저장할 때는 해당 엔티티의 상태가 변경되어 데이터베이스에 업데이트된다.
만약 책A와 책B가 모두 같은 저자를 참조하고 있고, 이 저자의 정보를 수정한다면, 변경 감지는 이를 감지하여 해당 저자에 대한 변경 사항을 책A와 책B에 모두 반영되는 것이다.
삭제 (Delete)
삭제 버튼을 클릭하면 confirm으로 정말로 삭제할 건지 물어본다.
"운영체제" 책을 삭제하자 DB에서 삭제되고 목록이 새롭게 조회되었다.
https://github.com/Isaac-Seungwon/suwon-starfield-library.git
위 GitHub Link에서 소스 코드와 폴더 구조를 확인할 수 있다.
참고 자료
[JPA] MyBatis와 동시 사용 (DTO/엔티티 분리), J4J, 2021.03.30.
[JPA] 영속성 컨텍스트란? 그리고 영속성 관리, 키크니개발자, 2022.06.11.
[Spring Boot] JPA와 MyBatis 동시에 사용하기, jiniworld, 2019.12.27.
[SpringBoot] JPA & Mybatis를 혼용한 SpringBoot 개발 환경 구축, 김타루, 2020.01.29.