과거에는 객체를 데이터베이스에 저장하려면 복잡한 JDBC API와 SQL을 직접 작성해야 했다. MyBatis 등이 생겨나면서 JDBC API 코드는 사라졌지만 SQL을 한 땀 한 땀 작성해야 하는 번거로움은 남아있었다. JPA를 사용하면 SQL조차 작성할 필요가 없어진다.

비유: JDBC는 자전거, MyBatis는 오토바이, JPA는 자율주행 자동차와 같다. 자율주행 자동차를 올바르게 이용하려면 내부 동작 원리를 이해해야 한다. 그냥 타면 엉뚱한 곳으로 간다.


1단계: JPA란?

SQL 중심 개발의 문제점

객체를 데이터베이스에 저장·조회하려면 수많은 SQL과 매핑 코드가 필요하다.

// SQL 중심 개발: Member 필드가 추가되면 모든 SQL을 수정해야 함
String sql = "SELECT ID, NAME, TEL FROM MEMBER WHERE ID = ?";
// → age 필드 추가 시 SELECT, INSERT, UPDATE 모두 수정 필요

객체와 DB의 패러다임 불일치

graph LR
    subgraph OBJ["객체 세계"]
        O1["상속 계층"]
        O2["참조 (Association)"]
        O3["그래프 탐색"]
    end
    subgraph DB["관계형 DB"]
        D1["슈퍼타입/서브타입"]
        D2["외래키 (FK)"]
        D3["SQL JOIN으로만 탐색"]
    end
    O1 -.->|"불일치"| D1
    O2 -.->|"불일치"| D2
    O3 -.->|"불일치"| D3

JPA (Java Persistence API)

JPA는 자바 진영의 ORM(Object-Relational Mapping) 기술 표준이다. 개발자가 자바 객체를 조작하면 JPA가 적절한 SQL을 자동으로 생성해 실행한다.

JPA가 실무에서 어려운 이유

단순 CRUD만으로 시작하면 쉬워 보이지만, 실무에서는 수십 개 이상의 복잡한 객체와 테이블이 사용된다. N+1 문제, 지연 로딩 예외, 벌크 연산 불일치 등 JPA 내부 동작을 모르면 예상치 못한 버그를 만나게 된다.


2단계: 프로젝트 설정

의존성 추가 (pom.xml)

<!-- Hibernate (JPA 구현체) -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-entitymanager</artifactId>
    <version>5.6.15.Final</version>
</dependency>

<!-- H2 데이터베이스 (개발/테스트용) -->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.2.224</version>
</dependency>

JPA 환경설정 (persistence.xml)

src/main/resources/META-INF/persistence.xml 경로에 생성한다.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             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_2.xsd">
    <persistence-unit name="hello">
        <properties>
            <!-- 필수 속성: DB 연결 정보 -->
            <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"/>
            <!-- 방언: 각 DB에 맞는 SQL 자동 생성 -->
            <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"/>
        </properties>
    </persistence-unit>
</persistence>

dialect(방언)이란? 각 데이터베이스가 제공하는 SQL 문법과 함수는 조금씩 다르다. Hibernate는 방언 설정에 따라 각 DB에 맞는 SQL을 자동으로 생성해준다. MySQL이면 MySQLDialect, Oracle이면 OracleDialect를 사용한다.

JPA 구동 방식

graph TD
    XML["persistence.xml\n설정 파일"]
    EMF["EntityManagerFactory\n(애플리케이션 전체에서 1개만 생성)"]
    EM1["EntityManager\n(요청마다 1개 생성 후 버림)"]
    EM2["EntityManager\n(요청마다 1개 생성 후 버림)"]
    DB["Database"]

    XML -->|"1️⃣ Persistence.createEntityManagerFactory()"| EMF
    EMF -->|"2️⃣ emf.createEntityManager()"| EM1
    EMF -->|"2️⃣ emf.createEntityManager()"| EM2
    EM1 -->|"3️⃣ 쿼리 실행"| DB
    EM2 -->|"3️⃣ 쿼리 실행"| DB

3단계: 기본 CRUD 구현

엔티티 클래스 작성

H2 데이터베이스에 Member 테이블을 생성한다.

create table Member (
    id bigint not null,
    name varchar(255),
    primary key(id)
);

테이블과 매핑되는 엔티티 클래스를 작성한다.

package hellojpa;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity // JPA가 관리하는 클래스 — 반드시 필요
public class Member {
    @Id // 기본키 매핑
    private Long id;
    private String name;

    // 기본 생성자 필수 (JPA가 리플렉션으로 객체 생성)
    public Member() {}

