IntelliJ 프로젝트 생성으로 대체하였다.
H2 데이터베이스 설치 후 예제 테이블을 생성한다.
예제 2.1. 회원 테이블
CREATE TABLE MEMBER (
ID VARCHAR(255) NOT NULL,
NAME VARCHAR(255),
AGE INTEGER NOT NULL,
PRIMARY KEY (ID)
);
jpa 구현체로 하이버네이트를 사용하기 위한 핵심 라이브러리는 다음과 같다.
hibernate-core: 하이버네이트 라이브러리hibernate-entitymanager: 하이버네이트가 JPA 구현체로 동작하도록 JPA 표준을 구현한 라이브러리hibernate-jpa-2.1-api: JPA 2.1 표준 API를 모아둔 라이브러리그러나 IntelliJ 프로젝트 생성 시 스프링 부트 프로젝트를 생성하고 Spring Data JPA 의존성 스타터를 추가해 위와 같은 의존성들이 자동으로 설치되었다.
예제 2.3. 메이븐 설정 파일 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 https://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.5.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.example</groupId>
<artifactId>ch02-spa-start1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>ch02-spa-start1</name>
<description>ch02-spa-start1</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
스프링 부트 프로젝트를 생성했기 때문에 책의 내용과 상당히 다르다. 핵심은 JPA, 하이버네이트, H2 데이터베이스를 사용할 수 있게 메이븐 설정 파일을 구성했다는 것이다.
예제 2.4. 회원 테이블
CREATE TABLE MEMBER (
ID VARCHAR(255) NOT NULL,
NAME VARCHAR(255),
AGE INTEGER NOT NULL,
PRIMARY KEY (ID)
);
예제 2.5. 회원 클래스
package org.example.ch02spastart1.model;
public class Member {
private String id;
private String username;
private Integer age;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Member{" +
"id='" + id + '\'' +
", username='" + username + '\'' +
", age=" + age +
'}';
}
}
다음의 매핑 정보를 바탕으로 회원 클래스와 회원 테이블을 매핑한다.
| 매핑 정보 | 회원 클래스 | 회원 테이블 |
|---|---|---|
| 클래스와 테이블 | Member | MEMBER |
| 기본 키 | id | ID |
| 필드와 컬럼 | username | NAME |
| 필드와 컬럼 | age | AGE |
JPA가 제공하는 매핑 애너테이션을 사용한다. JPA는 매핑 애너테이션을 분석해 객체와 테이블 간의 관계를 파악한다.
예제 2.6. 매핑 정보가 포함된 회원 클래스
package org.example.ch02spastart1.model;
import jakarta.persistence.*;
@Entity
@Table(name = "MEMBER")
public class Member {
@Id
@Column(name = "ID")
private String id;
@Column(name = "NAME")
private String username;
// 매핑 정보가 없는 필드
private Integer age;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Member{" +
"id='" + id + '\'' +
", username='" + username + '\'' +
", age=" + age +
'}';
}
}
name 속성에 대상 테이블 이름을 명시했다. 이 애너테이션을 생략할 시 클래스 이름(엔터티 이름)을 테이블 이름으로 매핑한다.name 속성에 대상 테이블 컬럼의 이름을 명시했다.age 필드에 매핑 애너테이션이 생략되었다. 생략 시 필드명을 컬럼명으로 매핑한다. 만약 데이터베이스가 대소문자를 구분한다면 @Column(name = "AGE")와 같이 명시해야 한다.만약 스프링 부트의 Spring Data JPA 의존성 스타터를 사용하지 않는다면 META-INF/persistence.xml 파일로 JPA 설정 정보를 관리한다.
스프링 부트 환경에서 동일하게 설정하려면 다음과 같이 프로퍼티 파일을 구성할 수 있다.
resources/application.properties
spring.application.name=ch02-jpa-start1
# ====================================================================
# 필수 속성: 데이터 소스 및 JPA/하이버네이트 설정
# ====================================================================
# 1. 데이터 소스 설정 (DataSource)
# 표준 JPA 속성 대신 스프링 부트 속성 사용
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
# 2. JPA/하이버네이트 설정 (JPA/Hibernate)
# 데이터베이스 방언(Dialect) 설정
# 스프링 부트는 이 값을 보고 적절한 방언을 자동 설정하지만, 명시할 수도 있습니다.
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# 하이버네이트 DDL 자동 생성 기능 (CREATE TABLE, DROP TABLE 등)
# persistence.xml의 hbm2ddl.auto를 대체
# spring.jpa.hibernate.ddl-auto=create
# ====================================================================
# 옵션 속성
# ====================================================================
# 1. SQL 로깅 및 포맷
spring.jpa.show-sql=true
# hibernate.format_sql, hibernate.use_sql_comments 는 spring.jpa.properties.* 접두사를 사용
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
# 2. 새로운 ID 생성 전략 사용 설정
spring.jpa.properties.hibernate.id.new_generator_mappings=true
# 3. 영속성 유닛 이름 (persistence-unit name="jpabook" 대체)
# 일반적으로 명시할 필요는 없지만, 특정 설정을 위해 사용 가능
# spring.jpa.properties.hibernate.ejb.persistenceUnitName=jpabook
그러나 스프링 부트의 구성보다 관례(Convention over Configuration) 원칙을 활용하지 않을 것이기 때문에 JPA 환경설정(resources/META-INF/persistence.xml)을 다음과 같이 구성한다.
예제 2.7. JPA 환경설정 파일 persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.2"> <persistence-unit name="jpabook">
<properties>
<property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="jakarta.persistence.jdbc.password" value=""/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
<property name="hibernate.show_sql" value="true" />
<property name="hibernate.format_sql" value="true" />
<property name="hibernate.use_sql_comments" value="true" />
<property name="hibernate.id.new_generator_mappings" value="true" />
</properties>
</persistence-unit>
</persistence>
persistence 태그로 시작하며 XML 네임스페이스(xmlns), 사용할 버전(version)을 지정한다.persistence-unit)으로 시작하는데 일반적으로 연결할 데이터베이스당 하나의 영속성 유닛을 등록한다. 영속성 유닛에는 고유한 이름(name)이 부여되어야 한다.jakarta.persistence.jdbc.driver: JDBC 드라이버jakarta.persistence.jdbc.user: 데이터베이스 사용자 IDjakarta.persistence.jdbc.password: 데이터베이스 사용자 PWjakarta.persistence.jdbc.url: 데이터베이스 접속 URLhibernate.dialect: 데이터베이스 방언(Dialect) 설정hibernate.show_sql: 하이버네이트가 실행한 SQL 출력hibernate.format_sql: 하이버네이트가 실행한 SQL을 출력할 때 보기 쉽게 정렬hibernate.use_sql_comments: 하이버네이트가 생성한 SQL에 주석 추가hibernate.id.new_generator_mappings: JPA 표준에 맞춘 새로운 키 생성 전략 사용이름이 javax.persistence(현재는 jakarta.persistence)로 시작하는 속성은 JPA 표준 속성으로 특정 구현체에 종속되지 않는다. 반면 hibernate로 시작하는 속성은 하이버네이트에서만 사용할 수 있다.
사용한 속성은 대부분 데이터베이스에 연결하기 위한 것이며, 가장 중요한 속성은 데이터베이스 방언을 설정하는 hibernate.dialect이다.
JPA는 특정 데이터베이스 기술에 종속적이지 않기 때문에 마이그레이션이 매우 간편하지만, 각각의 데이터베이스는 기본적으로 다음과 같은 차이점들이 존재한다.
VARCHAR, 오라클은 VARCHAR2를 사용한다.SUBSTRING(), 오라클은 SUBSTR()을 사용한다.LIMIT, 오라클은 ROWNUM을 사용한다.위와 같이 데이터베이스마다 존재하는 표준 SQL과의 차이점, 독립적인 기능을 JPA에서는 방언(Dialect)이라고 한다. 특정 데이터베이스에 종속되는 기능을 많이 사용할수록 마이그레이션이 복잡해진다. 하이버네이트를 포함한 대부분의 JPA 구현체들은 이를 해결하기 위해 다양한 데이터베이스 방언 클래스를 제공한다.

