객체 관계형 매핑의 개념을 설명하기 전에 앞서 다음과 같은 자바 클래스가 있습니다.
public class User {
private Integer id;
private String firstName;
private String lastName;
}
Database에는 USERS라는 table이 있습니다.
id | first_name | last_name |
1 | foo1 | bar1 |
2 | foo2 | bar2 |
3 | foo3 | bar3 |
자바 클래스와 해당 테이블을 매핑하기 위해서는 자바에 많은 방법들이 있습니다.
1. JDBC
2. JOOQ 또는 Mybatis와 같은 Spring의 JDBC 추상화 SQL 프레임워크
3. Hibernate 또는 다른 JPA와 같은 ORM
많은 방법들이 있지만 가장 기본이 되는 JDBC를 이해하는 것이 가장 중요합니다. Hibernate 나 다른 모든 라이브러리는 모두 내부적으로 JDBC를 사용합니다.
JDBC란 무엇인가
자바에서 데이터베이스에 액세스하는 low-level입니다. 모든 프레임워크에서 내부적으로 JDBC를 사용합니다. 물론 SQL 쿼리를 직접 사용할 수 있습니다.
JDBC의 좋은점은 모든 JDK/JRE와 함께 제공되므로 타사 라이브러리가 필요하지 않습니다. 특정 데이터베이스에 적합한 JDBC 드라이버만 필요합니다.
JDBC에서 JAVA 사용하기
위의 Users 테이블을 포함하는 데이터베이스가 있습니다. 해당 테이블에서 모든 사용자를 선택하고 이를 Java 개체 목록인 List<User>로 변환하는 쿼리를 작성하려고 합니다.
package db;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class JdbcQueries {
public static void main(String[] args) throws SQLException {
final String DB_URL = "jdbc:mysql://localhost:3306/001?useSSL=false&allowPublicKeyRetrieval=true";
final String DB_USERNAME = "root";
final String DB_PASSWORD = "1234";
try(Connection conn = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD)) {
PreparedStatement st = conn.prepareStatement("select * from users");
ResultSet rs = st.executeQuery();
List<User> users = new ArrayList<>();
while (rs.next()) {
Integer id = rs.getInt("id");
String firstName = rs.getString("first_name");
String lastName = rs.getString("last_name");
User user = new User(id, firstName, lastName);
users.add(user);
}
}
}
}
Connection 부분에서 mysql 데이터베이스로 연결하는데 try-with-resources를 사용하여 작업이 완료되면 자동으로 해당 리소스가 닫힙니다.
PreparedStatement를 생성하고 실행하여 SQL 문을 생성하고 실행합니다.
SQL 쿼리가 반환하는 모든 행을 ResultSet에 담아서 수동으로 열 이름과 타입을 지정하여 탐색합니다.
JDBC 요약
위의 예제 코드에서 JDBC가 왜 low-level인지 보여줍니다. SQL에서 JAVA로 전환하려면 수동으로 해야할 작업이 많기 때문입니다. 그리고 데이터베이스를 스스로 연결하고 닫아야합니다. 이런 것들로 인해 보다 편리하게 사용할 수 있게 프레임워크가 필요합니다.
Java ORM 프레임워크: Hibernate, JPA
Java 개발자는 일반적으로 SQL문을 작성하는 것보다 Java 클래스를 작성하는 데 더 익숙합니다. 따라서 많은 프로젝트가 Java 우선 접근 방식으로 작성됩니다. 즉 해당 데이터베이스 테이블을 생성하기 전에 Java 클래스를 생성해야 한다는 것입니다. 이는 자연스럽게 객체 관계형 매핑으로 질문으로 이어집니다.
- Java 클래스를 새로 작성하면 아직 생성되지 않은 테이블에 어떻게 매핑되는지?
- Java 클래스로 데이터베이스를 생성할 수 있는지?
Hibernate나 다른 JPA가 본격적인 ORM이 작동을 합니다.
Hibernate란?
Hibernate는 2001년에 처음 출시된 ORM 라이브러리입니다. Hibernate가 특징은 다음과 같습니다.
- 초기 매핑과 별도로 많은 작업을 수행하지 않고도 데이터베이스 테이블과 java 클래스 간에 쉽게 변환할 수 있습니다.
- CRUD 작업에 대해 SQL 코드를 작성할 필요가 없습니다.
- SQL 위에 몇가지 쿼리 메커니즘(HQL, Criteria API)를 제공하여 데이터베이스를 쿼리합니다.
HQL(Hibernate Query Language) 특징
HQL은 자바 객체를 대상으로 쿼리를 작성합니다. 엔티티 객체와 그 객체의 속성을 사용하여 데이터를 조회하고 조작할 수 있습니다. 다양한 DB에 대해 Hibernate가 알아서 적절한 SQL로 변환됩니다.
Criteria API란
기존 텍스트로 구성된 JPQL을 빌더 클래스를 사용하여 타입에 안전한 쿼리를 생성합니다.
create table users (
id integer not null,
first_name varchar(255),
last_name varchar(255),
primary key(id)
)
public class User {
private Integer id;
private String firstName;
private String lastName;
// getter & setter 생략
}
여기서 User 클래스와 users 데이터베이스 테이블을 어떻게 매핑되어야 한다고 Hibernate에게 알려주는지 알아보겠습니다.
Hibernate 매핑 어노테이션 사용하는 방법
기본적으로 Hibernate는 어떤 클래스가 데이터베이스 테이블에 어떻게 매핑되어야 하는지 알 수 있는 방법이 없습니다.
예전에는 Hibernate가 무엇을 해야 하는지 알려주기 위해 .xml 파일을 작성했습니다.
어노테이션을 사용하여 User 클래스가 어떻게 매핑되는지 보겠습니다.
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(name="first_name")
private String firstName;
@Column(name="last_name")
private String lastName;
}
- @Entity: 이 클래스를 매핑하여 데이터베이스 테이블을 만듭니다.
- @Table: 클래스를 매핑할 데이터베이스 테이블을 Hibernate에게 알려줍니다.
- @Column: 필드를 매핑할 데이터베이스 열을 Hibernate에게 알려줍니다.
- @Id 와 @GeneratedValue : Hibernate에게 테이블의 기본 키가 무엇인지, 그리고 그것이 데이터베이스에 의해 자동 생성되었는지 알려줍니다.
Bootstrapping API
부트스트래핑은 SessionFactory를 구축하고 초기화하는 프로세스를 나타냅니다. 부트스트래핑을 하기 위해서는 Hibernate에 필요한 서비스인 ServiceRegistry가 필요합니다. 이 레지스트리에서 애플리케이션의 도메인 모델과 데이터베이스에 대한 매핑을 나타내는 메타데이터 개체를 구축할 수 있습니다.
Service
ServiceRegistry를 알기 전에 Service가 무엇인지 알아야됩니다. Hibernate에서 Service란 가장 일반적인 서비스에 대한 구현을 제공하며 대부분의 경우에 충분합니다. 그렇지 않으면 기능을 수정하거나 추가해서 자체 서비스를 구축할 수 있습니다.
ServiceRegistry
SessionFactory를 구축하는 첫 번째 단계는 ServiceRegistry를 생성하는 것입니다. 이를 통해 Hibernate에 필요한 기능을 제공하고 Java SPI 기능을 기반으로 하는 다양한 서비스를 보유할 수 있습니다 .ServiceRegistry는 Bean이 Service 유형만 있는 경량 종속성 주입 도구입니다. ServiceRegistry에는 2가지 타입이 있으며 계층적입니다.
1번째는 부모가 없고 다음 세가지 필수 서비스를 보유하는 BootstrapServiceRegistry입니다.
- ClassLoaderService: Hibernate가 다양한 런타임 환경의 ClassLoader와 상호 작용할 수 있도록 합니다.
- IntegratorService: 타사 애플리케이션이 Hibernate와 통합될 수 있도록 Integrator 서비스의 검색 및 관리를 제어합니다.
- StrategySelector: 다양한 전략 계약의 구현을 해결합니다.
BootstrapServiceRegistry 구현을 빌드하기 위해 BootstrapServiceRegistryBuilder 팩토리 클래스를 사용합니다. 이를 통해 유형이 안전한 세 가지 서비스를 사용자 정의할 수 있습니다.
BootstrapServiceRegistry bootstrapServiceRegistry = new BootstrapServiceRegistryBuilder()
.applyClassLoader()
.applyIntegrator()
.applyStrategySelector()
.build();
2번째는 ServiceRegistry는 이전 BootstrapServiceRegistry를 기반으로 구축되고 위에 언급된 세 가지 서비스를 보유하는 StandardServiceRegistry입니다. 추가적으로 StandardServiceInitiators 클래스에 나열된 Hibernate에 필요한 다양한 서비스를 포함합니다.
StandardServiceRegistryBuilder standardServiceRegistry = new StandardServiceRegistryBuilder();
내부적으로 StandardServiceRegistryBuilder는 BootstrapServiceRegistry 인스턴스를 생성하고 사용합니다. 이미 생성된 인스턴스를 전달하기 위해 오버로드된 생성자를 사용할 수 있습니다.
BootstrapServiceRegistry bootstrapServiceRegistry =
new BootstrapServiceRegistryBuilder().build();
StandardServiceRegistryBuilder standardServiceRegistryBuilder =
new StandardServiceRegistryBuilder(bootstrapServiceRegistry);
hibernate.cfg.xml과 같은 리소스 파일에서 구성을 로드하기 위해 이 빌더를 사용하고, 마지막으로 StandardServiceRegistry의 인스턴스를 얻기 위해 build() 메소드를 호출합니다.
StandardServiceRegistry standardServiceRegistry = standardServiceRegistryBuilder
.configure()
.build();
Metadata
BootstrapServiceRegistry 또는 StandardServiceRegistry 유형의 ServiceRegistry를 인스턴스화하여 필요한 모든 서비스를 구성했으므로 이제 애플리케이션의 도메인 모델과 해당 데이터베이스 매핑에 대한 표현을 제공해야 합니다.
MetadataSources metadataSources = new MetadataSources(standardServiceRegistry);
metadataSources.addAnnotatedClass();
metadataSources.addResource()
Metadata metadata = metadataSources.buildMetadata();
SessionFactory
메타데이터에서 SessionFactory를 생성하는것입니다.
SessionFactory sessionFactory = metadata.buildSessionFactory();
이제 세션을 열고 엔터티를 유지하고 읽을 수 있습니다.
Session session = sessionFactory.openSession();
Movie movie = new Movie(100L);
session.persist(movie);
session.createQuery("FROM Movie").list();
Hibernate 시작하는 방법
클래스에 어노테이션을 추가한 후에도 Hibernate 자체를 부트스트래핑해야 합니다. Hibernate의 진입점인 SessionFactory를 구성해야 합니다. 세션은 기본적으로 데이터베이스 연결이며 이 세션을 사용하여 SQL/HQL/Criteria query를 실행합니다.
Spring을 이용하지 않고 시작해보겠습니다.
public static void main(String[] args) {
String path = new File(Hibernate.class.getProtectionDomain().getCodeSource().getLocation().getPath()).getParentFile().getAbsolutePath() + "/src/db/hibernate.cfg.xml";
ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder()
.configure(new File(path))
.build();
MetadataSources sources = new MetadataSources(serviceRegistry);
sources.addAnnotatedClass(User.class);
Metadata metadata = sources.buildMetadata();
SessionFactory sessionFactory = metadata.buildSessionFactory();
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.org/dtd/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="connection.url">jdbc:mysql://localhost:3306/002</property>
<property name="connection.username">root</property>
<property name="connection.password">1234</property>
<property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>
<property name="connection.pool_size">1</property>
<property name="current_session_context_class">thread</property>
<property name="cache.provider_class">org.hibernate.cache.NoCacheProvider</property>
<property name="show_sql">true</property>
<property name="hbm2ddl.auto">create</property>
</session-factory>
</hibernate-configuration>
기본 지속성이 Hibernate에서 작동하는 방식
이제 매핑을 설정하고 SessionFactory를 구성했으므로 남은 작업은 SessionFactory에서 세션을 가져온 다음 데이터베이스를 이용하는 것입니다. Hibernate/JPA 용어에서 이를 지속성이라고 합니다. 왜냐하면 Java 개체를 데이터베이스 테이블에 유지하기 때문입니다. 데이터를 저장하기 위한 SQL은 직접 작성할 필요가 없습니다.
Session session = sessionFactory.openSession();
User user = new User("foo1","bar1");
session.save(user);
JDBC와 비교해서 Statement를 이용해서 파라미터를 바인딩하고 update할 필요없이 Hibernate에서는 객체를 생성하고 save에 맞춰서 insert하는 SQL을 생성할 것입니다.
이제 select와 update 및 delete를 해보겠습니다.
session.beginTransaction();
User userId1 = session.get(User.class, 1);
userId1.setFirstName("foo11");
session.update(userId1);
session.getTransaction().commit();
session.beginTransaction();
session.delete(userId1);
session.getTransaction().commit();
save()는 호출 후 생성된 키가 객체에 존재함을 보장하여 transaction없이 insert가 되지만 update 및 delete는 그렇지 않기 때문에 transaction 안에서 이뤄줘야 됩니다.
Hibernate의 쿼리 언어(HQL)을 사용하는 방법
객체를 저장하건 삭제하는 기본적인 지속성만 알아보았습니다. 그러나 실제로는 더 복잡한 SQL문을 작성해야 하는 경우도 있습니다. 이를 위해 HQL이라는 자체 쿼리 언어를 제공합니다. HQL은 SQL과 유사해 보이지만 Java 개체에 중점을 두고 있으며 실제로 기본 SQL 데이터베이스와는 독립적입니다.
List<User> users = session.createQuery("from User u where u.firstName = 'foo1'", User.class).list()
SQL 쿼리랑 유사해 보이나 HQL에서는 테이블이나 열에 액세스하지 않습니다. 대신 매핑된 User 클래스의 속성에 액세스하고 있습니다. 그런 다음 Hibernate는 HQL문이 데이터베이스 SQL 문으로 변환하는 지 확인합니다. select의 경우에 반환된 행을 User 개체로 자동으로 변환합니다.
Hibernate에서 Criteria API 사용하는 방법
HQL 문을 작성할 때 기본적으로 여전히 일반 문자열을 작성하거나 연결합니다. HQL/SQL 문을 동적으로 만드는 것은 어렵습니다. 이를 위해 Hibernate는 Criteria API를 통해 또 다른 쿼리 언어를 제공합니다. 기본적으로 Criteria API에는 병렬로 존재하는 두 가지 버전이 있습니다. 버전 1은 더이상 사용되지 않으며 버전 2보다 사용하기는 훨씬 쉽습니다. v2 쿼리를 작성하는 것은 학습을 더 필요로 하고 더 많은 설정을 필요로 합니다.
EntityManagerFactory emf = session.getEntityManagerFactory();
EntityManager em = emf.createEntityManager();
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<User> criteria = builder.createQuery(User.class);
Root<User> root = criteria.from(User.class);
criteria.select(root)
.where(builder.equal(root.get("firstName"), "foo1"));
List<User> users2 = em.createQuery(criteria).getResultList();
위에 v2는 유형에 대한 안전성과 유연성을 위해 복잡합니다. 조건문을 자유롭게 작성하여 where 절을 동적으로 구현할 수 있습니다.
Hibernate의 단점
hibernate는 단순히 매핑 및 쿼리 기능만 제공하는 것이 아닙니다. 실제 매핑 및 쿼리는 보다 훨씬 더 복잡합니다. 게다가 Hibernate는 cascading, lazy loading, caching 등과 같은 수많은 편의 기능을 제공합니다.
- cascading : 부모 테이블의 레코드에 대한 작업이 관련된 자식 테이블의 레코드에 자동으로 적용되는 기능입니다.
- lazy loading: 데이터가 실제로 필요한 시점까지 데이터베이스로부터의 로딩을 지연시키는 기법입니다. 연관된 객체를 즉시 로딩하지 않고, 해당 객체에 처음 접근할 때 데이터베이스에서 로딩합니다.
복잡하기 때문에 개발자는 Hibernate가 무엇을 하고 있는지에 대한 배경 지식을 계속 학습해야 합니다. 객체로 관리하기 때문에 더이상 개발자는 SQL을 공부하지 않아야 한다고 생각할 수도 있습니다. 하지만 Hibernate가 SQL 문을 잘 생성하고 최적화하는지에 대한 확인을 위해 SQL 기술을 더 학습해야 합니다.
이런점에서 Hibernate를 사용한다고 해서 기초적인 튜토리얼 문법만 가지고 사용하는게 아니라 Hibernate 원리와 SQL에 대한 지식을 쌓아야합니다.
JPA(Java Persistence API) 란
JPA는 구현이나 라이브러리가 아닌 단지 자바 애플리케이션에서 데이터베이스를 사용하는 방식을 정의했을 뿐입니다. 따라서 JPA는 라이브러리가 JPA 규격을 준수하기 위한 기능에 대한 표준을 정의합니다. JPA를 구현하는 라이브러리는 Hibernate, EclipseLink 등이 있습니다. 라이브러리가 특정 방식으로 데이터베이스에 객체를 저장하는 것을 지원하고, 매핑 및 쿼리 기능을 지원하는 경우 JPA를 준수한 경우라고 볼 수 있습니다.
JPA와 Hibernate의 차이점
이론적으로 JPA를 사용하면 Hibernate와 같은 JPA 구현 라이브러리를 사용하지 않을 수 있습니다. Hibernate는 JPA보다 많은 기능을 합니다. 예를 들어 JPQL은 HQL보다 기능이 더 적습니다. 그래서 유효한 JPQL은 HQL에 유효하지만 반대의 경우에는 작동하지 않을 수 있습니다.
JPA에서 기본 지속성이 작동하는 방식
JPA에서 진입점은 EntityMangerFactory 및 EntityManager입니다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-config");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
User user4 = User.builder()
.firstName("foo4")
.lastName("bar4")
.build();
em.persist(user4);
em.getTransaction().commit();
em.close();
META-INF 안에 persistence.xml이 필요합니다.
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
version="2.1">
<persistence-unit name="jpa-config">
<properties>
<property name="persistenceXmlLocation" value="src/db/persistence.xml" />
<property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
<property name="javax.persistence.jdbc.user" value="root"/>
<property name="javax.persistence.jdbc.password" value="1234"/>
<property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/002"/>
</properties>
</persistence-unit>
</persistence>
JPQL의 사용방법
JPA에는 자체 쿼리 언어인 JPQL이 제공됩니다.
List<User> users = em.createQuery("from User u where u.firstName = :firstName")
.setParameter("firstName", "foo4")
.getResultList();
QueryDSL
QueryDSL은 Criteria API보다 쿼리 구성이 더 쉽고 일반 문자열보다 type 안전성이 뛰어납니다.
QClass 만들기
javac -cp D:/JAR/* -processor com.querydsl.apt.jpa.JPAAnnotationProcessor -d bin -s D:/WORKSPACE/JDBC/src/generated D:/WORKSPACE/JDBC/src/db/User.java D:/WORKSPACE/JDBC/src/db/Card.java
QUser qUser = QUser.user;
JPAQuery<?> query = new JPAQuery<Void>(em);
List<User> users2 = query.select(qUser)
.from(qUser)
.where(qUser.firstName.eq("foo4"))
.fetch();