이 게시글은 김영한님의 자바 ORM 표준 JPA 프로그래밍 책을 기반으로 주요 내용을 정리하여 작성한 글입니다.
JPA 사용 이유
JPA는 객체지향 패러다임과 관계형 데이터베이스 사이에서 발생하는 패러다임 불일치 문제를 해결하고자 도입된 기술입니다. 대표적인 패러다임 불일치의 문제는 다음과 같습니다.
- 상속: 객체는 상속을 통해 다형성을 지원하지만, 데이터베이스 테이블은 이러한 상속 구조를 직접적으로 지원하지 않습니다.
- 연관 관계: 객체는 참조를 통해 연관된 객체와의 관계를 표현하고 자유롭게 탐색할 수 있지만, 테이블은 외래 키로 관계를 표현하며 조인을 통해서만 다른 테이블과의 관계를 가져옵니다.
객체와 테이블의 주요 차이점 중 하나는 객체는 참조가 있는 방향으로만 접근할 수 있지만, 테이블은 외래 키 하나로 양방향 조회가 가능하다는 점입니다. 예를 들어, 객체에서는 다음과 같은 방식으로 참조를 통해 연관된 객체를 찾아갑니다.
Team team = member.getTeam();
그러나 SQL을 직접 다룬다면, 객체 그래프 탐색을 유연하게 수행할 수 없습니다. JPA는 이러한 객체 간의 연관 관계를 데이터베이스와 연계하여 필요한 시점에 데이터를 불러올 수 있도록 합니다. 이를 통해 객체 지향적인 설계와 SQL 처리의 균형을 맞추어 줍니다.
JPA의 객체 그래프 탐색 기능
JPA를 사용하면 연관된 객체를 지연 로딩 방식(Lazy Loading)으로 필요 시점에만 조회할 수 있어 효율적인 데이터베이스 접근이 가능합니다.
member.getOrder().getOrderItem(); // 객체 그래프를 자유롭게 탐색
비교 방법
- 동일성(Identity): 객체의 주소를 비교하는 == 연산.
- 동등성(Equality): 객체의 내용을 비교하는 equals() 메서드.
데이터베이스의 동일한 행(Row)을 조회하더라도 객체 측면에서는 별도의 인스턴스로 인식되어 동일성 비교에서 실패할 수 있습니다. JPA는 트랜잭션 내에서 동일한 엔티티를 조회할 경우 동일한 객체를 반환하여 이러한 문제를 해결합니다.
JPA란?
JPA는 자바의 ORM(Object-Relational Mapping) 기술 표준으로, 객체와 관계형 데이터베이스를 매핑하는 기술입니다. JPA는 단순히 API 표준 명세를 제공하는 인터페이스 모음으로, 이를 사용하기 위해서는 Hibernate와 같은 JPA 구현체가 필요합니다.
JPQL
JPA는 JPQL(Java Persistence Query Language)을 통해 객체 지향적인 쿼리를 지원합니다.
select m from Member m
여기서 Member는 엔티티 객체를 의미하며, JPQL은 실제 데이터베이스 테이블이 아닌 엔티티 클래스에 대한 정보를 사용합니다.
영속성 관리
영속성 컨텍스트는 엔티티를 영구 저장하는 환경으로, 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 보관하고 관리합니다.
em.persist(member);
영속성 컨텍스트는 내부적으로 1차 캐시를 통해 동일한 엔티티를 반복 조회해도 같은 인스턴스를 반환하여 성능을 최적화하고 객체의 동일성을 보장합니다. 이를 통해 트랜잭션을 커밋하기 전까지 내부 쿼리 저장소에 쿼리를 모아두었다가 커밋 시에 일괄적으로 처리하는 쓰기 지연 기능을 제공합니다.
JPA 엔티티 상태 및 생명 주기
엔티티는 비영속, 영속, 준영속, 삭제의 생명주기를 가지며, 각 상태에 따라 영속성 컨텍스트와의 관계가 달라집니다.
- 비영속: 영속성 컨텍스트와 관계없는 상태
- 영속: 영속성 컨텍스트에 저장된 상태
- 준영속: 영속성 컨텍스트에서 분리된 상태
- 삭제: 영속성 컨텍스트와 데이터베이스에서 삭제된 상태
영속성 컨텍스트는 트랜잭션을 지원하는 쓰기 지연을 통해 효율적인 데이터베이스 처리를 제공합니다.
변경 감지
변경 감지(Dirty Checking)
엔티티의 변경 사항을 데이터베이스에 자동으로 반영하는 기능입니다.
- 작동 방식
- 영속성 컨텍스트가 엔티티의 초기 상태를 스냅샷으로 저장합니다.
- 플러시 시점에 스냅샷과 현재 엔티티 상태를 비교하여 변경 사항을 확인합니다.
- 변경된 엔티티가 감지되면, 자동으로 UPDATE SQL을 생성하여 변경 내용을 반영합니다.
Dirty Checking으로 생성되는 UPDATE 쿼리는 기본적으로 모든 필드를 업데이트하는 방식입니다. JPA에서는 이 전체 필드 업데이트를 기본 설정으로 사용하며, 이러한 방식은 다음과 같은 장점이 있습니다:
- 쿼리 재사용 가능: 생성되는 쿼리가 동일하므로, 애플리케이션 부트 실행 시점에 쿼리를 미리 생성하여 재사용할 수 있습니다.
- 데이터베이스 최적화: 데이터베이스는 동일한 쿼리를 받으면, 이전에 파싱된 쿼리를 재사용하여 성능을 높일 수 있습니다.
다만, 엔티티에 필드가 많아지면(20~30개 이상) 전체 필드 업데이트는 비효율적일 수 있습니다. 이 경우, 필요한 필드만 업데이트하는 동적 쿼리(@DynamicUpdate 사용)를 고려할 수 있습니다. 하지만 필드 수가 많은 엔티티는 설계에서 정규화가 부족했을 가능성이 높으므로 주의가 필요합니다.
플러시 (Flush)
플러시는 영속성 컨텍스트의 변경 내용을 DB에 반영하는 동작으로, JPQL을 실행할 때 자동으로 호출됩니다. 플러시 시점에 변경된 엔티티는 쓰기 지연 SQL 저장소에 등록되어 DB에 전달됩니다.
기본 키 생성 전략
JPA는 다양한 기본 키 생성 전략을 지원하여 데이터베이스 벤더의 특성에 맞게 최적화된 방식으로 키를 생성할 수 있습니다. 주요 전략은 다음과 같습니다:
- IDENTITY: 이 방식은 데이터베이스에 기본 키 생성을 맡기는 전략입니다. 엔티티가 데이터베이스에 저장되어야만 기본 키 값을 알 수 있기 때문에, JPA는 이를 위해 데이터베이스에 추가로 조회를 요청하게 됩니다. 또한, 엔티티가 영속 상태가 되려면 식별자가 반드시 필요하므로, 이 전략에서는 트랜잭션의 쓰기 지연 기능이 동작하지 않습니다.
- SEQUENCE: 이 방식은 데이터베이스의 시퀀스를 이용해 기본 키를 생성하는 전략입니다. 먼저 데이터베이스 시퀀스를 통해 식별자를 조회하고, 조회한 식별자를 엔티티에 할당한 후 영속성 컨텍스트에 저장합니다. 이후 트랜잭션이 커밋되며 플러시가 발생하면 엔티티가 데이터베이스에 저장됩니다. 또한, SequenceGenerator를 통해 시퀀스 값을 미리 할당함으로써 여러 JVM 환경에서도 키 충돌을 방지할 수 있습니다.
- TABLE: 키 생성을 위한 전용 테이블을 사용하여 시퀀스를 흉내내는 방식입니다.
- AUTO: DB 독립성을 보장하며, 개발 초기 단계에서 유용하게 활용할 수 있습니다.