궁금했던 점

상황 주문 로직을 위해 "주문 테이블(Order)"과 "상품 테이블(Product)"를 구성했다.
어떤 주문에 어떤 상품이 담기는지를 구성하다보니 N:M구조가 되어,
중간 테이블 "주문 매핑 상품 테이블(Order_mapping)"을 중간에 두었다.


Order와 Order_mapping은 식별관계이고,Product과 Order_mapping 또한 식별관계이다.

식별관계인 테이블이 있다고 가정한 상황에서
Order_mapping의 PK가 복합키로 존재한다면 DB관리에 불편함이 있었다.
단일 PK로 레코드를 고유 식별가능하도록 PK를 추가한다면 여전히 식별관계인가?

NO => 비식별 관계이다.


구분 기준

두 테이블간의 관계가 자식 테이블의 기본 키(PK)에 영향을 미치는지에 따라 구분


 

이 글에서
부모 테이블의 기본키 = Parent PK
자식 테이블의 기본키 = Child PK
로 명명하겠습니다.

1. 식별 관계 (Identifying Relationship)

정의

  • Parent PKChild PK에 포함되는 관계
  • Child PKParent PK와 자식 자신의 고유 속성으로 구성됨
    • Child PK = { Parent PK, 자신(Child)의 고유 속성 }

특징

  1. Parent PK를 반드시 포함
    • Child PKParent PK를 포함해야 함.
    • 즉, 부모의 존재가 자식을 "식별"하는데 필수적이다.
  2. 강한 의존성
    • 자식 테이블은 부모 테이블에 강하게 의존하며,
      부모 없이는 자식의 존재가 정의되지 않음.
  3. ERD 표현
    • ERD에서 굵은 선으로 표현 됨

예제

  • 부모 : Order (주문 테이블)
  • 자식 : OrderItem (주문 상세)
Order (부모 테이블)
------------------------
| order_id (PK)        |
------------------------

Order_maaping (자식 테이블)
------------------------
| order_id (PK, FK)    |
| product_id (PK)      |
| quantity             |
------------------------
  • 설명
    • OrderItemorderId는 부모 Order의 PK에서 상속받았으며
      OrderItem의 PK를 구성한다.
    • OrderItemOrder가 존재하지 않으면 식별할 수 없다.

2. 비식별 관계 (Non-Identifying Relationship)

정의

  • Parent PKChild PK에 포함되지 않는 관계
  • Parent PKChild PK로만 존재하며, 자식의 PK에는 영향을 미치지 않는다.

특징

  1. Parent PK를 참조만 함
    • Parent PK는 자식 테이블의 FK로만 사용되며,
      Child PK를 구성하지 않는다.
  2. 약한 의존성
    • 자식 테이블은 부모 테이블에 약하게 의존하며,
      부모 없이도 고유하게 식별될 수 있다.
  3. ERD 표현
    • ERD에서 점선으로 표현 됨

예제

  • 부모 : Order (주문 테이블)
  • 자식 : OrderItem (주문 상세)
Order (부모 테이블)
------------------------
| order_id (PK)        |
------------------------

Order_maaping (자식 테이블)
------------------------
| order_maaping_id (PK)   |  -- 자식 테이블의 고유 PK
| order_id (FK)        |  -- 부모 테이블의 FK
| product_id           |
------------------------
  • 설명
    • order_item_id가 자식 테이블 OrderItem에서 고유 식별자로 사용된다.
    • order_id는 단순희 부모 테이블을 참조하는 자식 테이블의 외래 키로 존재
    • 부모의 존재 여부와 상관없이, 자식 테이블의 order_item_id만으로 레코드를 고유하게 식별가능

정리

특징 식별 관계 비식별 관게
자식 PK에 부모 PK 포함 여부 포함 미포함
의존성 부모 없이 존재 불가 부모 없이 존재 가능
ERD표현 굵은 실선 점선
테이블 설계 자식 테이블의 PK가 복합 키로 구성됨 자식 테이블의 PK는 독립적인 단일키 가능
사용 사례 강한 의존 관계 (예: 주문과 주문 상세) 약한 의존 관계 (예: 고객과 주문)

 

Child PK가 복합키로 (Parent PK, 자식 테이블의 속성)으로 구성되어 있다면 -> 식별 관계

