영속성 전이와 N+1 문제

?에서 !로·2022년 3월 22일
0

영속성 전이

특정 엔티티의 영속 상태가 변경되었을 때 종속된 엔티티들의 영속 상태가 대상 엔티티를 따라 함께 반영되는 것을 말한다.

JPA에서 테이블 사이의 관계를 맺어주는 방법으로 @OneToOne, @OneToMany, @ManyToOne, @ManyToMany 등의 어노테이션을 사용할 때 cascade 라는 속성을 통해 종속된 엔티티의 영속 상태가 함께 반영되는 시점을 지정할 수 있다.

CascadeType 별 영속성 전이가 이루어지는 시점

  • PERSIST - 대상(target) 엔티티가 new 상태에서 managed 상태로 변경되는 시점
  • REMOVE - 대상 엔티티가 managed 상태에서 removed 상태로 변경되는 시점
  • DETACH - 대상 엔티티가 managed 상태에서 detached 상태로 변경되는 시점
  • MERGE - 대상 엔티티가 detached 상태에서 managed 상태로 변경되는 시점
  • REFRESH - 엔티티 매니저의 refresh() 메소드 호출 시점
  • ALL - 모든 상태 변화에 대해 종속된 엔티티들의 영속 상태를 함께 반영

+) refresh() : 데이터베이스로부터 인스턴스 값을 다시 읽어 오기

고아 객체

부모 엔티티와 연관관계가 끊어진 자식 엔티티를 고아 객체라 하며, JPA에서 부모와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 고아 객체 제거 기능을 제공한다. (orphanRemoval = true)

참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제한다.

  • 참조하는 객체가 여러개라면 문제가 발생할 수 있기 때문에 @OneToOne, @OneToMany에만 사용이 가능하다.
  • 부모가 제거되면 자식은 고아가 되기 때문에 자식도 함께 제거되는데, casecase에 REMOVE를 설정한 것과 동일하다.
  • 영속성 컨텍스트를 flush 하는 시점에 delete SQL이 실행된다.

CascadeType.REMOVE + orphanRemoval = true

부모 엔티티를 삭제할 때는 CascadeType.REMOVE 와 orphanRemoval = true 모두 동일하게 자식 엔티티도 같이 삭제된다. 하지만 부모 엔티티에서 자식 엔티티를 제거하는 경우, CascadeType.REMOVE는 자식 엔티티가 그대로 남아있고 rphanRemoval = true는 자식 엔티티를 고아 객체로 판단해 제거한다.

연관관계 매핑

테이블과 객체

데이터베이스 테이블은 외래 키 하나로 양 쪽 테이블에서 조인이 가능하지만, 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능하다. 븍, 객체의 연관관계가 단방향이어도 테이블에서는 외래키를 통해 양방향 관계이다.

구성

  • 방향 : 단방향, 양방향
    • 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향 관계, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 한다.
  • 다중성 : 다대일, 일대다, 일대일, 다대다
  • 연관관계의 주인 : 양방향 설정 시 연관관계의 주인을 정해야 한다.

JPA에서

  • @JoinColumn어노테이션을 사용해 name 속성에 매핑할 외래 키 이름을 지정한다.
    • 생략할 경우 필드명_참조하는 테이블의 기본 키 컬럼명을 기본값으로 사용한다.

ManyToMany

관계형 데이터베이스는 테이블 2개로 다대다 관계를 표현할 수 없다. 따0라서, 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용한다.

@ManyToMany

연결 테이블을 자동으로 처리해주므로 도메인 모델이 단순해지고 편리하다.

하지만!!
연결테이블에 추가 컬럼을 담을 필요가 생길 수 있기 때문에 직접 정의해 주는 것이 좋다. 또한 기본키의 경우 식별 관계보다 비식별 관계가 단순하고 편리하다.

+) 식별관계 : 부모의 기본키를 자식의 기본키+식별키로 사용한다. 비식별관계 : 부모의 기본키는 외래키로만 사용하고 새로운 기본키 정의

OneToMany

일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 것이다.
-> 본인 테이블에 외래 키가 있으면 엔티티 저장과 연과관계 매핑 처리를 insert 쿼리 한번으로 끝낼수 있지만, 다른 테이블에 있기 때문에 연관관계 매핑 처리를 위한 Update 쿼리 (외래키를 갖고있는 엔티티)를 추가로 실행해야 한다.

따라서, 일대다 단방향 매핑은 성능 문제와 관리 비용이 발생하기 때문에 다대일 양방향 매핑이 효율적이다.

OneToOne

일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래 키를 가질 수 있으며 선택해야 한다. 필요할 경우 양방향

보통 주 테이블에 외래 키를 갖는 것이 편리하기 때문에 선호한다.

양방향

양방향은 실질적으로 단항향 관계 2개가 이어진 것으로, 외래 키를 비롯한 제어의 권한을 갖는 연관 관계의 주인이 누군지 mappedBy 속성을 사용해서 지정해 줘야 한다. 즉, mappedBy 속성에 연관관계 주인인 엔티티의 참조 필드(외래키)를 지정해 주어야 한다.

