FetchType.LAZY로 되어있는 여러개의 연관관계를 가진 엔티티에 일반 조회 vs fetch join 조회

도모·2023년 1월 16일
1
post-thumbnail

1.시작하며

Entity에서 여러개의 컬럼이 FetchType.LAZY가 걸려있을 때

Fetch join을 사용하니 여러개가 중첩된 형태의 조인 쿼리가 날라가는걸 발견하였습니다.

Fetch join을 사용하지 않고 조회 vs Fetch join을 사용하고 조회

어떤것이 더 검색 성능이 좋을까가 궁금하여 테스트 해보았습니다.

2.문제 상황

우선 게시글인 Record의 엔티티입니다.

@Entity(name = "RECORD")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Where(clause = "DELETED_AT is null")
@SQLDelete(sql = "UPDATE RECORD SET RECORD.DELETED_AT = CURRENT_TIMESTAMP WHERE RECORD.RECORD_ID = ?")
@Getter
public class Record extends BaseEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name = "RECORD_ID")
	private Long id;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "RECORD_CATEGORY_ID")
	private RecordCategory recordCategory;

	@ManyToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "MEMBER_ID")
	private Member writer;

	@Column(name = "TITLE")
	private String title;

	@Column(name = "CONTENT")
	private String content;

	@Column(name = "NUM_OF_IMAGE")
	private Integer numOfImage;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "RECORD_COLOR_ID")
	private RecordColor recordColor;

	@OneToOne(fetch = FetchType.LAZY)
	@JoinColumn(name = "RECORD_ICON_ID")
	private RecordIcon recordIcon;
}

해당 레코드에 findById로 조회하였을 때 LAZY 속성으로 인해 연관관계가 맺어져있는 다른 엔티티의 값을 바로 가져오지 않고, 사용하는 시점에 쿼리를 날려 가져와서 여러개의 쿼리가 나가는 문제가 있었다.

Hibernate: 
    select
        record0_.RECORD_ID as record_i1_4_0_,
        record0_.CREATED_AT as created_2_4_0_,
        record0_.DELETED_AT as deleted_3_4_0_,
        record0_.MODIFIED_AT as modified4_4_0_,
        record0_.CONTENT as content5_4_0_,
        record0_.NUM_OF_IMAGE as num_of_i6_4_0_,
        record0_.RECORD_CATEGORY_ID as record_c8_4_0_,
        record0_.RECORD_COLOR_ID as record_c9_4_0_,
        record0_.RECORD_ICON_ID as record_10_4_0_,
        record0_.TITLE as title7_4_0_,
        record0_.MEMBER_ID as member_11_4_0_ 
    from
        RECORD record0_ 
    where
        record0_.RECORD_ID=? 
        and (
            record0_.DELETED_AT is null
        )
Hibernate: 
    select
        member0_.MEMBER_ID as member_i1_3_0_,
        member0_.CREATED_AT as created_2_3_0_,
        member0_.DELETED_AT as deleted_3_3_0_,
        member0_.MODIFIED_AT as modified4_3_0_,
        member0_.LOGIN_TYPE as login_ty5_3_0_,
        member0_.NICKNAME as nickname6_3_0_,
        member0_.OAUTH_ID as oauth_id7_3_0_,
        member0_.PASSWORD as password8_3_0_,
        member0_.USERNAME as username9_3_0_ 
    from
        MEMBER member0_ 
    where
        member0_.MEMBER_ID=? 
        and (
            member0_.DELETED_AT is null
        )
Hibernate: 
    select
        recordcolo0_.RECORD_COLOR_ID as record_c1_6_0_,
        recordcolo0_.CREATED_AT as created_2_6_0_,
        recordcolo0_.DELETED_AT as deleted_3_6_0_,
        recordcolo0_.MODIFIED_AT as modified4_6_0_,
        recordcolo0_.HEX_CODE as hex_code5_6_0_,
        recordcolo0_.NAME as name6_6_0_ 
    from
        RECORD_COLOR recordcolo0_ 
    where
        recordcolo0_.RECORD_COLOR_ID=? 
        and (
            recordcolo0_.DELETED_AT is null
        )
Hibernate: 
    select
        recordicon0_.RECORD_ICON_ID as record_i1_7_0_,
        recordicon0_.CREATED_AT as created_2_7_0_,
        recordicon0_.DELETED_AT as deleted_3_7_0_,
        recordicon0_.MODIFIED_AT as modified4_7_0_,
        recordicon0_.NAME as name5_7_0_ 
    from
        RECORD_ICON recordicon0_ 
    where
        recordicon0_.RECORD_ICON_ID=? 
        and (
            recordicon0_.DELETED_AT is null
        )

그래서 한방쿼리로 해결하려고 fetch join을 적용 시켰습니다.

public interface RecordRepository extends JpaRepository<Record, Long> {
	@Query("select r from RECORD r join fetch r.writer join fetch r.recordColor join fetch r.recordIcon"
			+ " where r.id = :id")
	Optional<Record> findById(Long id);
}

그랬더니 아래와 같이 여러개가 중첩 되어있는 형태의 쿼리가 날라가게 되었습니다.