Child PK가 단일키로 고유 식별자 역할을 한다면 -> 비식별 관계


 

반응형

'Computer Science > Database' 카테고리의 다른 글

[Database] N+1 문제  (0) 2024.11.29

N+1 문제 ?

  • 데이터베이스 쿼리 최적화와 관련된 성능문제
  • 주로 ORM(Object-Relational Mapping) 도구(JPA, Hibernate 등)를 사용할 때 발생되며,
    한 번의 데이터 조회로 충분한 정보를 가져올 수 있음에도 N개의 추가적인 쿼리가 실행되는 비효율적인 상황
  • 1번의 메인 쿼리 + N번의 추가 쿼리 = 총 N + 1번의 쿼리
    • 성능 저하
      • 데이터가 많을수록 데이터베이스에 과도한 부하 발생
      • 특히 대량의 데이터를 조회할 때 성능 문제가 크개 발생
    • 네트워크 지연
      • 데이터베이스와 애플리케이션 간 통신량이 증가하여 요청 처리 시간이 늘어남

N+1 문제의 동작 원리

  • 쿼리 실행 과정
    1. 먼저 1개의 메인 쿼리로 데이터 리스트를 가져옵니다.
    2. 가져온 데이터 각각에 대해 추가로 1개의 쿼리가 실행되어,
      결과적으로 N개의 추가 쿼리가 발생합니다.

N+1 문제 예제

  • Post : Comment = 1 : N
@Entity
public class Post {
    @Id @GeneratedValue
    private Long id;

    private String title;

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    private List<Comment> comments = new ArrayList<>();
}

@Entity
public class Comment {
    @Id @GeneratedValue
    private Long id;

    private String content;

    @ManyToOne(fetch = FetchType.LAZY)
    private Post post;
}
  • 문제 상황 : Post 리스트와 각 Post의 Comment를 조회
    • Post가 100개라면 : 1 + 100 = 101번 쿼리가 수행된다.
List<Post> posts = postRepository.findAll(); // 전체 조회를 위한 findAll() 호출 : SELECT * FROM post;

for (Post post : posts) {
    System.out.println(post.getTitle());
    for (Comment comment : post.getComments()) { 
        // 각 Post에 대한 getComments() 호출 = 각 Post마다 Comment를 가져오기 위한 N개의 추가 쿼리 실행
        // SELECT * FROM comment WHERE post_id = 1;
        // SELECT * FROM comment WHERE post_id = 2;
        // SELECT * FROM comment WHERE post_id = 3;
        // ...

        System.out.println(comment.getContent());
    }
}

N + 1 문제 해결 방법

1. Fetch Join 사용

  • JPA의 fetch join을 사용해 필요한 데이터를 한 번의 쿼리로 Post와 Comment를 가져옵니다.
@Query("SELECT p FROM Post p JOIN FETCH p.comments")
List<Post> findAllWithComments();
SELECT p.*, c.* 
FROM post p 
JOIN comment c ON p.id = c.post_id;
  • 특징
    • JPA의 JOIN FETCH 구문을 사용하여 연관된 Entity를 즉시 로딩(Eager Fetch)합니다.
      • 즉시 로딩 : JPA(Hibernate)에서 entity를 조회할 때, 연관된 모든 데이터(entity 나 collection)를 즉시 함께 로딩하는 방식
    • 여러 연관 데이터를 한 번의 쿼리로 가져옵니다.

장점

  • 한 번의 쿼리로 연관 데이터 로딩
    • 모든 데이터를 한 번에 가져오므로 N+1문제가 발생하지 않음
  • 직관적이고 간단
    • 쿼리가 명확해 복잡한 설정 없이 해결 가능

단점

  • 데이터 중복
    • Post가 동일한 여러 Comment와 연관되어 있다면,
      Post 데이터가 중복으로 반환될 수 있음.
    • 이를 해결하기 위해선 반환 결과를 수동으로 처리해야 함 (DISTINCT 사용)
  • 쿼리 복잡성 증가
    • 연관 관계가 많아질수록 쿼리가 복잡해지고 데이터베이스 성능이 저하될 수 있다.
  • 페이징 제한
    • Fetch Join을 사용하면 JPA에서 페이징이 제대로 동작하지 않을 수 있음.

