ํ ์ด ํ๋ก์ ํธ์์ JPA ๊ธฐ๋ฐ ํ์ ์ญ์ ๋ฐฐ์น๋ฅผ ๊ฐ๋ฐํ๋ฉด์ ๊ฒช์๋ ์คํ ๋ฆฌ์ ๋ํด ์ ๋ฆฌํ๊ณ ์ ์์ฑํ ๋ด์ฉ
1. Background
(1) JPA ์ฐ๊ด ๊ด๊ณ
ํ์ ์ญ์ ๋ฐฐ์น๋ soft delete ๋ฅผ ํตํด ํํดํ ํ์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํน์ ๊ธฐ๊ฐ๋์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๊ดํ ๋ค ์ ๊ฑฐํ๋ ๋ฐฐ์น์ด๋ค. DB ๋ฐ์ดํฐ๋ JPA ๊ธฐ๋ฐ์ผ๋ก ์ ์ดํ๊ณ ์๋ค.
ํ์ ์ญ์ ๋ฐฐ์น๋ Member ์ MemberRoleMapping ๋ฐ์ดํฐ๋ฅผ ์ ๊ฑฐํ๋ ๊ณผ์ ์ด๋ฉฐ ์ฐ๊ด ๊ด๊ณ๋ฅผ ๋งบ๊ณ ์์๋ค. ๋ํ, Member entity ์ ๊ฑฐํ ๋ ํจ๊ป MemberRoleMapping ๋ฐ์ดํฐ๋ฅผ ์ ๊ฑฐํ๊ธฐ ์ํด cascade option ์ด ์ค์ ๋์ด ์๋ค.

// Member Entity
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
public class Member {
@Id
@UnixTimeOrderedUuidGeneratedValue
@Column(columnDefinition = "BINARY(16)")
private UUID id;
@OneToOne(
fetch = FetchType.LAZY,
mappedBy = "member",
orphanRemoval = true)
private MemberRoleMapping memberRoleMapping;
// ...
}
// MemberRoleMapping Entity
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberRoleMapping {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@Setter
private Member member;
// ...
}
2. Delete Step ์ฟผ๋ฆฌ ๊ณ ๋ คํ๊ธฐ
(1) ์ฐ๊ด ๊ด๊ณ์์ Delete query ์์ N + 1 Query ์ ๊ฑฐํ๊ธฐ
1์ฐจ ์๋๋ก Member ์ MemberRoleMapping ์ ์ ๊ฑฐํ๊ธฐ ์ํด ๊ธฐ์กด cascade option ์ ํ์ฉํด ์ฐ๊ด ๊ด๊ณ ๋ฐ์ดํฐ๋ฅผ ์ ๊ฑฐํ๊ณ ์ ํ๋ค. ํ์ง๋ง N + 1 Query ๊ฐ ๋ฌธ์ ์๋ค. Member ์ delete query ์ธ์๋ MemberRoleMapping select query + MemberRoleMapping delete query ๊ฐ ๋ฐ์ํ๋ค. ๋ง์ฝ 1.000๊ฑด์ ํ์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ฑฐํ๊ณ ์ ํ๋ ๊ฒฝ์ฐ, 2,000๊ฑด์ ์ถ๊ฐ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค. ๋ฐฐ๋ณด๋ค ๋ฐฐ๊ผฝ์ด ๋ ํฐ ์ํฉ์ด๋ค. ๐ซ
- member delete 1๊ฑด(์ถ๊ฐ : member_role_mapping delete 1๊ฑด + member_role_mapping select 1๊ฑด)
memberDeleteWriter
@Bean
@StepScope
public ItemWriter<? super Member> memberDeleteItemWriter() {
final RepositoryItemsWriter<Member> writer = new RepositoryItemsWriter<>();
writer.setRepository(memberBatchRepository);
writer.setMethodName("delete");
return writer;
}
์ถ๊ฐ ์ฟผ๋ฆฌ
// memberRoleMapping ์กฐํ ์ฟผ๋ฆฌ
Hibernate: select mrm1_0.id,mrm1_0.created_date,mrm1_0.member_id,mrm1_0.member_role_id,mrm1_0.modified_date from member_role_mapping mrm1_0 where mrm1_0.member_id=?
// memberRoleMapping ์ญ์ ์ฟผ๋ฆฌ
Hibernate: delete from member_role_mapping where id=?
// member ์ญ์ ์ฟผ๋ฆฌ
Hibernate: delete from member where id=?
(2) RepositoryItemWriter ๋ IN ์ delete ๋ถ๊ฐ๋ฅ. CustomRepositoryItemWriter ๋ง๋ค๊ธฐ
N+1 ์ฟผ๋ฆฌ ์ด์๋ฅผ ๊ฐ์ ํ๊ณ ์ ์ญ์ ๋ฐฐ์น์์๋ ์ฐ๊ด ๊ด๊ณ ํ์ฉํ๋ ๋์ memberId ๋ฅผ IN ์ ์ ๊ธฐ๋ฐํ delete query ๋ก ๋ณ๊ฒฝํ๊ณ ์ ํ๋ค. delete query ๋ฅผ ์ํํ ๋๋ index ๋ฅผ ํ๊ธฐ ๋๋ฌธ์ FK ๋๋ PK ๋ฅผ ํ์ฉํ๊ณ ์ถ์๋ค.
ํ์ง๋ง, ํ๊ฐ์ง ๋ฌธ์ ๊ฐ ์๋ค. RepositoryItemReader ๋ IN ์ ์ ์ฒ๋ฆฌํ ์ ์๋ค.
๋ด๋ถ ์ฝ๋๋ฅผ ์ดํด๋ณด์. RepositoryItemReader ๋ ๋ฆฌํ๋ ์ ์ ๊ธฐ๋ฐ์ผ๋ก chunk ๋จ์์ ๋ฐ์ดํฐ๋ฅผ ์ผ์ผ์ด ์ํํ์ฌ ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋ ๋ฐฉ์์ผ๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ IN ์ ์ ํ์ฉํ ์ ์๋ค. ํ๋ผ๋ฏธํฐ๋ฅผ List type ์ผ๋ก ์ถ๊ฐํ์ฌ ์คํํ ๊ฒฝ์ฐ, reflection ์ ํตํด target method ์ ์ฌ๋ฐ๋ฅด์ง ์์ argument ๊ฐ ์ฃผ์ ๋ ๊ฒ์ผ๋ก ํ๋จํด NoSuchMethodException ๋ฅผ ๋ฐํํ๋ค.

