JPA 간단 정리
더보기
JPA 요약 정리
1. JPA란?
- Java Persistence API: 자바 애플리케이션과 데이터베이스 간 매핑 및 상호작용을 위한 표준 인터페이스.
- 주요 역할:
- 자바 객체와 데이터베이스 테이블 간 자동 매핑.
- SQL 작성 없이 객체 중심으로 데이터 처리.
2. JPA를 사용하는 이유
이유 설명SQL 대신 자바 코드로 작업 | 복잡한 SQL 대신 간단한 자바 코드로 데이터 저장 및 조회 가능. |
객체 지향적 데이터 처리 | 데이터베이스 테이블을 객체로 변환하여 컬렉션처럼 다룰 수 있음. |
유지보수성 향상 | 데이터베이스 구조 변경 시 엔티티 수정만으로 코드 유지보수 가능. |
데이터베이스 독립성 | 특정 데이터베이스(MySQL, Oracle 등)에 종속되지 않음. |
비즈니스 로직에 집중 가능 | CRUD 작업 자동화로 반복적인 SQL 작업 제거. |
3. JPA에서의 연관관계
연관관계 설명 예제1 : 1 | 한 테이블의 행이 다른 테이블의 하나의 행과만 연결됨. | 사용자(User) ↔ 사용자 상세(UserDetail) |
1 : N | 한 테이블의 행이 다른 테이블의 여러 행과 연결됨. | 회원(Member) ↔ 주문(Order) |
N : M | 한 테이블의 여러 행이 다른 테이블의 여러 행과 연결됨. | 학생(Student) ↔ 강의(Course) (중간 테이블 필요) |
4. Fetch Type (로딩 전략)
Fetch Type 특징 기본값Lazy | 연관 데이터를 실제로 사용하는 시점에 조회. | @OneToMany, @ManyToMany |
Eager | 엔티티 조회 시 연관 데이터를 즉시 조회. | @ManyToOne, @OneToOne |
5. JPA 코드 예제
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY) // Lazy 로딩
private List<Order> orders = new ArrayList<>();
}
@Entity
@Getter
@Setter
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String product;
@ManyToOne(fetch = FetchType.EAGER) // Eager 로딩
@JoinColumn(name = "member_id")
private Member member;
}
요약
JPA의 정의 | 자바 애플리케이션과 데이터베이스 간 매핑 및 상호작용을 위한 표준 인터페이스. |
사용 이유 | SQL 없이 자바 코드로 데이터 처리, 유지보수 용이, 데이터베이스 독립성 보장. |
연관관계 | 1:1, 1:N, N:M 관계를 객체로 매핑 가능. |
Fetch Type | 연관 데이터 로딩 시점을 제어하는 전략. Lazy(지연 로딩), Eager(즉시 로딩). |
장점
- 코드 간결화: 객체 중심 개발로 SQL 관리 부담 감소.
- 생산성 향상: 반복 작업 제거로 개발 속도 증가.
- 데이터베이스 독립성: 특정 DBMS에 종속되지 않음.
N + 1 문제란 무엇인가
1. N + 1 문제란?
- 정의:
- 데이터베이스에서 부모-자식 관계 데이터를 가져올 때, 비효율적으로 많은 쿼리가 실행되는 문제.
- 주로 지연 로딩(FetchType.LAZY)을 사용하는 상황에서 발생.
- N + 1의 의미:
- 1: 부모 데이터를 조회하는 1개의 쿼리.
- N: 부모마다 자식 데이터를 조회하기 위한 N개의 쿼리.
2. N + 1 문제 예시
상황 설명
- 데이터베이스에 부모 테이블(Parent)과 자식 테이블(Child)이 있고, 한 부모는 여러 자식을 가질 수 있음.
- 목표: 모든 부모와 관련된 자식 데이터를 조회.
테이블 구조
- Parent 테이블
id | name |
1 | 엄마 |
2 | 아빠 |
- Child 테이블
id | parent_id | name |
1 | 1 | 딸 |
2 | 1 | 아들 |
3 | 2 | 막내딸 |
1. 기본 쿼리 흐름
-- 부모 데이터를 조회 (1번 실행)
SELECT * FROM parent;
-- 부모 각각에 대해 자식 데이터를 조회 (N번 실행)
SELECT * FROM child WHERE parent_id = 1;
SELECT * FROM child WHERE parent_id = 2;
2. 실행 결과
- 부모가 100명이라면:
- 1번 부모 조회 쿼리 실행.
- 100번 자식 조회 쿼리 실행.
- 총 101번의 쿼리 실행.
3. N + 1 문제 확인 방법
Spring Boot에서 SQL 로그 활성화
spring:
jpa:
properties:
hibernate:
show_sql: true # 실행 쿼리 표시
format_sql: true # 쿼리 포맷팅
SQL 로그 예시
-- 부모 조회
SELECT * FROM parent;
-- 부모별 자식 데이터 조회
SELECT * FROM child WHERE parent_id = 1;
SELECT * FROM child WHERE parent_id = 2;
4. 왜 N + 1 문제가 문제인가?
문제점 | 설명 |
성능 저하 | 필요 이상으로 많은 쿼리가 실행되어 애플리케이션이 느려짐. |
데이터베이스 부하 증가 | 트래픽이 많은 환경에서는 데이터베이스에 큰 부담을 줄 수 있음. |
비효율적인 리소스 사용 | 동일한 데이터를 반복적으로 조회하므로 CPU, 메모리 등의 리소스가 낭비됨. |
즉시 로딩 & 지연 로딩
더보기
1. 즉시 로딩 (Eager Loading)
- 정의:
- 엔티티를 조회할 때 연관된 엔티티를 즉시 함께 조회하는 로딩 방식.
- 연관 데이터를 미리 가져와서 사용할 준비를 함.
- 특징:
- FetchType.EAGER를 설정하면 즉시 로딩이 적용됨.
- 부모 엔티티와 자식 엔티티가 JOIN 쿼리로 한 번에 조회됨.
- 모든 연관 데이터를 항상 가져오므로 필요하지 않은 데이터까지 로드될 수 있음.
- 장점:
- 데이터를 미리 가져오므로 추가적인 데이터베이스 쿼리가 발생하지 않음.
- 애플리케이션에서 데이터를 바로 사용할 수 있어 편리.
- 단점:
- 불필요한 데이터까지 로드될 수 있어 성능 저하가 발생.
- 연관된 데이터가 많거나 관계가 깊을 경우 쿼리가 복잡해지고 메모리 사용량 증가.
즉시 로딩 예제
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) // 즉시 로딩 설정
private List<Child> children;
}
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
// 실행
List<Parent> parents = parentRepository.findAll(); // 부모와 자식을 함께 로드
실행 쿼리
SELECT p.*, c.*
FROM parent p
LEFT JOIN child c
ON p.id = c.parent_id; -- 부모와 자식 데이터를 한 번의 쿼리로 로드
2. 지연 로딩 (Lazy Loading)
- 정의:
- 엔티티를 조회할 때 연관된 엔티티를 필요한 시점에 조회하는 로딩 방식.
- 데이터를 사용할 때 별도의 쿼리가 실행되어 로드됨.
- 특징:
- FetchType.LAZY를 설정하면 지연 로딩이 적용됨.
- 부모 엔티티를 조회할 때 자식 엔티티는 바로 로드되지 않고, Proxy 객체로 대체.
- 자식 데이터가 실제로 필요할 때 데이터베이스 쿼리가 실행.
- 장점:
- 필요한 데이터만 로드하므로 메모리 사용량 감소.
- 연관 데이터가 많거나 사용 빈도가 낮은 경우 성능 최적화 가능.
- 단점:
- 데이터가 필요할 때마다 추가적인 쿼리 발생.
- 여러 연관 데이터가 필요한 경우 N+1 문제가 발생할 수 있음.
지연 로딩 예제
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY) // 지연 로딩 설정
private List<Child> children;
}
@Entity
public class Child {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
// 실행
List<Parent> parents = parentRepository.findAll(); // 부모만 로드
for (Parent parent : parents) {
System.out.println(parent.getChildren()); // 자식 데이터를 사용할 때 쿼리 실행
}
실행 쿼리
- 부모 조회:
SELECT * FROM parent;
- 자식 조회 (사용 시):
SELECT * FROM child WHERE parent_id = 1; SELECT * FROM child WHERE parent_id = 2;
3. 즉시 로딩 vs 지연 로딩 비교
즉시 로딩 (Eager Loading) | 지연 로딩 (Lazy Loading) | |
데이터 로드 시점 | 부모 엔티티를 조회할 때, 연관 데이터도 함께 로드. | 연관 데이터는 실제로 접근할 때 로드. |
쿼리 실행 방식 | 부모와 자식을 JOIN 쿼리로 한 번에 조회. | 부모를 조회 후, 연관 데이터는 개별 쿼리로 조회. |
성능 | 적은 쿼리로 모든 데이터를 가져오지만, 불필요한 데이터까지 로드. | 필요한 데이터만 가져오므로 메모리 사용량 감소. |
N+1 문제 발생 여부 | 없음. | 연관 데이터 사용 시 N+1 문제 발생 가능. |
적합한 상황 | - 자식 데이터가 항상 필요한 경우- 간단한 관계 | - 자식 데이터가 드물게 필요- 대량 데이터 처리 시 |
설정 방식 | fetch = FetchType.EAGER | fetch = FetchType.LAZY |
4. 결론
- 즉시 로딩 (Eager):
- 연관 데이터를 항상 함께 로드해야 하는 경우 사용.
- 간단한 관계에 적합하지만, 관계가 복잡하거나 대량 데이터를 처리할 때는 성능 저하 가능.
- 지연 로딩 (Lazy):
- 연관 데이터를 사용할 일이 적거나, 데이터가 많아 메모리 효율성을 고려해야 하는 경우 적합.
- N+1 문제가 발생할 수 있으므로 Fetch Join, Batch Size 등 최적화 방법을 함께 적용해야 함.
N + 1 문제 해결 방법
5.1 FetchType.EAGER로 변경
해결 원리
- 부모 데이터를 불러올 시에 자식 데이터도 한번에 다 '즉시' 전부 불러옴, 기존의 지연 로딩의 경우 필요한 경우에 불러오기 때문에 데이터를 다시 요청해야하지만, 즉시 로딩의 경우 이미 데이터를 다 불러왔기에 더이상 불러올 필요가 없다.
더보기
FetchType.EAGER로 N+1 문제 해결이 되는 이유
1. N+1 문제의 원인
- FetchType.LAZY (지연 로딩):
- 부모 데이터를 조회한 후, 각 부모의 자식 데이터를 별도의 쿼리로 가져옴.
- 즉, 부모 1번 조회 + 자식 N번 조회 → N+1 쿼리 발생.
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY) // 기본 설정 private List<Child> children;
- 실행 쿼리 예시:
SELECT * FROM parent; -- 부모 데이터 조회 (1회) SELECT * FROM child WHERE parent_id = 1; -- 부모 1번 자식 조회 (1회) SELECT * FROM child WHERE parent_id = 2; -- 부모 2번 자식 조회 (1회) -- 부모 N개일 경우 총 N+1번 쿼리 실행
2. FetchType.EAGER가 작동하는 방식
- FetchType.EAGER (즉시 로딩):
- 부모 데이터를 조회할 때 연관된 자식 데이터를 함께 조회.
- 부모와 자식 데이터를 한 번의 JOIN 쿼리로 가져오므로 쿼리 실행 횟수가 줄어듦.
@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) // 즉시 로딩 설정 private List<Child> children;
- 실행 쿼리 예시:
- SELECT p.*, c.* FROM parent p LEFT JOIN child c ON p.id = c.parent_id; -- 부모와 자식을 JOIN하여 한 번에 조회
- 결과:
- 부모와 자식 데이터를 한 번에 가져오기 때문에 N+1 문제가 발생하지 않음.
3. 해결 원리
- Lazy vs Eager의 차이:
- Lazy 로딩은 데이터를 필요로 하는 시점에 별도의 쿼리를 실행하여 자식 데이터를 가져옴 → N+1 문제 발생.
- Eager 로딩은 부모 데이터를 조회하는 시점에 연관된 자식 데이터를 미리 가져옴 → 쿼리 실행 횟수 감소.
- 쿼리 실행 구조:
- Lazy: 부모를 조회한 후, 자식 데이터를 부모 개수만큼 별도로 조회 → 여러 번의 쿼리 발생.
- Eager: 부모와 자식을 JOIN하여 한 번의 쿼리로 데이터 조회 → 쿼리 횟수 감소.
4. FetchType.EAGER의 특징
즉시 로딩 | 부모 데이터를 조회할 때, 자식 데이터를 함께 가져옴. |
쿼리 최적화 | 부모-자식 관계를 JOIN으로 처리해 쿼리 횟수를 최소화. |
간단한 설정 | 엔티티에서 fetch = FetchType.EAGER 설정으로 간편하게 적용. |
5. 예제 코드와 쿼리 차이
Lazy 로딩
@OneToMany(mappedBy = "parent", fetch = FetchType.LAZY)
private List<Child> children;
- 실행 쿼리:
SELECT * FROM parent; -- 부모 데이터 조회 SELECT * FROM child WHERE parent_id = 1; -- 부모 1번의 자식 데이터 조회 SELECT * FROM child WHERE parent_id = 2; -- 부모 2번의 자식 데이터 조회
Eager 로딩
@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER)
private List<Child> children;
- 실행 쿼리:
SELECT p.*, c.* FROM parent p LEFT JOIN child c ON p.id = c.parent_id; -- 부모와 자식 데이터를 한 번에 조회
6. FetchType.EAGER가 N+1 문제를 해결하는 이유
- Lazy 로딩:
- 부모 데이터를 먼저 조회하고, 자식 데이터를 필요로 할 때마다 별도 쿼리 실행 → N+1 쿼리 발생.
- Eager 로딩:
- 부모와 자식 데이터를 한 번의 JOIN 쿼리로 가져오기 때문에, 추가 쿼리 실행 없이 모든 데이터를 한 번에 로드 → N+1 문제 해결.
7. EAGER의 한계
- 불필요한 데이터 로드:
- 항상 연관 데이터를 즉시 로드하므로, 필요 없는 데이터도 로드될 가능성.
- 메모리 사용량 증가:
- 대량의 데이터가 JOIN으로 로드되면 메모리 사용량이 증가할 수 있음.
- 복잡한 관계에서 성능 저하:
- JOIN이 여러 테이블에 걸쳐 발생하면 쿼리 성능이 저하될 가능성.
8. 결론
- FetchType.EAGER는 부모와 자식 데이터를 한 번의 JOIN 쿼리로 로드하여 N+1 문제를 해결.
- 하지만 항상 즉시 로드되기 때문에 불필요한 데이터까지 가져올 가능성이 있어, Fetch Join이나 @EntityGraph 같은 대안도 상황에 따라 활용해야 함.
추가적으로 궁금한 점이 있으면 말씀해주세요! 😊
적용 방법
- 기본적으로 Lazy로 설정된 관계를 EAGER로 변경.
@Entity
public class Parent {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) // 즉시 로딩
private List<Child> children;
}
실행 쿼리
-- 부모와 자식 데이터를 즉시 조회
SELECT p.*, c.*
FROM parent p
LEFT JOIN child c
ON p.id = c.parent_id;
장점
- 쿼리 횟수가 감소하여 성능 개선.
단점
- 필요하지 않은 데이터까지 즉시 조회되어 메모리 사용량 증가.
5.2 Fetch Join 사용
해결 원리
- 애초에 연관된 일부의 데이터만 미리 설정해 즉시 한번에 불러옴
적용 방법
- JPQL에서 JOIN FETCH를 사용하여 연관 데이터를 함께 조회.
@Query("SELECT p FROM Parent p JOIN FETCH p.children")
List<Parent> findAllWithChildren();
실행 쿼리
SELECT p.*, c.*
FROM parent p
LEFT JOIN child c
ON p.id = c.parent_id;
장점
- N + 1 문제 해결.
- 필요한 데이터만 효율적으로 조회.
5.3 @EntityGraph 사용
해결 원리
- JPA에서 Fetch Join과 유사한 방식으로 작동하여 N+1 문제를 해결
- 엔티티 간의 연관 관계를 명시적으로 지정해 즉시 로딩(Eager)을 적용.
적용 방법
- JPA의 **@EntityGraph**를 활용하여 연관 데이터를 Fetch Join 방식으로 조회.
@EntityGraph(attributePaths = {"children"})
@Query("SELECT p FROM Parent p")
List<Parent> findAllWithChildren();
장점
- Fetch Join과 동일한 효과를 제공.
- 코드 재사용성이 높음.
5.4 Batch Size 설정
해결 원리
- 지연 로딩(Lazy Loading)을 유지하면서 데이터를 묶어서 한 번에 조회하도록 최적화.
- JPQL을 재작성할 필요 없이 애노테이션으로 간단히 설정 가능.
적용 방법
- Lazy 로딩을 유지하면서 여러 건의 데이터를 한 번에 가져오도록 Batch Size 설정.
Spring 설정
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 10
실행 쿼리
-- 부모 조회
SELECT * FROM parent;
-- 자식 데이터 조회 (10건씩 묶어서 실행)
SELECT * FROM child WHERE parent_id IN (1, 2, 3, ..., 10);
장점
- Lazy 로딩 유지.
- 데이터 조회 최적화.
6. 해결 방법 비교
N + 1 문제 해결 방법 비교 정리
특징 | FetchType.EAGER | Fetch Join | EntityGraph | Batch Size |
쿼리 실행 횟수 | 1회 + N회 (부모 1회, 자식 N회) | 1회 | 1회 | N / Batch Size (Batch Size만큼 묶어서 조회) |
로딩 방법 | Eager (즉시 로딩) | Eager (즉시 로딩) | Eager (즉시 로딩) | Lazy (지연 로딩) |
설정 난이도 | 간단 (예: fetch = FetchType.EAGER) | 쿼리 재작성 필요 | 간단 (예: @EntityGraph 애노테이션 사용) | 간단 (전역 설정 또는 개별 설정 가능) |
유연성 | 낮음 (항상 Eager 로딩) | 중간 (필요한 경우에만 Fetch Join 적용 가능) | 중간 (필요한 엔티티에만 적용 가능) | 높음 (Lazy 로딩 유지, 필요 데이터만 로드 가능) |
주요 단점 | 불필요한 데이터 항상 로드, 관계가 깊으면 성능 저하 | 복잡한 관계나 대량 데이터에서 성능 저하 | 복잡한 관계나 대량 데이터에서 성능 저하 | IN 절의 비효율성, Lazy로 인해 예측 불가 쿼리 발생 |
적합한 상황 | 간단한 관계 데이터, 자주 참조되는 연관 데이터 | 관계가 간단하고 즉시 로드가 필요한 경우 | 관계가 단순하고 재사용성이 중요한 경우 | 대량 데이터 조회 시 Lazy 로딩 유지가 필요한 경우 |
각 방법에 대한 설명
1. FetchType.EAGER
- 설명:
- 연관된 데이터를 즉시 로드.
- 간단한 설정으로 사용 가능.
- 적합한 상황:
- 연관 데이터가 많지 않고, 자주 참조되는 경우.
- 한계:
- 항상 데이터를 로드하므로 불필요한 성능 저하 가능.
2. Fetch Join
- 설명:
- JPQL의 JOIN FETCH를 사용해 연관 데이터를 함께 조회.
- 쿼리 실행 횟수를 최소화.
- 적합한 상황:
- 관계가 단순하고, 대량 데이터가 아닌 경우.
- 한계:
- 복잡한 관계에서는 쿼리 성능이 저하될 수 있음.
3. EntityGraph
- 설명:
- JPA의 @EntityGraph를 사용해 Fetch Join을 간편하게 적용.
- 쿼리 동작 방식을 코드와 분리하여 재사용성을 높임.
- 적합한 상황:
- 관계가 단순하고, 코드 재사용성을 높이고 싶은 경우.
- 한계:
- 복잡한 데이터 관계에서는 성능 저하 가능.
4. Batch Size
- 설명:
- Lazy 로딩 유지하면서 연관 데이터를 묶어서 한 번에 조회.
- 전역 설정 또는 개별 설정 가능.
- 적합한 상황:
- 대량 데이터 조회 시 Lazy 로딩 유지가 필요한 경우.
- 한계:
- IN 절의 비효율성과 예측 불가능한 쿼리 발생 가능.
결론
- FetchType.EAGER: 간단한 관계에서 데이터가 항상 필요할 경우 적합.
- Fetch Join: 즉시 로드가 필요하고 관계가 간단한 경우.
- EntityGraph: Fetch Join과 유사하지만 재사용성을 고려할 때 유용.
- Batch Size: Lazy 로딩을 유지하면서 대량 데이터를 효율적으로 처리하고 싶을 때.
7. 결론
- 상황에 맞는 해결 방법을 선택:
- 데이터가 많지 않은 경우: FetchType.EAGER.
- 특정 쿼리 최적화: Fetch Join or @EntityGraph.
- 대량의 Lazy 로딩 데이터 최적화: Batch Size.
'DB > JPA ( Java Persistence API )' 카테고리의 다른 글
[JPA] Lock (동시성) (1) | 2025.02.24 |
---|---|
[JPA] SpringData JPA 심화 (0) | 2025.01.28 |
[JPA] 테이블 객체 (0) | 2025.01.27 |
[JPA] 쿼리 파일 만들기 (QueryMapper) (0) | 2025.01.26 |
[JPA] 데이터베이스 연결 (Driver) (1) | 2025.01.25 |