Hibernate: 
    select
        record0_.RECORD_ID as record_i1_4_0_,
        member1_.MEMBER_ID as member_i1_3_1_,
        recordcolo2_.RECORD_COLOR_ID as record_c1_6_2_,
        recordicon3_.RECORD_ICON_ID as record_i1_7_3_,
        record0_.CREATED_AT as created_2_4_0_,
        record0_.DELETED_AT as deleted_3_4_0_,
        record0_.MODIFIED_AT as modified4_4_0_,
        record0_.CONTENT as content5_4_0_,
        record0_.NUM_OF_IMAGE as num_of_i6_4_0_,
        record0_.RECORD_CATEGORY_ID as record_c8_4_0_,
        record0_.RECORD_COLOR_ID as record_c9_4_0_,
        record0_.RECORD_ICON_ID as record_10_4_0_,
        record0_.TITLE as title7_4_0_,
        record0_.MEMBER_ID as member_11_4_0_,
        member1_.CREATED_AT as created_2_3_1_,
        member1_.DELETED_AT as deleted_3_3_1_,
        member1_.MODIFIED_AT as modified4_3_1_,
        member1_.LOGIN_TYPE as login_ty5_3_1_,
        member1_.NICKNAME as nickname6_3_1_,
        member1_.OAUTH_ID as oauth_id7_3_1_,
        member1_.PASSWORD as password8_3_1_,
        member1_.USERNAME as username9_3_1_,
        recordcolo2_.CREATED_AT as created_2_6_2_,
        recordcolo2_.DELETED_AT as deleted_3_6_2_,
        recordcolo2_.MODIFIED_AT as modified4_6_2_,
        recordcolo2_.HEX_CODE as hex_code5_6_2_,
        recordcolo2_.NAME as name6_6_2_,
        recordicon3_.CREATED_AT as created_2_7_3_,
        recordicon3_.DELETED_AT as deleted_3_7_3_,
        recordicon3_.MODIFIED_AT as modified4_7_3_,
        recordicon3_.NAME as name5_7_3_ 
    from
        RECORD record0_ 
    inner join
        MEMBER member1_ 
            on record0_.MEMBER_ID=member1_.MEMBER_ID 
    inner join
        RECORD_COLOR recordcolo2_ 
            on record0_.RECORD_COLOR_ID=recordcolo2_.RECORD_COLOR_ID 
    inner join
        RECORD_ICON recordicon3_ 
            on record0_.RECORD_ICON_ID=recordicon3_.RECORD_ICON_ID 
    where
        (
            record0_.DELETED_AT is null
        ) 
        and record0_.RECORD_ID=? 

두 쿼리중에 어떤게 더 성능상 빠를까 궁금하여 레코드와 회원 테이블에 각각 1000 만개의 더미테이터를 적재한 후 테스트를 진행 해 보았습니다.

3.조회 테스트 결과

각각 무작위로 10개씩 조회를 해본 결과 입니다.

fetch join을 적용시키지 않은 조회

  • 테스트
    1. id : 123445
    2. id : 5655343
    3. id: 9584873
    4. id : 4758455
    5. id: 6859
    6. id: 8879082
    7. id: 9110000
    8. id: 1234542
    9. id: 503422
    10. id: 3949583

Total값 기준으로 평균을 내보았더니 50.63 이 나왔습니다.

fetch join을 적용시킨 조회

  • 테스트
    1. id : 123445
    2. id : 5655343
    3. id: 9584873
    4. id : 4758455
    5. id: 6859
    6. id: 8879082
    7. id: 9110000
    8. id: 1234542
    9. id: 503422
    10. id: 3949583

Total 값 기준으로 평균을 내보았더니 87.685 가 나왔습니다.

4.반복 테스트 결과

Postman을 활용하여 100회를 테스트 해보았을 때

fetch join을 적용시키지 않은 조회

fetct join을 적용시킨 조회

5.잘못된 테스트 였나?

회원 테이블과 레코드 테이블에만 각각 1000만개의 데이터가 적재되어있었습니다.

조인되는 테이블은 회원테이블, 아이콘 테이블, 컬러 테이블 을 조인하기 때문에

아이콘 테이블과, 컬러 테이블에도 1000만개의 데이터를 적재하고 다시 테스트를 진행 해보았습니다.

fetch join을 적용시키지 않은 조회

  • 테스트
    1. id : 123445
    2. id : 5655343
    3. id: 9584873
    4. id : 4758455
    5. id: 6859
    6. id: 8879082
    7. id: 9110000
    8. id: 1234542
    9. id: 503422
    10. id: 3949583

Total 값 기준으로 평균을 내보았더니 48.176 가 나왔습니다.

fetch join을 적용시킨 조회

  • 테스트
    1. id : 123445
    2. id : 5655343
    3. id: 9584873
    4. id : 4758455

      5. id: 6859
    5. id: 8879082
    6. id: 9110000
    7. id: 1234542
    8. id: 503422
    9. id: 3949583

Total 값 기준으로 평균을 내보았더니 123.342 가 나왔습니다.

확연하게 fetch join을 적용시킨 조회가 더 느리게 나왔습니다.

반복테스트 100회도 실행 해보겠습니다.

fetch join을 적용시키지 않고 반복테스트

fetch join을 적용시키고 반복테스트

6.결론

반복 테스트의 경우 fetch join을 적용시킨게 더 빨랐지만

각각 다른 id 값으로 테스트 해본 평균은 패치 조인을 적용시키지 않은 경우에 더 빨랐습니다.

레코드 조회는 각각 다른 아이디로 호출이 될 가능성이 더 높기 때문에

패치 조인을 적용시키지 않아야 할 것 같습니다.

profile
안녕하세요. 백엔드 엔지니어 도모입니다.

0개의 댓글