객체 매핑 시작
회원 테이블이 다음과 같이 정의되었다고 가정하겠습니다.
CREATE TABLE MEMBER { | |
ID VARCHAR(255) NOT NULL, -- 아이디 (기본 키) | |
NAME VARCHAR(255), -- 이름 | |
AGE INTEGER, -- 나이 | |
PRIMARY KEY(ID) | |
} |
그리고 회원 클래스가 다음과 같이 정의되었다고 가정하겠습니다.
@Data | |
public class Member { | |
private String id; | |
private String username; | |
private Integer age; | |
} |
JPA를 사용하려면 가장 먼저 회원 클래스와 회원 테이블을 매핑해야 하고 이를 위해 회원 클래스에 JPA가 제공하는 매핑 어노테이션을 추가하겠습니다.
@Entity | |
@Table(name = "MEMBER") | |
public class Member { | |
@Id | |
@Column(name = "ID") | |
private String id; | |
@Column(name = "NAME") | |
private String username; | |
// 매핑 정보가 없는 필드로 기본 매핑 | |
private Integer age; | |
} |
매핑 정보 | 회원 객체 | 회원 테이블 |
클래스와 테이블 | Member | MEMBER |
기본 키 | id | ID |
필드와 컬럼 | username | NAME |
필드와 컬럼 | age | AGE |
부연 설명
- @Entity: 클래스를 테이블과 매핑한다고 JPA가 알리며 이렇게 @Entity가 사용된 클래스를 엔티티 클래스라고 부름
- @Table: 엔티티 클래스에 매핑할 테이블 정보를 알려주며 여기서는 name 속성을 사용해서 Member 엔티티를 MEMBER 테이블에 매핑함
- @Id: 엔티티 클래스의 필드를 테이블의 기본 키에 매핑하며 이렇게 @Id가 사용된 필드를 식별자 필드라고 부름
- @Column: 필드를 컬럼에 매핑
- 매핑 정보가 없는 필드: 매핑 어노테이션을 생략하면 필드명을 사용해서 컬러명으로 매핑, 만약 대소문자를 구분하는 데이터베이스를 사용할 경우 명시적으로 @Column(name="AGE")와 같이 매핑해야 함
persistence.xml 설정
책이 지필된 시점에는 JPA가 persistence.xml을 사용해서 필요한 설정 정보를 관리했지만 현재는 SpringBoot가 제공하는 Auto-Configuration 기능과 application.yml을 활용하여 JPA, 데이터소스 및 Hibernate 등의 설정을 간편하게 처리할 수 있습니다.
따라서 책에서 소개한 persistence.xml에 정의된 설정을 기반으로 Java Config 클래스로 변환한 예제를 소개하겠습니다.
<?xml version="1.0" encoding="UTF-8"?> | |
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.1"> | |
<persistence-unit name="jpabook"> | |
<properties> | |
<!-- 필수 속성 --> | |
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/> | |
<property name="javax.persistence.jdbc.user" value="sa"/> | |
<property name="javax.persistence.jdbc.password" value=""/> | |
<property name="javax.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" /> | |
<!--<property name="hibernate.hbm2ddl.auto" value="create" />--> | |
</properties> | |
</persistence-unit> | |
</persistence> |
부연 설명
- 설정 파일은 persistence로 시작하며 이곳에 XML 네임스페이스와 사용할 버전을 지정
- JPA 설정은 영속성 유닛 (persistence-unit)이라는 것부터 시작하는데 일반적으로 연결할 데이터베이스 당 하나의 영속성 유닛을 등록하며 영속성 유닛에는 고유한 이름을 부여해야 하는데 여기서는 jpabook이라는 이름을 사용
- JPA 표준 속성
- javax.persistence.jdbc.driver: JDBC 드라이버
- javax.persistence.jdbc.user: 데이터베이스 접속 아이디
- javax.persistence.jdbc.password: 데이터베이스 접속 비밀번호
- javax.persistence.jdbc.url: 데이터베이스 접속 URL
- 하이버네이트 속성
- hibernate.dialect: 데이터베이스 방언 설정
package com.tistory.jaimemin.config; | |
import java.util.Properties; | |
import javax.persistence.EntityManagerFactory; | |
import javax.sql.DataSource; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
import org.springframework.boot.autoconfigure.domain.EntityScan; | |
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; | |
import org.springframework.jdbc.datasource.DriverManagerDataSource; | |
import org.springframework.orm.jpa.JpaTransactionManager; | |
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; | |
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; | |
import org.springframework.transaction.PlatformTransactionManager; | |
@Configuration | |
@EntityScan(basePackages = "com.tistory.jaimemin.entity") // 엔티티 패키지 지정 | |
@EnableJpaRepositories(basePackages = "com.tistory.jaimemin.repository") // 레포지토리 패키지 지정 | |
public class JpaConfig { | |
// DataSource 빈 정의 | |
@Bean | |
public DataSource dataSource() { | |
DriverManagerDataSource dataSource = new DriverManagerDataSource(); | |
dataSource.setDriverClassName("org.h2.Driver"); | |
dataSource.setUrl("jdbc:h2:tcp://localhost/~/test"); | |
dataSource.setUsername("sa"); | |
dataSource.setPassword(""); | |
return dataSource; | |
} | |
// EntityManagerFactory 빈 정의 | |
@Bean | |
public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) { | |
LocalContainerEntityManagerFactoryBean emFactoryBean = new LocalContainerEntityManagerFactoryBean(); | |
emFactoryBean.setDataSource(dataSource); | |
emFactoryBean.setPackagesToScan("com.tistory.jaimemin.entity"); // 엔티티 스캔 경로 지정 | |
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); | |
emFactoryBean.setJpaVendorAdapter(vendorAdapter); | |
Properties jpaProperties = new Properties(); | |
jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); | |
jpaProperties.put("hibernate.show_sql", "true"); | |
jpaProperties.put("hibernate.format_sql", "true"); | |
jpaProperties.put("hibernate.use_sql_comments", "true"); | |
jpaProperties.put("hibernate.id.new_generator_mappings", "true"); | |
// 필요에 따라 아래 hbm2ddl.auto 옵션 활성화 할 수 있습니다. | |
// jpaProperties.put("hibernate.hbm2ddl.auto", "create"); | |
emFactoryBean.setJpaProperties(jpaProperties); | |
emFactoryBean.setPersistenceUnitName("jpabook"); // persistence-unit 이름 설정 | |
return emFactoryBean; | |
} | |
// TransactionManager 빈 정의 | |
@Bean | |
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) { | |
return new JpaTransactionManager(emf); | |
} | |
} |
1. 데이터베이스 방언
- JPA는 특정 데이터베이스에 종속적이지 않은 기술이기 때문에 다른 데이터베이스로 손쉽게 교체 가능
- 그런데 각 데이터베이스가 제공하는 SQL 문법과 함수가 조금씩 다르다는 문제점 존재
- ex) 가변 문자 타입으로 MySQL은 VARCHAR, ORACLE은 VARCHAR2를 사용
- ex) 문자열을 자르는 함수로 SQL 표준은 SUBSTRING()을 사용하지만 오라클은 SUBSTR()을 사용
- 이처럼 SQL 표준을 지키지 않거나 특정 DB만의 고유한 기능을 JPA에서는 Dialect (방언)이라고 부름
- 애플리케이션 개발자가 특정 데이터베이스에 종속되는 기능을 많이 사용하면 나중에 데이터베이스를 교체하기 어려움
- 하이버네이트를 포함한 대부분의 JPA 구현체들은 이런 문제를 해결하기 위해 다양한 데이터베이스 방언 클래스 제공
- 개발자는 JPA가 제공하는 표준 문법에 맞추어 JPA를 사용하면 되고, 특정 데이터베이스에 의존적인 SQL은 데이터베이스 방언이 처리해 주기 때문에 DB가 변경되어도 애플리케이션 코드를 변경할 필요 없이 데이터베이스 방언만 교체하면 됨

