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(즉시 로딩).

장점

  1. 코드 간결화: 객체 중심 개발로 SQL 관리 부담 감소.
  2. 생산성 향상: 반복 작업 제거로 개발 속도 증가.
  3. 데이터베이스 독립성: 특정 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()); // 자식 데이터를 사용할 때 쿼리 실행
}

실행 쿼리

  1. 부모 조회:
    SELECT * FROM parent;
    
  2. 자식 조회 (사용 시):
    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

+ Recent posts