ํ์ง๋ง CustomRepositoryItemWriter ํด๋์ค๋ฅผ ๋ง๋ค์ด์ IN์ ์ ์ฒ๋ฆฌํ ์ ์๋ค. chunk ์ ์์ดํ ๋ค์ ์ํํ๋ ๋ก์ง์ ๊ทธ๋๋ก argument ๋ก ์ฃผ์ ํ๋ CustomRepositoryItemWriter ๋ฅผ ์์ฑํ๋ฉด List ํ์ ์ ํ๋ผ๋ฏธํฐ ๋ฉ์๋๋ฅผ ํธ์ถํ ์ ์๋ค.

CustomRepositoryItemWriter ๋ง๋ค์ด๋ณด์ ๐
CustomRepositoryItemWriter ๋ฅผ ํตํด IN ์ ์ java.util.List ๊ธฐ๋ฐ์ argument ๋ฅผ ์ ๋ฌ ๊ฐ๋ฅํ๋๋ก ๋ง๋ค๊ณ JPQL ๊ธฐ๋ฐ์ delete custom query ๋ฅผ ์์ฑํด ๋ฑ๋กํ๋ค. ์ฟผ๋ฆฌ๋ฅผ ํ์ธํด๋ณด๋ฉด ๊ธฐ์กด์ N + 1 ์ฟผ๋ฆฌ๋ ๋ฐ์ํ์ง ์๊ณ IN ์ ์ ๊ธฐ๋ฐ์ผ๋ก ๋ ์ํฐํฐ ๋ชจ๋ ์ญ์ ํ๋ ๊ฒ์ ํ์ธํด๋ณผ ์ ์๋ค. (Member delete query ๋ JpaRepository ์์ ์ ๊ณตํ๋ deleteAllByIdInBatch ๋ฅผ ์ฌ์ฉํ๋ค.)
CustomRepositoryItemWriter
// memberDeleteItemWriter
@Bean
@StepScope
public ItemWriter<? super Member> memberDeleteItemWriter() {
final CustomRepositoryItemWriter<Member> writer = new CustomRepositoryItemWriter<>();
writer.setRepository(memberBatchRepository);
writer.setMethodName("deleteMemberRoleMappingByMemberIds");
return writer;
}
// MemberBatchRepository
public interface MemberBatchRepository extends JpaRepository<Member, UUID> {
@Modifying
@Query("DELETE FROM MemberRoleMapping mrm WHERE mrm.member.id IN :memberIds")
void deleteMemberRoleMappingByMemberIds(@Param("memberIds") Iterable<UUID> memberIds);
}
์ญ์ ์ฟผ๋ฆฌ
// member_role_mapping ์ญ์ ์ฟผ๋ฆฌ
Hibernate: delete mrm1_0 from member_role_mapping mrm1_0 where mrm1_0.id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
// member ์ญ์ ์ฟผ๋ฆฌ
Hibernate: delete m1_0 from member m1_0 where m1_0.id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
(3) membeRoleMapping + member ์ญ์ ์ฟผ๋ฆฌ: ํธ๋์ญ์ ๋ฌถ๊ธฐ
Spring Batch ์์๋ Step level ์์ TransactionManager ๋ฅผ ์ ์ธํด ํธ๋์ญ์ ์ ์ง์ํ๋ค. ๊ทธ๋ฌ๋ฏ๋ก Step level ์์ ํธ๋์ญ์ ์ ๋ฌถ์ด์ผ ํ๋ค. ๊ธฐ๋ณธ Step ๊ตฌ์กฐ๋ reader - proccessor - writer ๋ฅผ ๊ฐ๊ฐ ํ๋์ฉ ์ ์ธํ ์ ์๋ค.
ํ์ฌ Step ๊ตฌ์ฑ์ ์ญ์ ํ์ ๋ชฉ๋ก ์กฐํ(reader) - ํ์ ๊ถํ ๋งคํ ์ญ์ (writer) - ํ์ ์ญ์ (writer) ์ด๋ฏ๋ก ๋ถ๊ฐ๋ฅํด๋ณด์ด์ง๋ง CompositeItemWriter ๋ฅผ ํ์ฉํ๋ฉด reader ์ ์กฐํ ๋ฐ์ดํฐ๋ฅผ ํตํด ์ฌ๋ ค Writer ๋ฅผ ์ ์ธํ ์ ์๋ค.
@Bean
public Step memberDeletionStep(JobRepository jobRepository) {
log.info("memberDeleteStep started.");
return new StepBuilder("memberDeleteStep", jobRepository)
.<UUID, Member>chunk(100, transactionManager)
.reader(memberDeleteItemReader())
.writer(memberDeleteCompositeWriter())
.build();
}
@Bean
@StepScope
public ItemWriter<? super Member> memberDeleteCompositeWriter() {
log.debug("memberDeleteCompositeWriter started.");
return new CompositeItemWriter<>(List.of(
memberRoleDeleteItemWriter(),
memberDeleteItemWriter()));
}
3. ์คํ ๊ณํ ํ์ธ
์ํ ๋ฐ์ดํฐ 50๊ฑด์ ๊ธฐ์ค์ผ๋ก ์คํ ๊ณํ์ ํ์ธํด๋ณด์๋ค. memberId ๋ฅผ FK ๋ก ์ ์ธ์ผ๋ก ์ธํด ์์ฑ๋ index ์ธ member_id ๋ฅผ ํตํด์ index range scan ๊ธฐ๋ฐ์ผ๋ก ๋์ํ ๊ฒ์ ํ์ธํด๋ณผ ์ ์๋ค.
explain delete from member_role_mapping in member_id IN (?, ?, ?);
+--+-----------+-------------------+----------+-----+-------------+---------+-------+-----+----+--------+-----------+
|id|select_type|table |partitions|type |possible_keys|key |key_len|ref |rows|filtered|Extra |
+--+-----------+-------------------+----------+-----+-------------+---------+-------+-----+----+--------+-----------+
|1 |DELETE |member_role_mapping|null |range|member_id |member_id|17 |const|1 |100 |Using where|
+--+-----------+-------------------+----------+-----+-------------+---------+-------+-----+----+--------+-----------+
show index from member_role_mapping
+-------------------+----------+--------------+------------+--------------+---------+-----------+--------+------+----+----------+-------+-------------+-------+----------+
|Table |Non_unique|Key_name |Seq_in_index|Column_name |Collation|Cardinality|Sub_part|Packed|Null|Index_type|Comment|Index_comment|Visible|Expression|
+-------------------+----------+--------------+------------+--------------+---------+-----------+--------+------+----+----------+-------+-------------+-------+----------+
|member_role_mapping|0 |PRIMARY |1 |id |A |1 |null |null | |BTREE | | |YES |null |
|member_role_mapping|1 |member_id |1 |member_id |A |1 |null |null |YES |BTREE | | |YES |null |
|member_role_mapping|1 |member_role_id|1 |member_role_id|A |1 |null |null |YES |BTREE | | |YES |null |
+-------------------+----------+--------------+------------+--------------+---------+-----------+--------+------+----+----------+-------+-------------+-------+----------+
4. ๋ฐฐ์น ์คํ ์๊ฐ ํ์ธ & ํ๊ธฐ
50๊ฑด์ ์ํ ๋ฐ์ดํฐ๋ฅผ ํ ๋๋ก Step์ ๋ง๋ค์ด ๋น๊ตํด๋ณด์.
RepositoryItemWriter ๊ธฐ๋ฐ์ ๋์กฐ๊ตฐ์ผ๋ก ๋น๊ตํ์ ๋, CustomRepositoryItemWriter ์ ์คํ ์๊ฐ์ด 1/3 ์ผ๋ก ์ค์ด๋ค์๋ค.
# N + 1 ๊ธฐ๋ฐ delete ๋ฐฐ์น ์คํ ์๊ฐ
Step: [memberDeleteStep] executed in 654ms
# IN์ delete ๋ฐฐ์น ์คํ ์๊ฐ
Step: [memberDeleteStep] executed in 192ms
JPA ๋ฅผ ๊ธฐ๋ฐ Spring Batch Job ์ ์์ฑํ๋ฉฐ ๋๋์
- Entity ์ ์ฐ๊ด ๊ด๊ณ๋ key๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๊ฐ์ ์ฐธ์กฐ๋ฅผ ํ์ฉํ๊ณ ์ต๋ํ ์ง์ฐํด์ ์ฐ๊ด ๊ด๊ณ๋ฅผ ์ค์ ํ๋ ๊ฒ์ด ์ ํฉํ ๊ฒ ๊ฐ๋ค. ์ฐ๊ด ๊ด๊ณ ์ค์ ์, ๊ณ ๋ ค ์ฌํญ๊ณผ ์ ์ฝ ์ฌํญ์ด ๋ง์์ ธ ๋ณ๊ฒฝ์ด ์ด๋ ค์์ง ์ ์๋ค. (e.g. N + 1 ์ฟผ๋ฆฌ, cascade option)
- ๋ฐฐ์น ์์ ์์ rows deletion ์ IN ์ ์ ๊ธฐ๋ฐ์ผ๋ก ๋์ํ๋๋ก ๊ตฌ์ฑ์ ์ฐ์ ์์๋ก ๊ณ ๋ คํ์. ๋น์ฐํ ์ด์ผ๊ธฐ์ง๋ง ๋ถ๊ฐ ์ฟผ๋ฆฌ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ฑ๋ฅ์ ์ํฅ์ ๋ฏธ์น ์ ์๋ค.
'๐ backend > spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| MessageSourceAutoConfiguration ์ฝ๋ ๊ฒํ ํ๊ธฐ (0) | 2024.11.28 |
|---|---|
| [kurly tech blog] Redisson, Spring AOP ๊ธฐ๋ฐ ๋ถ์ฐ๋ฝ ์ ์ฉ ๋ฐฉ๋ฒ summary (0) | 2024.11.26 |
| [Spring Batch] Job ์คํ ํ๋กํผํฐ, JobParameter, Scope (0) | 2024.08.26 |
| Spring Batch Architecture (0) | 2024.05.10 |
| hibernate.query.in_clause_parameter_padding = true ? (1) | 2024.03.06 |