애플리케이션 개발
앞서 객체 매핑을 완료하고 persistence.xml로 JPA 설정도 완료했으므로 JPA 애플리케이션을 시작하는 코드를 작성할 차례입니다.
public class JpaMain { | |
public static void main(String[] args) { | |
//엔티티 매니저 팩토리 생성 | |
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook"); | |
EntityManager em = emf.createEntityManager(); //엔티티 매니저 생성 | |
EntityTransaction tx = em.getTransaction(); //트랜잭션 기능 획득 | |
try { | |
tx.begin(); //트랜잭션 시작 | |
logic(em); //비즈니스 로직 | |
tx.commit();//트랜잭션 커밋 | |
} catch (Exception e) { | |
e.printStackTrace(); | |
tx.rollback(); //트랜잭션 롤백 | |
} finally { | |
em.close(); //엔티티 매니저 종료 | |
} | |
emf.close(); //엔티티 매니저 팩토리 종료 | |
} | |
public static void logic(EntityManager em) { | |
String id = "id1"; | |
Member member = new Member(); | |
member.setId(id); | |
member.setUsername("jaimemin"); | |
member.setAge(30); | |
//등록 | |
em.persist(member); | |
//수정 | |
member.setAge(20); | |
//한 건 조회 | |
Member findMember = em.find(Member.class, id); | |
System.out.println("findMember=" + findMember.getUsername() + ", age=" + findMember.getAge()); | |
//목록 조회 | |
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList(); | |
System.out.println("members.size=" + members.size()); | |
//삭제 | |
em.remove(member); | |
} | |
} |
코드는 다음과 같이 크게 세 부분으로 나뉘어 있습니다.
- 엔티티 매니저 설정
- 트랜잭션 관리
- 비즈니스 로직
1. 엔티티 매니저 설정
- JPA를 시작하려면 우선 persistence.xml의 설정 정보를 사용해서 EntityManagerFactory를 생성해야 함
- 이떄 Persistence 클래스를 사용하는데 해당 클래스는 EntityManagerFactory를 생성해서 JPA를 사용할 수 있게 준비
- EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook"); 라인이 동작하면 META-INF/persistence.xml에서 이름이 jpabook인 persistence-unit을 찾아서 EntityManagerFactory를 생성
- JPA 구현체에 따라서는 데이터베이스 커넥션 풀도 생성하기 때문에 EntityManagerFactory를 생성하는 비용은 비싼 편
- 이에 따라 EntityManagerFactory는 빈으로 선언하여 애플리케이션 전체에서 딱 한 번만 생성하고 공유해서 사용
- 이후 EntityManagerFactory에서 JPA의 기능 대부분을 제공하는 EntityManager를 생성
- EntityManager를 사용해서 엔티티를 데이터베이스에 등록, 수정, 삭제 및 조회 가능
- EntityManager는 내부에 데이터소스를 유지하면서 DB와 통신하므로 가상의 데이터베이스로 생각할 수 있음
- EntityManager는 데이터베이스 커넥션과 밀접한 관계가 있으므로 쓰레드 간 공유하거나 재사용해서는 안됨
- 마지막으로 사용이 끝난 EntityManager는 close() 메서드를 통해 반드시 종료해야 함
- 애플리케이션을 종료할 때는 EntityManagerFactory를 close() 메서드를 통해 종료해야 함
2. 트랜잭션 관리
- JPA를 사용하면 항상 트랜잭션 안에서 데이터를 변경해야 함
- 트랜잭션을 시작하려면 EntityManager에서 트랜잭션 API를 받아와야 함
- 트랜잭션 없이 데이터 변경 시도하면 예외 발생
try { | |
tx.begin(); //트랜잭션 시작 | |
logic(em); //비즈니스 로직 | |
tx.commit();//트랜잭션 커밋 | |
} catch (Exception e) { | |
e.printStackTrace(); | |
tx.rollback(); //트랜잭션 롤백 | |
} finally { | |
em.close(); //엔티티 매니저 종료 | |
} |
3. 비즈니스 로직
- 코드에서 비즈니스 로직을 보면 등록, 수정, 삭제, 조회 작업이 EntityManager를 통해 수행되는 것을 확인 가능
- EntityManager는 객체를 저장하는 가상의 데이터베이스처럼 보임
- 등록: 엔티티를 저장하려면 EntityManager의 persist() 메서드에 저장할 에티티를 넘겨주면 되며 JPA는 회원 엔티티의 매핑 정보를 분석해서 INSERT문을 만들어 데이터베이스에 전달
- 수정: 엔티티를 수정한 후에 수정 내용을 반영하려면 em.update() 같은 메서드를 호출해야 할 것 같은데 단순히 엔티티의 값만 변경한 것을 확인 가능, JPA는 더티 체킹을 통해 어떤 엔티티가 변경되었는지 추적하는 기능을 갖추고 있어 트랜잭션 내 값을 변경할 경우 UPDATE SQL을 생성해서 데이터베이승에 값을 변경
- 삭제: 엔티티를 삭제하려면 EntityManager의 remove() 메서드에 삭제하려는 엔티티를 넘겨주면 되며 JPA는 DELETE SQL을 생성해서 실행
- 한건 조회: find() 메서드는 조회할 엔티티 타입과 @Id로 데이터베이스 테이블의 기본 키와 매핑한 식별자 값으로 엔티티 하나를 조회하는 가장 단순한 조회 메서드이며 호출 시 SELECT SQL을 생성해서 데이터베이스에 결과를 조회
//등록 | |
em.persist(member); | |
//수정 | |
member.setAge(20); | |
//한 건 조회 | |
Member findMember = em.find(Member.class, id); | |
System.out.println("findMember=" + findMember.getUsername() + ", age=" + findMember.getAge()); | |
//삭제 | |
em.remove(member); |
4. JPQL
예제에서 하나 이상의 회원 목록을 조회하는 코드는 다음과 같습니다.
//목록 조회 | |
List<Member> members = em.createQuery("select m from Member m", Member.class).getResultList(); | |
System.out.println("members.size=" + members.size()); |
JPA.는 엔티티 객체를 중심으로 개발하므로 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색해야 합니다.
- 그런데 테이블이 아닌 엔티티 객체를 대상으로 검색하기 위해서는 DB의 모든 데이터를 애플리케이션으로 불러와서 엔티티 객체로 변경한 다음 검색해야 하는데, 이는 사실상 불가능
- JPA는 JPQL (Java Persistence Query Lanaguage)이라는 쿼리 언어로 이런 문제를 해결
- JPA는 SQL을 추상화한 JPQL이라는 객체지향 쿼리 언어를 제공
- JPQL은 엔티티 객체를 대상으로 쿼리 하는 반면 SQL은 데이터베이스 테이블을 대상으로 쿼리 하는 차이점이 존재하지만 문법 자체는 거의 유사해서 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등을 사용 가능
- 위 코드에서 `select m from Member m`이 바로 JPQL이며 여기서 from Member는 회원 엔티티 객체를 말하는 것이지 MEMBER 테이블이 아님
- JPQL을 사용하려면 먼저 em.createQuery(JPQL, 반환타입) 메서드를 실행해서 쿼리 객체를 생성한 뒤 쿼리 객체의 getResultList() 메서드를 호출하면 됨
참고
자바 ORM 표준 JPA 프로그래밍 - 김영한 저
'DB > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[6장] 다양한 연관관계 매핑 (0) | 2025.02.27 |
---|---|
[5장] 연관관계 매핑 기초 (1) | 2025.02.24 |
[4장] 엔티티 매핑 (0) | 2025.02.21 |
[3장] 영속성 관리 (0) | 2025.02.21 |
[1장] JPA 소개 (0) | 2025.02.18 |