JPA (Java Persistence API)는 자바 진영의 ORM(Object-Relational Mapping) 기술 표준입니다. ORM은 객체 지향 프로그래밍 언어와 관계형 데이터베이스 간의 데이터를 변환하는 기술입니다. 이를 통해 객체는 객체대로, 관계형 데이터베이스는 관계형 데이터베이스대로 각각의 특성을 유지하면서 데이터베이스 작업을 수행할 수 있습니다.
JPA (Java Persistence API)
- 정의: 자바 진영의 ORM 기술 표준.
- 주요 기능: 객체와 관계형 데이터베이스 간의 매핑을 통해 데이터 저장, 검색, 갱신, 삭제 등의 작업을 지원.
ORM (Object-Relational Mapping)
- 정의: 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 테이블 간의 매핑 기술.
- 목적: 객체 지향 설계와 관계형 데이터베이스 설계 간의 불일치를 해결하고, 데이터베이스 작업을 쉽게 할 수 있게 함.
- 특징:
- 객체 지향 설계: 객체는 객체대로 설계하여 유지.
- 관계형 데이터베이스 설계: 관계형 데이터베이스는 본래의 구조대로 설계하여 유지.
- 매핑: ORM 프레임워크가 객체와 테이블 간의 매핑을 담당.
JPA(Java Persistence API)는 ORM(Object-Relational Mapping) 기술로, 내부적으로 JDBC(Java Database Connectivity)를 사용하여 데이터베이스와 상호 작용합니다.
ORM은 내부적으로 JDBC를 사용: ORM 프레임워크는 내부적으로 JDBC를 사용하여 데이터베이스와 통신하지만, 개발자가 이를 직접 다룰 필요는 없습니다.
JPA 동작 - 저장
- MemberDAO에서 Entity Object를 데이터베이스에 저장하기 위해 JPA의 persist 메서드를 호출합니다.
- JPA는 엔티티를 분석하고, INSERT SQL 쿼리를 생성한 후, 내부적으로 JDBC API를 사용하여 데이터베이스와 통신합니다.
- JDBC를 통해 생성된 SQL 쿼리가 데이터베이스에 전달되고, 데이터베이스는 쿼리를 실행하여 데이터를 저장합니다.
- JPA는 객체 지향 프로그래밍과 관계형 데이터베이스 간의 패러다임 불일치를 해결하여 개발자가 보다 직관적이고 생산적으로 데이터베이스 작업을 할 수 있게 합니다.
JPA 동작 - 조회
- MemberDAO에서 특정 ID를 기반으로 엔티티 객체를 조회하기 위해 JPA의 find 메서드를 호출합니다.
- JPA는 엔티티를 분석하고, SELECT SQL 쿼리를 생성한 후, 내부적으로 JDBC API를 사용하여 데이터베이스와 통신합니다.
- JDBC를 통해 생성된 SQL 쿼리가 데이터베이스에 전달되고, 데이터베이스는 쿼리를 실행하여 결과를 반환합니다.
- JPA는 반환된 결과를 엔티티 객체에 매핑하고, 이를 MemberDAO에 반환합니다.
- JPA는 객체 지향 프로그래밍과 관계형 데이터베이스 간의 패러다임 불일치를 해결하여 개발자가 보다 직관적이고 생산적으로 데이터베이스 작업을 할 수 있게 합니다.
Spring Data JPA와 JPA는 서로 다릅니다. Spring Data JPA는 JPA(Java Persistence API)를 더 쉽게 사용할 수 있도록 도와주는 상위 레벨의 라이브러리입니다.
JPA (Java Persistence API)
- 정의: 자바 애플리케이션에서 객체와 관계형 데이터베이스 간의 매핑을 관리하는 표준 API입니다.
- 기능: 기본적인 CRUD(Create, Read, Update, Delete) 작업, 쿼리 생성, 트랜잭션 관리 등을 제공합니다.
- 주요 메서드:
- persist(): 엔티티 저장
- find(): 엔티티 조회
- remove(): 엔티티 삭제
- merge(): 엔티티 병합(수정)
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
// Create
Member member = new Member();
member.setName("John Doe");
em.persist(member);
// Read
Member foundMember = em.find(Member.class, memberId);
// Update
foundMember.setName("Jane Doe");
em.getTransaction().commit();
// Delete
em.getTransaction().begin();
em.remove(foundMember);
em.getTransaction().commit();
Spring Data JPA
- 정의: JPA를 더 쉽게 사용할 수 있도록 도와주는 Spring 프레임워크의 서브 프로젝트입니다. JPA를 기반으로 다양한 편의 기능을 제공합니다.
- 기능:
- Repository 인터페이스를 통해 기본적인 CRUD 작업을 자동으로 생성
- 메서드 이름을 기반으로 쿼리 생성
- 페이징 및 정렬 지원
- 다양한 데이터 접근 기술과의 통합
- 주요 인터페이스 및 클래스:
- JpaRepository<T, ID>: 기본 CRUD 및 페이징 기능을 제공하는 인터페이스
- CrudRepository<T, ID>: 기본 CRUD 기능을 제공하는 인터페이스
public interface MemberRepository extends JpaRepository<Member, Long> {
// 메서드 이름을 기반으로 쿼리 생성
List<Member> findByName(String name);
}
// 사용 예시
@Autowired
private MemberRepository memberRepository;
// Create or Update
Member member = new Member();
member.setName("John Doe");
memberRepository.save(member);
// Read
Optional<Member> foundMember = memberRepository.findById(memberId);
// Update
if (foundMember.isPresent()) {
Member memberToUpdate = foundMember.get();
memberToUpdate.setName("Jane Doe");
memberRepository.save(memberToUpdate);
}
// Delete
if (foundMember.isPresent()) {
memberRepository.delete(foundMember.get());
}
패러다임 불일치란 객체 지향 프로그래밍과 관계형 데이터베이스 간의 차이로 인해 발생하는 문제를 말합니다. JPA는 이러한 불일치를 해결하기 위해 다양한 기능을 제공합니다.
1. JPA와 상속
문제:
객체 지향 프로그래밍에서는 클래스 상속이 일반적입니다. 그러나 관계형 데이터베이스에서는 테이블 상속이라는 개념이 없습니다. 이를 해결하기 위해 JPA는 상속 매핑 전략을 제공합니다.
해결 방법:
JPA는 상속을 매핑하기 위해 세 가지 전략을 제공합니다:
- 단일 테이블 전략 (Single Table Strategy): 상속 구조의 모든 클래스를 하나의 테이블에 매핑합니다.
- 조인 전략 (Joined Strategy): 각 클래스에 대해 별도의 테이블을 생성하고, 상속 구조를 조인으로 매핑합니다.
- 구체 클래스 전략 (Table per Class Strategy): 각 구체 클래스에 대해 별도의 테이블을 생성합니다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
@Entity
public class Employee extends Person {
private String department;
}
2. JPA와 연관관계
문제:
객체 지향 프로그래밍에서는 객체 간의 연관관계를 필드로 표현하지만, 관계형 데이터베이스에서는 외래 키로 표현합니다. 이를 매핑하는 것이 필요합니다.
해결 방법:
JPA는 여러 가지 연관관계 매핑을 지원합니다:
- 일대일 (OneToOne)
- 일대다 (OneToMany)
- 다대일 (ManyToOne)
- 다대다 (ManyToMany)
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "customer_id")
private Customer customer;
}
3. JPA와 객체 그래프 탐색
문제:
객체 지향 프로그래밍에서는 연관된 객체를 통해 객체 그래프를 탐색할 수 있습니다. 그러나 관계형 데이터베이스에서는 필요한 데이터를 조회하기 위해 JOIN을 사용해야 합니다.
해결 방법:
JPA는 객체 그래프 탐색을 위해 지연 로딩(Lazy Loading)과 즉시 로딩(Eager Loading)을 지원합니다. 또한, JPQL(Java Persistence Query Language)을 사용하여 필요한 데이터를 효율적으로 조회할 수 있습니다.
- 지연 로딩 (Lazy Loading): 실제로 객체가 필요할 때 데이터를 조회합니다.
- 즉시 로딩 (Eager Loading): 엔티티를 조회할 때 연관된 데이터를 모두 조회합니다.
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "customer", fetch = FetchType.LAZY)
private List<Order> orders;
}
4. JPA와 비교하기
문제:
객체 지향 프로그래밍에서는 객체의 동등성을 비교할 때 equals와 hashCode 메서드를 사용합니다. 반면, 관계형 데이터베이스에서는 기본 키를 사용하여 레코드를 식별합니다.
해결 방법:
JPA는 엔티티의 동등성을 비교하기 위해 equals와 hashCode 메서드를 적절히 구현하도록 권장합니다. 엔티티의 식별자는 기본 키를 기반으로 비교하는 것이 일반적입니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Member member = (Member) o;
return Objects.equals(id, member.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
JPA의 성능 최적화 기능
1. 1차 캐시와 동일성(identity) 보장
2. 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
3. 지연 로딩(Lazy Loading)
JPA의 1차 캐시와 동일성 보장은 성능 최적화와 데이터 일관성 유지에 중요한 역할을 합니다. 이를 통해 동일한 트랜잭션 내에서 동일한 엔티티 인스턴스를 반환하여 데이터의 일관성을 유지하고, 데이터베이스 조회 횟수를 줄입니다.
1차 캐시와 동일성 보장
1. 같은 트랜잭션 안에서는 같은 엔티티를 반환 - 약간의 조회 성능 향상
JPA는 동일한 트랜잭션 내에서 동일한 엔티티를 여러 번 조회할 경우, 처음 조회한 엔티티를 1차 캐시에 저장하고, 이후에는 캐시된 엔티티를 반환합니다. 이렇게 하면 데이터베이스에 대한 불필요한 조회를 줄여 성능을 향상시킬 수 있습니다.
String memberId = "100";
Member m1 = jpa.find(Member.class, memberId); // 처음 조회 시 SQL 실행
Member m2 = jpa.find(Member.class, memberId); // 두 번째 조회 시 캐시에서 반환
System.out.println(m1 == m2); // true, 동일한 인스턴스를 반환
// SQL은 처음 조회 시 1번만 실행됨
2. DB Isolation Level이 Read Committed이어도 애플리케이션에서 Repeatable Read 보장
1차 캐시는 트랜잭션 내에서 조회된 엔티티를 캐시함으로써, 데이터베이스의 격리 수준(Isolation Level)이 Read Committed인 경우에도 애플리케이션에서 Repeatable Read 수준의 일관성을 보장합니다. 이는 동일한 트랜잭션 내에서 동일한 엔티티에 대한 반복 조회 시, 항상 동일한 데이터를 반환함을 의미합니다.
- Read Committed: 각 트랜잭션은 커밋된 데이터만 읽을 수 있습니다.
- Repeatable Read: 트랜잭션 내에서 동일한 쿼리를 실행할 때 항상 동일한 결과를 반환합니다.
JPA의 1차 캐시는 트랜잭션 내에서 엔티티의 일관성을 유지하여, 트랜잭션이 종료될 때까지 동일한 엔티티에 대해 동일한 상태를 유지합니다.
EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();
String memberId = "100";
// 처음 조회 시 데이터베이스에서 조회하고, 1차 캐시에 저장
Member m1 = em.find(Member.class, memberId); // SQL 실행
// 두 번째 조회 시 1차 캐시에서 반환
Member m2 = em.find(Member.class, memberId); // 캐시에서 반환
// 동일한 인스턴스를 참조함을 확인
System.out.println(m1 == m2); // true
em.getTransaction().commit();
em.close();
트랜잭션을 지원하는 쓰기 지연 - INSERT
1. 트랜잭션을 커밋할 때까지 INSERT SQL을 모음
트랜잭션이 시작된 후, 엔티티 매니저는 persist 메서드를 호출하여 엔티티를 데이터베이스에 저장하려고 할 때 즉시 SQL을 실행하지 않고, 트랜잭션이 커밋될 때까지 INSERT SQL을 내부적으로 모읍니다. 이를 통해 여러 INSERT 작업을 한꺼번에 처리할 수 있습니다.
2. JDBC BATCH SQL 기능을 사용해서 한번에 SQL 전송
트랜잭션이 커밋될 때, 모아둔 INSERT SQL을 한꺼번에 데이터베이스에 전송합니다. 이 과정에서 JDBC의 배치(Batch) 기능을 사용하여 다수의 SQL 문을 한 번의 네트워크 통신으로 실행합니다. 이는 성능을 크게 향상시킬 수 있습니다.
EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않음
transaction.commit(); // [트랜잭션] 커밋 - 커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보냄
트랜잭션을 지원하는 쓰기 지연 - UPDATE/DELETE
1. UPDATE, DELETE로 인한 로우(ROW)락 시간 최소화
트랜잭션 내에서 엔티티를 변경하거나 삭제하는 작업을 할 때, 즉시 SQL을 실행하지 않고, 트랜잭션이 커밋될 때까지 SQL 실행을 지연시킵니다. 이를 통해 데이터베이스의 로우(ROW) 락을 최소화할 수 있습니다. 이는 비즈니스 로직을 수행하는 동안 데이터베이스 로우가 락에 의해 차단되지 않도록 합니다.
2. 트랜잭션 커밋 시 UPDATE, DELETE SQL 실행하고, 바로 커밋
트랜잭션이 커밋될 때, 모아둔 UPDATE, DELETE SQL을 실행합니다. 이렇게 하면 변경 사항이 즉시 데이터베이스에 반영되고, 락 시간이 최소화됩니다.
EntityManager em = entityManagerFactory.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin(); // [트랜잭션] 시작
// 엔티티 변경 및 삭제
changeMember(memberA);
deleteMember(memberB);
비즈니스_로직_수행(); // 비즈니스 로직 수행 동안 DB 로우 락이 걸리지 않음
// 트랜잭션 커밋 시점에 UPDATE, DELETE SQL을 실행하고 바로 커밋
transaction.commit(); // [트랜잭션] 커밋
JPA의 트랜잭션을 지원하는 쓰기 지연 기능은 영속성 컨텍스트(Persistence Context)에 의해 관리됩니다. 영속성 컨텍스트는 엔티티 매니저(Entity Manager)에 의해 생성되고 관리되는 캐시와 같은 역할을 합니다. 이 컨텍스트는 트랜잭션 동안 엔티티 객체의 상태를 유지하고, 트랜잭션이 커밋될 때까지 데이터베이스와의 상호작용을 지연시킵니다. 이를 통해 여러 가지 성능 최적화와 데이터 일관성을 보장합니다.
JPA의 지연 로딩(Lazy Loading)과 즉시 로딩(Eager Loading)은 엔티티를 조회할 때 연관된 엔티티를 어떻게 로드할지를 결정하는 중요한 개념입니다. 이 두 로딩 전략은 성능 최적화와 데이터베이스 상호작용을 관리하는 데 중요한 역할을 합니다.
지연 로딩 (Lazy Loading)
정의
지연 로딩은 연관된 엔티티를 실제로 사용할 때까지 데이터베이스에서 로드하지 않는 전략입니다. 이는 필요하지 않은 데이터를 미리 로드하지 않음으로써 초기 로딩 시간을 단축하고 메모리 사용량을 줄일 수 있습니다.
동작 방식
- 연관된 엔티티 접근 시 로딩: 연관된 엔티티가 처음 접근될 때 데이터베이스에서 조회됩니다.
- 프록시 객체 사용: JPA는 실제 엔티티 대신 프록시 객체를 반환하여 지연 로딩을 구현합니다. 프록시 객체는 실제로 사용될 때 데이터베이스에서 데이터를 가져옵니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
}
// 사용 예시
Member member = memberDAO.find(memberId); // MEMBER 조회 (SELECT * FROM MEMBER)
Team team = member.getTeam(); // TEAM 조회 (SELECT * FROM TEAM) -> 여기서 실제로 데이터베이스 조회
String teamName = team.getName(); // teamName 사용
프록시 객체란?
프록시 객체는 실제 엔티티 객체를 대신하여 중간에서 역할을 하는 객체입니다. 프록시 객체는 실제 엔티티가 로드될 때까지 데이터를 로드하지 않고, 실제 데이터가 필요할 때(즉, 해당 엔티티에 접근할 때) 데이터베이스에서 데이터를 로드합니다.
프록시의 역할
- 중간 역할: 프록시 객체는 실제 엔티티를 대신하여 중간에서 데이터 로딩을 지연시킵니다.
- 데이터베이스 조회 지연: 프록시 객체는 실제로 데이터가 필요할 때까지 데이터베이스 조회를 지연시킵니다.
즉시 로딩 (Eager Loading)
정의
즉시 로딩은 엔티티를 조회할 때 연관된 엔티티도 함께 로드하는 전략입니다. 이는 JOIN SQL을 사용하여 한번에 필요한 모든 데이터를 조회합니다.
동작 방식
- JOIN을 사용한 즉시 로딩: 주 엔티티를 조회할 때 연관된 엔티티도 함께 JOIN하여 조회합니다.
- 즉시 데이터 로드: 연관된 모든 데이터를 한꺼번에 로드하여 추가적인 데이터베이스 조회를 피합니다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne(fetch = FetchType.EAGER)
@JoinColumn(name = "team_id")
private Team team;
}
// 사용 예시
Member member = memberDAO.find(memberId); // MEMBER와 TEAM을 JOIN하여 조회 (SELECT M.*, T.* FROM MEMBER M JOIN TEAM T ON M.team_id = T.id)
Team team = member.getTeam(); // 이미 로드된 team
String teamName = team.getName(); // teamName 사용
- 지연 로딩: 실제로 필요할 때 데이터를 로드하여 초기 로딩 시간을 단축하고 메모리 사용량을 줄입니다. 연관된 엔티티를 접근할 때마다 데이터베이스 조회가 발생합니다.
- 즉시 로딩: 한 번의 JOIN 쿼리로 모든 연관된 데이터를 함께 로드하여 추가적인 데이터베이스 조회를 피합니다. 불필요한 데이터까지 로드될 수 있어 메모리 사용량이 증가할 수 있습니다.
'JPA' 카테고리의 다른 글
[JPA] JPA 연관관계 매핑 (2) (0) | 2024.06.29 |
---|---|
[JPA] JPA 연관관계 매핑 (1) (0) | 2024.06.29 |
[JPA] JPA 엔티티 매핑 (0) | 2024.06.29 |
[JPA] JPA 영속성 관리 (0) | 2024.06.29 |
[JPA] JPA 생성 및 개발 (0) | 2024.06.29 |