[JPA] JAVA ORM 표준 JPA
JPA(Java Persistence API)
SQL 없이 객체를 데이터베이스에 직접 저장할 수 있게 하고, 객체와 관계형 데이터베이스를 명확히 구분해준다. 즉, 중간에서 매핑 역할을 수행하는 ORM(Object-Relational Mapping) 기술의 표준 인터페이스이다.
객체지향과 관계형 데이터베이스의 패러다임 불일치를 해결해준다.
JPA를 사용하는 이유
유지보수 용이
- 필드 추가/변경 시 SQL을 수정할 필요가 없고 엔티티 클래스만 수정하면 자동으로 반영
생산성 향상
- CRUD SQL 이나, 반복적인 JDBC 코드를 개발자가 작성할 필요가 없다.
객체지향적 개발 가능
- SQL 중심이 아닌 객체 중심으로 개발해 비즈니스 로직에 집중할 수 있다.
데이터베이스 독립성
- 특정 DB에 종속되지 않고, DB 변경 시 설정만 바꾸면 된다.
패러다임 불일치 해결
- 상속, 연관관계, 객체 그래프 탐색 등 객체지향 기능을 자연스럽게 사용 가능하다.
- 복잡한 JOIN 로직을 객체 참조로 대체
성능 최적화
- 1차 캐시, 지연 로딩, 쓰기 지연 등 다양한 최적화 기능 제공
SQL 대신 객체로 작업하면서 생산성을 높이고, 유지보수를 쉽게 하기 위해 사용한다.
ORM(Object-Relational Mapping)
객체와 관계형 데이터베이스의 데이터를 자동으로 매핑해주는 기술, 객체지향 프로그래밍의 객체와 관계형 데이터베이스의 테이블 사이의 불일치를 해결해주는 다리 역할을 한다.
1
2
3
4
5
객체(Object) ↔ 테이블(Table)
필드(Field) ↔ 컬럼(Column)
인스턴스(Instance) ↔ 행(Row)
- JPA = 자바의 ORM 기술 표준 인터페이스 (명세)
- Hibernate, EclipesLink = JPA를 구현한 구현체
JPA는 ‘무엇을 해야 하는지(What)’ , Hibernate 같은 구현체가 ‘어떻게 할 것인가(How)’ 를 구현
JPA의 구동 방식
JPA는 persistence.xml 설정 파일을 읽어서 EntityManagerFactory를 생성한다.
1
2
3
4
5
6
7
persistence.xml 읽기
       ↓
EntityManagerFactory 생성 (앱 전체에서 1개만)
       ↓
EntityManager 생성 (요청마다 생성/삭제)
       ↓
트랜잭션 단위로 작업 수행
EntityManagerFactory
- 애플리케이션 로딩 시점 1개만 생성해 전체 공유
- 생성 비용이 크기 때문에 1개만 생성해 재사용
EntityManager
- 요청(트렌젝션)마다 생성하고 사용 후 버림
- 쓰레드 간 공유 금지 (동시성 이슈)
- 데이터베이스 커넥션과 밀접한 관계
사용 흐름
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. EntityManagerFactory 생성 (앱 시작 시 1번)
EntityManagerFactory emf = 
    Persistence.createEntityManagerFactory("hello");
