Entity에서 여러개의 컬럼이 FetchType.LAZY가 걸려있을 때
Fetch join을 사용하니 여러개가 중첩된 형태의 조인 쿼리가 날라가는걸 발견하였습니다.
Fetch join을 사용하지 않고 조회 vs Fetch join을 사용하고 조회
어떤것이 더 검색 성능이 좋을까가 궁금하여 테스트 해보았습니다.
우선 게시글인 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 만개의 더미테이터를 적재한 후 테스트를 진행 해 보았습니다.
각각 무작위로 10개씩 조회를 해본 결과 입니다.
fetch join을 적용시키지 않은 조회
- 테스트
- id : 123445
![]()
- id : 5655343
- id: 9584873
- id : 4758455
- id: 6859
![]()
- id: 8879082
- id: 9110000
- id: 1234542
- id: 503422
- id: 3949583
Total값 기준으로 평균을 내보았더니 50.63
이 나왔습니다.
fetch join을 적용시킨 조회
- 테스트
- id : 123445
![]()
- id : 5655343
- id: 9584873
- id : 4758455
- id: 6859
- id: 8879082
- id: 9110000
- id: 1234542
- id: 503422
- id: 3949583
Total 값 기준으로 평균을 내보았더니 87.685
가 나왔습니다.
Postman을 활용하여 100회를 테스트 해보았을 때
fetch join을 적용시키지 않은 조회
fetct join을 적용시킨 조회
회원 테이블과 레코드 테이블에만 각각 1000만개의 데이터가 적재되어있었습니다.
조인되는 테이블은 회원테이블, 아이콘 테이블, 컬러 테이블 을 조인하기 때문에
아이콘 테이블과, 컬러 테이블에도 1000만개의 데이터를 적재하고 다시 테스트를 진행 해보았습니다.
fetch join을 적용시키지 않은 조회
- 테스트
- id : 123445
- id : 5655343
- id: 9584873
- id : 4758455
- id: 6859
- id: 8879082
- id: 9110000
- id: 1234542
- id: 503422
- id: 3949583
Total 값 기준으로 평균을 내보았더니 48.176
가 나왔습니다.
fetch join을 적용시킨 조회
- 테스트
- id : 123445
- id : 5655343
- id: 9584873
- id : 4758455
5. id: 6859
- id: 8879082
- id: 9110000
- id: 1234542
- id: 503422
- id: 3949583
![]()
Total 값 기준으로 평균을 내보았더니 123.342
가 나왔습니다.
확연하게 fetch join을 적용시킨 조회가 더 느리게 나왔습니다.
반복테스트 100회도 실행 해보겠습니다.
fetch join을 적용시키지 않고 반복테스트
fetch join을 적용시키고 반복테스트
반복 테스트의 경우 fetch join을 적용시킨게 더 빨랐지만
각각 다른 id 값으로 테스트 해본 평균은 패치 조인을 적용시키지 않은 경우에 더 빨랐습니다.
레코드 조회는 각각 다른 아이디로 호출이 될 가능성이 더 높기 때문에
패치 조인을 적용시키지 않아야 할 것 같습니다.