    public Member(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    // getter, setter 생략
}

JPA로 CRUD 실행

public class JpaMain {

    public static void main(String[] args) {
        // EntityManagerFactory: 애플리케이션 전체에서 딱 1개만 생성
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // EntityManager: DB 작업 단위마다 새로 생성, 스레드 간 공유 금지!
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            // CREATE — persist()로 영속성 컨텍스트에 저장
            Member member = new Member(1L, "HelloA");
            em.persist(member); // DB에 저장은 아직 안됨 (쓰기 지연)

            // READ — find()로 1차 캐시에서 먼저 조회
            Member findMember = em.find(Member.class, 1L);
            System.out.println("name = " + findMember.getName());

            // UPDATE — setter 호출만으로 자동 반영 (변경 감지)
            findMember.setName("HelloB"); // em.update() 불필요!

            // DELETE — remove()로 삭제 예약
            // em.remove(findMember);

            tx.commit(); // flush() 실행 → SQL 전송 → DB 커밋
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close(); // EntityManager 반환
        }

        emf.close(); // 애플리케이션 종료 시 호출
    }
}

실행 로그

-- em.persist(member) → tx.commit() 시점에 INSERT 실행
Hibernate:
    /* insert hellojpa.Member */
    insert into Member (name, id) values (?, ?)

-- findMember.setName("HelloB") → tx.commit() 시점에 UPDATE 실행
Hibernate:
    /* update hellojpa.Member */
    update Member set name=? where id=?

4단계: 주의사항

EntityManagerFactory vs EntityManager

// EntityManagerFactory: 1개만 생성, 비용이 크므로 재사용
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

// EntityManager: 요청마다 새로 생성, 절대 스레드 간 공유 금지!
// (EntityManager는 Thread-safe하지 않음)
EntityManager em = emf.createEntityManager(); // 요청 시작
// ... 작업 수행 ...
em.close(); // 요청 종료 시 반드시 닫기

트랜잭션 필수

// JPA의 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
    // 데이터 변경 작업
    tx.commit();
} catch (Exception e) {
    tx.rollback(); // 예외 발생 시 반드시 롤백
} finally {
    em.close();
}

Spring 환경에서는 자동 관리

실무에서는 Spring이 EntityManagerFactory, EntityManager, 트랜잭션을 모두 자동으로 관리해준다.

@Service
@Transactional // Spring이 트랜잭션 + EntityManager를 자동으로 관리
public class MemberService {

    @Autowired
    private MemberRepository memberRepository;

    public void save(Member member) {
        memberRepository.save(member); // EntityManager.persist() 내부 호출
    }
}

극한 시나리오

시나리오 1: EntityManager를 스레드 간 공유했을 때

// 잘못된 코드: static 필드로 EntityManager를 공유
public class BadMemberService {
    private static EntityManager em = emf.createEntityManager(); // 위험!

    public void save(Member member) {
        em.persist(member); // 다른 스레드의 작업과 충돌 → 데이터 오염, 예외 발생
    }
}
// EntityManager는 Transaction 경계를 가지므로
// 여러 스레드가 공유하면 서로 다른 트랜잭션의 데이터가 섞임

시나리오 2: 트랜잭션 없이 데이터 변경

// 트랜잭션 없이 변경 시 예외 발생
EntityManager em = emf.createEntityManager();
Member member = em.find(Member.class, 1L);
member.setName("changed"); // 변경 감지는 트랜잭션 안에서만 동작
em.flush(); // javax.persistence.TransactionRequiredException 발생!

시나리오 3: IDENTITY 전략의 특수성

@Entity
public class Member {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; // DB가 AUTO_INCREMENT로 생성
}

// IDENTITY 전략은 persist() 시점에 즉시 INSERT 실행!
// 이유: INSERT 후에야 DB가 생성한 id값을 알 수 있기 때문에
// 쓰기 지연 최적화(배치 INSERT)가 적용되지 않는다는 점 주의
em.persist(new Member()); // 이 시점에 즉시 INSERT SQL 실행

실무 체크리스트

□ EntityManagerFactory는 애플리케이션 전체에서 1개만 생성
□ EntityManager는 요청(트랜잭션)마다 새로 생성, 스레드 간 공유 금지
□ 모든 데이터 변경은 트랜잭션 안에서 실행
□ Spring 환경에서는 @Transactional로 자동 관리 활용
□ dialect는 실제 사용하는 DB에 맞게 설정
□ 운영 환경에서 hibernate.hbm2ddl.auto=create 절대 금지

참조 - 자바 ORM 표준 JPA 프로그래밍 By 김영한

댓글