+) 연관 관계의 주인은 연관 관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제를 할 수 있지만, 연관 관계의 주인이 아니면 조회만 가능하다.
+) 데이터베이스는 무조건 다(N)쪽이 외래 키를 갖는다.

기본적으로 단방향 매핑으로 하고 나중에 역방향으로 객체 탐색이 꼭 필요하다고 느낄 때 추가하는 것이 좋다.

무한루프

JPA의 양방향 매핑 관계에서 잘못하면 무한루프가 발생할 가능성이 있다.

이런 상황은 보통 엔티티를 JSON으로 변환하려는 상황에서 발생한다. 예를들어 회원, 팀이 양방향 관계에 놓여있다고 했을 때 회원 엔티티를 JSON으로 변환 한다고 가정한다면, 회원에서는 팀을 팀에서는 회원을 JSON으로 변환시키려는 과정에서 무한 루프가 발생한다.

그래서 보통 dto를 만들고 JSON으로 변환할 데이터만 정의해서 그 dto로 JSON을 만든다.

JPQL

Java Persistence Query Language의 약자로, DB 테이블이 아니라 엔티티의 객체를 대상으로 검색하는 객체 지향 쿼리이다. JPA는 JpaRepository를 상속한 인터페이스 메소드 이름을 분석해서 JPQL로 변환하고, 이를 가지고 SQL을 만들어서 DB에 SQL을 실행하는 과정을 거친다.

  • jpql은 SQL을 추상화 해서 특정 데이터 베이스에 의존하지 않는다.
    • 데이터 베이스 Dialect만 변경하면 jpql을 수정하지 않아도 (표준화된 함수 사용) 데이터 베이스를 변경할 수 있다.

N+1 문제

jpa의 entity 조회시 Query 가 발생하고 내부에 존재하는 다른 연관관계에 접근할 때 조회화는 데이터 개수 N만큼 쿼리가 발생하는 현상

발생 케이스

즉시로딩

이론적으로 즉시로딩은 조인을 사용해서 한 번의 SQL로 테이블 정보를 함께 조회하지만 JPQL을 실행할 때 문제가 발생한다.

예를 들어 특정 회원 정보를 조회할 때, 회원 엔티티 조회 커리가 실행되고(1) 연관된 주문 엔티티에서 해당 회원을 참조하는 주문 데이터 N개에 대한 N개의 SQL이 실행된다.

지연 로딩

초기에는 해당 엔티티만 조회하는 쿼리가 한번 실행되지만, 객체 초기화 할 때 초기화하는 수 만큼 N+1문제가 발생한다.

해결 방법

패치 조인

가장 일반적인 방법으로 JPQL에서 성능 최적화를 위해 제공하는 기능이다.

select distinct m from Member m join fetch m.orders

패치 조인은 inner join이기 때문에 일대다 관계에서 일인 테이블을 기준으로 데이터를 조회하면 카티션 곱이 발생해 데이터가 중복되므로 distinct를 함께 사용해 줘야 한다.

fetch 조인 한계

  • 페이징 처리 문제
    JPA는 Pageable 인터페이스를 통해 쉬운 페이징 처리 기능을 제공한다. 하지만 N+1 문제 회피를 위해 사용하는 fetch 조인과 함께 사용한다면 문제가 될 수 있다.

    • ToOne 애너테이션을 통해 형성된 관계인 경우 테이블 조인에 따라 데이터 수가 변경되지 않으므로 페이징 처리 할 수 있다.
    • ToMany 애너테이션을 통해 형성된 관계인 경우 테이블 조인에 따라 데이터가 변경되어 페이징 처리와 페치 조인이 동시에 불가능하다. 만약 이런 상황에서 페이징과 페치 조인을 같이 사용한다면 데이터를 가져올 때 경고 로그를 남기고 db에서 데이터를 잘라서 가져오는 것이 아니라 데이터를 모두 메모리에 올리고 어플리케이션 단에서 페이징을 수행한다.

@EntityGraph 애너테이션 사용

@EntityGraph 애너테이션에 함께 조인할 엔티티 필드를 지정한다. 쿼리를 별도로 만들지 않아도 되기 때문에 편하다.

	@EntityGraph(attributePaths = {"team"}) 
    List<Member> findAll();

하이버네이트 @BatchSize

연관된 엔티티를 조회할 때 지정한 size 만큼 SQL의 in절을 사용해서 조회한다. 예를들어, size가 5이고 전체 데이터가 10개라면 2번의 SQL이 실행된다,

      @BatchSize(size = 5)  
      @OneToMany(mappedBy = "member", fetch = FetchType.EAGER) 
      private List<Order> orders = new ArrayList<Order>();

JPA 기타 질문

조회작업에서 @Transaction(read=true)를 사용하면 왜 성능이 빨리지나요?

JPA에서는 read=true 속성이 설정되면 영속성 컨텍스트 flush가 발생하지 않습니다. Flush는 영속성 컨텐스트의 변경내용을 데이터 베이스와 동기화하는 작업을 말합니다.

0개의 댓글