개발자는 JPA 표준 문법에 맞추어 코드를 작성하고, 특정 데이터베이스에 의존적인 기능들은 데이터베이스 방언이 처리하도록 하면 된다. 이렇게 함으로써 마이그레이션을 해야 할 때도 데이터베이스 방언만 교체할 수 있다. 참고로 데이터베이스 방언의 설정 방법은 JPA에 표준화되어 있지 않다.
하이버네이트는 다양한 데이터베이스 방언을 제공하는데, 대표적인 것들은 다음과 같다.
org.hibernate.dialect.H2Dialectorg.hibernate.dialect.Oracle10gDialectorg.hibernate.dialect.MySQL5InnoDBDialect서버 시작 코드를 다음과 같이 작성한다.
예제 2.8. 시작 코드
package jpabook.start.ch02jpastart1;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Ch02JpaStart1Application {
public static void main(String[] args) {
SpringApplication.run(Ch02JpaStart1Application.class, args);
// [엔터티 매니저 팩토리] - 생성
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
// [엔터티 매니저] - 생성
EntityManager em = emf.createEntityManager();
// [트랜잭션] - 획득
EntityTransaction tx = em.getTransaction();
try {
tx.begin(); // [트랜잭션] - 시작
logic(em); // 비즈니스 로직 실행
tx.commit(); // [트랜잭션] - 커밋
} catch (Exception e) {
tx.rollback(); // [트랜잭션] - 롤백
} finally {
em.close(); // [엔터티 매니저] - 종료
}
emf.close(); // [엔터티 매니저 팩토리] - 종료
}
private static void logic(EntityManager em) {
}
}
코드는 크게 세 가지 부분으로 구성되어 있다.

