본문 바로가기

Advance I/Springboot & JPA

22.06.13

페치 조인 (fetch join)

• SQL 조인 종류가 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능

• 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능 (ex. 쿼리 2번 -> 1번)

• join fetch 명령어 사용

• 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

 

엔티티 페치 조인

• 회원을 조회하면서 연관된 팀도 함께 조회 (SQL 한 번에)

• SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT

 

 [JPQL] select m from Member m join fetch m.team

 [SQL] SELECT M.*, T.* FROM MEMBER M

           INNER JOIN TEAM T ON M.TEAM_ID=T.ID

즉시로딩 옵션처럼 SQL이 번역되나,

엔티티에서 연관된 어떤 엔티티까지 한번에 조회할 것인지 동적으로 지정

 

페치 조인

INNER JOIN 이기 때문에 TEAM이 null인 MEMBER 엔티티는 누락된다. (OUTER(left) JOIN이라면 포함)

회원1, 회원2, 회원3, 팀A, 팀B가 영속성 컨텍스트(1차 캐시)에 조회된다.

페치 조인은 지연 로딩이든, 즉시 로딩이든 발생할 수 있는 N+1 문제를 해결한다.

 

컬렉션 페치 조인

일대다 관계, 컬렉션 페치 조인

[JPQL] select t from Team t join fetch t.members where t.name = ‘팀A'

[SQL] SELECT T.*, M.* FROM TEAM T

          INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'

 

컬렉션 페치 조인

원하는 대로 데이터를 조회하기는 하지만, 다대일 관계와 다르게 일대다 관계일 때 데이터가 중복될 수 있다.

컬렉션 수만큼 결과가 출력된다.

ex. select t from Team t => size:2 (팀A, 팀B)

      select t from Team t join fetch t.members => size:3 (팀A-회원1, 팀A-회원2, 팀B-회원3)

회원(다)-팀(일) join table -> 팀 중복. 2개의 팀A는 PK가 같으므로 같은 엔티티(0x100)

 

페치 조인과 DISTINCT

• SQL의 DISTINCT는 중복된 결과를 제거하는 명령. 

한 줄의 모든 데이터가 같아야 중복 제거 가능하기 때문에, 이것만으로는 중복을 모두 제거할 수 없다.

[SQL-DISTINCT] 중복제거 실패

JPQL의 DISTINCT 2가지 기능 제공

select distinct t from Team t join fetch t.members

1. SQL에 DISTINCT를 추가. 

2. 애플리케이션에서 엔티티 중복 제거 (같은 식별자를 가진 엔티티 제거)

[JPQL-DISTINCT] 중복제거 성공

 

페치 조인과 일반 조인의 차이

• 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음. 

• JPQL은 결과를 반환할 때 SELECT 절에 지정한 엔티티만 조회할 뿐, 연관관계를 고려하지 않는다.

• 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회한다. (즉시 로딩)

• 페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

 

페치 조인의 특징과 한계

• 페치 조인 대상에는 별칭을 줄 수 없다.

 - where을 통해 데이터를 거르고 싶어도 불가. 필터링 자체가 객체 그래프의 설계 사상에 어긋남

 - 하이버네이트는 가능, 페치조인+페치조인에는 사용하기도 하지만 가급적 사용X

• 둘 이상의 컬렉션은 페치 조인 할 수 없다. (일대다대다)

• 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.

 - 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능 (데이터 중복이 없기 때문에)

   즉, 일대다를 다대일로 뒤집어서 해결하거나, 지연 로딩을 사용하면서 batch size를 설정한다.

<property name="hibernate.default_batch_fetch_size" value="1000이하" />

 - 하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (매우 위험)

 

• 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화

• 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함

  @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략

• 실무에서 글로벌 로딩 전략은 모두 지연 로딩

• 최적화가 필요한 곳은 페치 조인 적용

 

페치 조인 - 정리

• 모든 것을 페치 조인으로 해결할 수는 없음

• 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적

• 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

1. 엔티티를 페치조인을 통해 조회

2. 엔티티를 페치조인을 통해 조회한 후 애플리케이션에서 DTO로 변환

3. JPQL을 생성할 때부터 DTO로 스위칭하여 조회

 

다형성 쿼리

TYPE

• 조회 대상을 특정 자식으로 한정

예) Item 중에 Book, Movie를 조회해라

[JPQL] select i from Item i where type(i) IN (Book, Movie)

[SQL] select i from i

          where i.DTYPE in (‘B’, ‘M’)   *DiscriminatorType

 