// 2. EntityManager 생성 (요청마다)
EntityManager em = emf.createEntityManager();
// 3. 트랜잭션 획득
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
    // 비즈니스 로직 실행
    Cat cat = new Cat();
		cat.setId(1L);
		cat.setCatname("생강이");
    
    tx.commit();  // 커밋
} catch (Exception e) {
    tx.rollback();  // 롤백
} finally {
    em.close();  // EntityManager 종료
}
emf.close();  // 앱 종료 시
- JPA의 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
- EntityManager는 쓰레드 간 공유 금지 사용 후 반드시 close()
- Spring Data JPA 사용 시 EntityManagerFactory, EntityManager 생성을 Spring이 자동으로 처리해 개발자는 @Repository,@Transactional만 사용
영속성 컨텍스트
엔티티를 영구 저장하는 환경
em.persist(entity); 는 엔티티를 DB에 저장하는게 아니라, 영속성 컨텍스트에 저장하는 것이다.
- 논리적인 개념으로 눈에 보이지 않음
- EntityManager를 통해 접근하고 EntityManager 생성 시 1:1로 영속성 컨텍스트가 생성
엔티티의 생명주기
비영속 (new/transient)
- 영속성 컨텍스트와 전혀 관계없는 새로운 상태
1
2
3
4
Cat cat = new Cat();
cat.setId(1L);
cat.setCatname("생강이");
// 아직 영속성 컨텍스트에 없음
영속 (managed)
- 영속성 컨텍스트에 관리되는 상태
1
em.persist(cat); //영속성 컨텍스트에 저장됨 (아직 데이터베이스에 저장 안됨)
준영속 (detached)
- 영속성 컨텍스트에 저장되었다가 분리된 상태
1
2
3
em.detach(cat);  // 특정 엔티티만 준영속 상태로
em.clear();         // 영속성 컨텍스트 완전히 초기화
em.close();         // 영속성 컨텍스트 종료
삭제 (removed)
- 삭제된 상태
1
em.remove(cat);  //객체를 삭제한 상태
영속성 컨텍스트의 장점
1차 캐시
- 영속성 컨텍스트 내부에 1차 캐시가 존재한다.
1
2
3
4
5
6
7
8
// 1차 캐시에 저장
Cat cat = new Cat();
cat.setId(1L);
cat.setCatname("생강이");
// 아직 영속성 컨텍스트에 없음
// 1차 캐시에서 조회 (select sql 실행안됨)
Cat findCat = em.find(Cat.class, 1L);
- 같은 트랜잭션 안에서는 같은 엔티티를 반환 (동일성 보장)
- DB 조회 횟수를 줄여 성능 향상
동일성(identity) 보장
1
2
3
4
Cat a = em.find(Cat.class, 1L);
Cat b = em.find(Cat.class, 1L);
System.out.println(a == b);  // true
- 1차 캐시로 반복 가능한 읽기 등급의 트랜잭션 격리 수준을 애플리케이션 차원에서 제공
트랜잭션을 지원하는 쓰기 지연
1
2
3
4
5
6
7
8
EntityTransaction tx = em.getTransaction();
tx.begin();
em.persist(catA);
em.persist(catB);
// 여기까지 INSERT SQL을 DB에 보내지 않음
tx.commit();  // 커밋하는 순간 DB에 INSERT SQL을 모아서 보냄
- INSERT SQL을 모아서 한번에 전송 → 성능 최적화
변경 감지 (Dirty Checking)
1
2
3
4
5
6
7
8
9
10
11
12
13
EntityTransaction tx = em.getTransaction();
tx.begin();
// 영속 엔티티 조회
Cat catA = em.find(Cat.class, 1L);
// 영속 엔티티 데이터 수정
catA.setCatname("여름이");
catA.setAge(3);
// em.update(member) 이런 코드 필요 없음!
tx.commit();  // 커밋 시점에 자동으로 UPDATE SQL 실행
- 트랜잭션 커밋 시 em.flush()자동 호출
- 엔티티와 스냅샷(최초상태) 비교
- 변경된 엔티티가 있으면 UPDATE SQL 생성
- UPDATE SQL을 DB 에 전송
- 트랜잭션 커밋
지연로딩
- 연관된 엔티티를 실제 사용할 때까지 조회를 미루는 기능
flush
영속성 컨텍스트의 변경 내용을 데이터베이스에 반영하는 것.
flush 발생 시:
- 변경 감지 (Dirty Checking)
- 수정된 엔티티를 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 DB에 등록 (등록,수정,삭제)
flush 사용 법:
1
2
3
em.flush();        // 직접 호출
tx.commit();       // 트랜잭션 커밋 시 자동 호출
JPQL 쿼리 실행;     // JPQL 쿼리 실행 시 자동 호출
- flush는 영속성 컨텍스트를 비우지 않음
- 영속성 컨테그스트의 변경 내용을 DB에 동기화 하는 것
- 트랜잭션이라는 작업 단위가 중요 → 커밋 직전에만 동기화 하면 됨.
기본키 매핑
직접 할당
1
2
@Id
private Long id;
- 애플리케이션에서 직접 ID값을 설정
- @GeneratedValue없이- @Id만 사용
자동 생성(@GeneratedValue)
1
2
3
@Id
@GeneratedValue(strategy = GenerationType.XXX)
private Long id;
- 데이터베이스가 자동으로 ID생성
- strategy로 생성 전략 선택
기본키 생성 전략 (strategy)
IDENTITY 전략
기본키 생성을 데이터베이스에 위임
1
2
3
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
- em.persist()호출 시점에 즉시 INSERT SQL 실행
- DB에 INSERT를 해야만 ID 값을 알 수 있음
- 쓰기 지연 불가능
SEQUENCE 전략
데이터베이스 시퀀스를 사용해 기본키 할당
1
2
3
4
5
6
7
8
9
10
11
12
13
@Entity
@SequenceGenerator(
    name = "cat_seq_generator",
    sequenceName = "cat_seq",
    initialValue = 1, 
    allocationSize = 50
)
public class Cat {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
                    generator = "cat_seq_generator")
    private Long id;
}
- em.persist()호출 시점에 시퀀스에서 값을 조회
- 조회한 값을 엔티티에 할당 후 영속성 컨텍스트에 저장
- 트랜잭션 커밋 시점에 INSERT SQL 실행 (쓰기 지연 가능)
- allocationSize로 성능 최적화 가능 (기본값 50)
TABLE 전략
키 생성 전용 테이블을 만들어 시퀀스를 흉내내는 전략
- 모든 DB에서 사용 가능
- 성능 이슈로 실무에서 잘 사용하지 않음
AUTO 전략 (기본값)
DB 방언(Dialect) 에 따라 자동으로 선택
1
2
3
@Id
@GeneratedValue(strategy = GenerationType.AUTO) // 또는 생략 가능
private Long id;
- 데이터베이스를 변경해도 코드 수정 불필요
- MySQL → IDENTITY
- Oracle → SEQUENCE