JPA를 시작하려면 우선 클래스 패스의 persistence.xml 설정 정보를 통해 엔터티 매니저 팩토리를 생성해야 한다. 이때 Persistence 클래스를 사용한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
이러면 resources/META-INF/persistence.xml 파일에서 이름이 jpabook인 영속성 유닛(persistence-unit)을 찾아 엔터티 매니저 팩토리를 생성한다. 이때 설정 정보를 읽어 JPA 동작을 위한 기반 객체를 생성하고 JPA 구현체에 따라서는 커넥션 풀도 생성하므로 엔터티 매니저 팩토리 객체는 애플리케이션 전체에서 하나만 생성하고 공유해야 한다.
EntityManager em = emf.createEntityManager();
엔터티 매니저를 사용해 엔터티를 데이터베이스에 등록/수정/삭제/조회할 수 있다. JPA의 기능 대부분은 엔터티 매니저가 제공하며, 내부에 데이터 소스를 유지하면서 데이터베이스와 통신한다. 엔터티 매니저는 데이터베이스 커넥션과 밀접한 관계가 있으므로 스레드 간 공유하거나 재사용하면 안 된다.
em.close();
emf.close();
사용이 끝난 엔터티 매니저와 엔터티 매니저 팩토리는 종료해야 한다.
예제 2.9. 트랜잭션 코드 부분
// [트랜잭션] - 획득
EntityTransaction tx = em.getTransaction();
try {
tx.begin(); // [트랜잭션] - 시작
logic(em); // 비즈니스 로직 실행
tx.commit(); // [트랜잭션] - 커밋
} catch (Exception e) {
tx.rollback(); // [트랜잭션] - 롤백
}
JPA 사용 시엔 항상 트랜잭션 안에서 데이터를 변경해야 한다. 트랜잭션 없이 데이터를 변경하면 예외가 발생한다. 트랜잭션을 시작하려면 엔터티 매니저(em)에서 트랜잭션 API(tx)를 받아와야 한다.
예제 2.10. 비즈니스 로직 코드
private static void logic(EntityManager em) {
String id = "id1";
Member member = new Member();
member.setId(id);
member.setUsername("유신");
member.setAge(25);
// 등록
em.persist(member);
// 수정
member.setAge(24);
// 한 건 조회
Member foundMember = em.find(Member.class, id);
System.out.println("foundMember" + foundMember + ", age = " + foundMember.getAge());
// 목록 조회
List<Member> members = em.createQuery("SELECT m FROM Member m", Member.class).getResultList();
System.out.println("members.size=" + members.size());
// 삭제
em.remove(member);
}
String id = "id1";
Member member = new Member();
member.setId(id);
member.setUsername("유신");
member.setAge(25);
// 등록
em.persist(member);
엔터티를 저장하려면 엔터티 매니저의 persist() 메서드에 엔터티 객체를 전달하면 된다. JPA는 회원 엔터티의 매핑 정보(애너테이션)를 분석해 다음과 같은 SQL을 생성한다.
insert
into
MEMBER (age, NAME, ID)
values
('id1', '유신', 25)
// 수정
member.setAge(24);
JPA는 어떤 엔터티가 변경되었는지 추적하는 기능을 갖추고 있기 때문에 위와 같이 엔터티 객체의 필드를 수정하면 다음과 같은 SQL을 생성한다.
update MEMBER
set
age=24,
NAME='유신'
where
ID='id1'
// 삭제
em.remove(member);
엔터티 매니저의 remove() 메서드에 삭제하려는 엔터티의 객체를 전달한다. JPA는 다음과 같은 SQL을 생성한다.
delete
from
MEMBER
where
ID='id1'
// 한 건 조회
Member foundMember = em.find(Member.class, id);
엔터티 매니저의 find() 메서드는 조회할 엔터티의 타입과 @Id 애너테이션으로 지정된 필드(기본 키와 매핑한 식별자 값)로 엔터티 하나를 조회한다. JPA는 먼저 다음과 같은 SQL을 생성한다.
select
*
from
MEMBER
where
ID='Id1'
그리고 조회한 결과 값으로 엔터티를 생성해 반환한다.
// 목록 조회
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> members = query.getResultList();
JPA는 엔터티 객체를 중심으로 개발하므로 검색 시에도 테이블이 아닌 엔터티 객체를 대상으로 검색해야 한다. 그러나 테이블이 아닌 엔터티 객체를 대상으로 검색하려면 데이터베이스의 모든 레코드를 애플리케이션으로 불러와 엔터티 객체로 변환한 후 검색해야 한다. 이는 사실상 불가능하기에 JPA는 JPQL(Java Persistence Query Language)이라는 쿼리 언어로 문제를 해결한다.
JPQL은 SQL을 추상화한 객체지향 쿼리 언어로, SQL과 문법이 거의 유사해서 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등을 사용할 수 있다. SQL과의 주요한 차이점은 다음과 같다.
즉, 위 코드에서 SELECT m FROM Member이 JPQL 쿼리이다. JPQL은 데이터베이스 테이블을 전혀 알지 못한다. 따라서 FROM Member는 데이터베이스의 MEMBER 테이블이 아닌 사용자가 생성한 Member 객체를 의미한다.
JPQL은 em.createQuery(JPQL, [return type]) 메서드를 실행해 쿼리 객체를 생성한 후 쿼리 객체의 getResultList() 메서드를 호출하여 사용할 수 있다. JPA는 다음과 같은 SQL을 생성한다.
select
m1_0.ID,
m1_0.age,
m1_0.NAME
from
MEMBER m1_0