2. EntityGraph 사용

  • @EntityGraph를 통해 연관 데이터를 한 번의 쿼리로 조회
@EntityGraph(attributePaths = {"comments"})
@Query("SELECT p FROM Post p")
List<Post> findAllWithComments();
  • 특징
    • JPA의 @EntityGraph를 사용해 연고나 데이터를 명시적으로 로딩
    • Fetch Join과 같이 즉시로딩하지만, JPQL 대신 어노테이션 기반으로 간단히 정의할 수 있다.

장점

  • JPA 표준 사용
    • JPQL없이 어노테이션으로 연관 데이터 로딩 설정 가능
  • 모듈화
    • 엔티티 수준에서 로딩 전략을 정의하므로 재사용성 높음
  • 페이징 지원
    • Fetch Join과 달리 페이징이 제대로 동작

단점

  • 복잡한 관계에서는 설정 번거로움
    • 다중 연관 관계가 있는 경우 attributePaths에 모든 경로를 지정해야 함
  • 쿼리 최적화 제약
    • 복잡한 조건을 포함한 커스텀 쿼리에는 Fetch Join보다 유연하지 못함.

3. Batch Size사용

  • Hibernate의 @BatchSize를 사용해 연관 데이터를 묶어서 가져옵니다.
    • 최대 10개의 Post마다 한 번의 쿼리로 Comment를 가져옵니다.
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
@BatchSize(size = 10)
private List<Comment> comments;
  • 특징
    • @BatchSize를 사용해 지연 로딩(Lazy Loading)을 개선
    • 연관된 데이터를 일정한 크기로 묶어 한 번에 로드함.
    • Hibernate가 여러 데이터를 한꺼번에 가져오도록 최적화

장점

  • 적당한 쿼리 수
    • 데이터가 많은 경우 Fetch Join보다 쿼리 수를 줄이면서 메모리 사용량도 낮춤
  • 지연 로딩 유지
    • 필요한 데이터만 가져와 불필요한 데이터 로딩 방지

단점

  • 여전히 여러 N개의 쿼리 실행
    • Fetch Join처럼 한 번의 쿼리로 데이터를 가져오지 못하며,
      N개의 쿼리는 아니지만 N / Batch Size 만큼 발생
  • 복잡한 설정
    • 각 연관 관계에 대해 Batch Size를 설정해야 하며
      기본(default)값이 없으므로 따로 관리해야함.

4. DTO로 필요한 데이터만 조회

  • 연관된 엔티티를 가져오는 대신 필요한 데이터를 DTO(Data Transfer Object)로 매핑하여 조회
@Query("SELECT new com.example.PostCommentDto(p.title, c.content) FROM Post p JOIN p.comments c")
List<PostCommentDto> findPostWithComments();
  • 특징
    • JPQL 또는 Native Query를 사용해 필요한 데이터만 선택적으로 조회하여 DTO에 매핑

장점

  • 최소한의 데이터 로딩
    • 필요한 필드만 로딩하여 메모리와 네트워크 자원 절약
  • 복잡한 관계에서도 유연
    • 다양한 조건 및 계산을 포함한 쿼리 작성 가능

단점

  • 유연성 제한
    • DTO가 정의되지 않은 추가 데이터를 사용하려면 쿼리를 재작성해야 함.
    • 쿼리 유지보수 어려움

정리

  Fetch Join EntityGraph Batch Size DTO 조회
장점 한 번의 쿼리로 간단히 문제 해결 * JPA 표준 방식
* 페이징 지원
* 적절한 쿼리 수 유지
* 지연 로딩 유지
* 필요한 데이터만 로딩
* 메모리 효율적 사용
단점 * 데이터 중복 조회 가능성
* 페이징 제한
복잡한 연관 관계 설정이 번거로움 * 여전히 N / Batch Size개 만큼 여러 쿼리 발생
* 설정 관리 필요
* 유연성 부족
* 쿼리 유지보수 어려움
적합한 상황 데이터 크기가 작고,
페이징이 필요 없을 때
페이징이 필요하며,
간단한 연관 관계일 때
대량 데이터를 효율적으로 로딩할 때 복잡한 계산이나 특정 필드만 필요할 때
반응형

'Computer Science > Database' 카테고리의 다른 글

[Database] ERD 식별 - 비식별 관계  (2) 2024.12.01

+ Recent posts