TREAT(JPA 2.1)

• 자바의 타입 캐스팅과 유사

• 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용

• FROM, WHERE, SELECT(하이버네이트 지원) 사용

예) 부모인 Item과 자식 Book이 있다.

[JPQL] select i from Item i

            where treat(i as Book).auther = ‘kim’ 

[SQL] select i.* from Item i

          where i.DTYPE = ‘B’ and i.auther = ‘kim’  *Single table 전략의 예

 

엔티티 직접 사용

기본 키 값

•  JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용 

엔티티를 식별하기 위해서 id값을 사용하기 때문

 

[JPQL] select count(m.id) from Member m //엔티티의 아이디를 사용

            select count(m) from Member m //엔티티를 직접 사용

[SQL](JPQL 둘다 같은 다음 SQL 실행) select count(m.id) as cnt from Member

 

엔티티를 파라미터로 전달하든, 식별자를 직접 전달하든 엔티티의 식별자 값이 넘어간다.

 

외래 키 값

연관된 객체(Team)은 테이블의 외래키(TEAM_ID)와 매핑되어 있다.

즉, 연관된 엔티티를 파라미터로 전달해도 외래 키 값이 넘어간다.

 

Named 쿼리

정적 쿼리

• 미리 정의해서 이름을 부여해두고 사용하는 JPQL

• 어노테이션, XML에 정의

• 애플리케이션 로딩 시점에 초기화 후 재사용 (SQL로 파싱 후 캐싱)

애플리케이션 로딩 시점에 쿼리를 검증

 

어노테이션 정의

@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query="select m from Member m where m.username = :username")
public class Member {
...
}

List<Member> resultList =
        em.createNamedQuery("Member.findByUsername", Member.class)
                .setParameter("username", "회원1")
                .getResultList();

-정의

@NamedQuery(name, query)

NamedQuery의 이름은 관례상 "엔티티명.쿼리이름" 으로 사용

-사용

em.createNamedQuery(name, entity.class)

     [.setParameter() ]

      .getResultList();

 

*엔티티 클래스가 지저분해지는 단점 -> Spring Data JPA를 사용하면 해결 가능 (실제 쿼리를 실행하는 메서드에 정의)

 

 

XML 정의

[META-INF/persistence.xml]

<persistence-unit name="jpabook" >
    <mapping-file>META-INF/ormMember.xml</mapping-file>

 

[META-INF/ormMember.xml]

<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1">
    <named-query name="Member.findByUsername">
        <query><![CDATA[
            select m
            from Member m
            where m.username = :username
        ]]></query>
    </named-query>
    
    <named-query name="Member.count">
        <query>select count(m) from Member m</query>
    </named-query>
</entity-mappings>

• XML이 항상 우선권을 가진다.

• 애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.

 

벌크 연산

다량의 UPDATE, DELETE SQL을 한 번에 처리

예제: 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?

JPA 변경 감지 기능

 1. 재고가 10개 미만인 상품을 리스트로 조회한다. 

 2. 상품 엔티티의 가격을 10% 증가한다. 

 3. 트랜잭션 커밋 시점에 변경감지가 동작한다.

변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행

 

벌크 연산 (executeUpdate)

String qlString = "update Product p " +
        "set p.price = p.price * 1.1 " +
        "where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
        .setParameter("stockAmount", 10)
        .executeUpdate();

• 쿼리 한 번으로 여러 테이블 로우 변경 (엔티티)

• executeUpdate()의 결과는 영향받은 엔티티 수 반환 (resultCount)

• UPDATE, DELETE 지원

• 하이버네이트는 INSERT(insert into .. select) 도 지원

 

벌크 연산 주의

• 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.

• 1번이나 2번 중 선택

1. 벌크 연산을 먼저 실행 (영속성 컨텍스트에는 아무것도 없는 상태)

2. 영속성 컨텍스트에 데이터가 있다면, 벌크 연산 수행 후 영속성 컨텍스트 초기화

  *벌크 연산도 JPQL이 실행되는 것이기 때문에, 그 시점에 플러시 자동 호출 후 UPDATE

  *영속성 컨텍스트에 있는 데이터에는 UPDATE가 반영되지 않았기 때문에 데이터베이스에 저장된 값과 다름

 

 

 

 

'Advance I > Springboot & JPA' 카테고리의 다른 글

22.06.21  (0) 2022.06.21
22.06.20 :: 실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화  (0) 2022.06.20
22.06.10  (0) 2022.06.10
22.06.09  (0) 2022.06.09
22.06.08  (0) 2022.06.08