BackEnd๐ŸŒฑ/Spring

[JPA] deleteAll(), deleteAllInBatch(), deleteInBatch() ์ •๋ฆฌ

dkswnkk 2024. 8. 12. 00:27

๊ฐœ์š”

Hibernate(JPA)์—์„œ๋Š” ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋Š” ์•„๋ž˜์˜ ๋‹ค์–‘ํ•œ ๋ฉ”์„œ๋“œ๋“ค์„ ์ง€์›ํ•œ๋‹ค.

  • delete(),deleteById()
  • deleteAll(), deleteAllById()
  • deleteInBatch()
  • deleteAllInBatch(), deleteAllByIdInBatch()

๋‹ค์–‘ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๊ฒƒ์€ ์ข‹์ง€๋งŒ, ๋„ค์ด๋ฐ๋งŒ ๋ณด๊ณ  ํ˜ผ๋ž€์Šค๋Ÿฌ์šด ๊ฒƒ๋“ค์ด ๋ช‡๋ช‡ ์žˆ๋‹ค. ๋Œ€ํ‘œ์ ์œผ๋กœ deleteInBatch()์™€ deleteAllInBatch(), ๊ทธ๋ฆฌ๊ณ  deleteAll()๊ณผ deleteAllInBatch()์™€ ๊ฐ™์€ ๋ฉ”์„œ๋“œ๋“ค์ด๋‹ค.

์ด๋ฒˆ ๊ฒŒ์‹œ๊ธ€์—์„œ๋Š” ์œ„ ๋ฉ”์„œ๋“œ๋“ค์ด ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ์‚ญ์ œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€๋ฅผ ์‚ดํŽด๋ณผ ๊ฒƒ์ด๊ณ , ์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ์™€ ์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ๋กœ ๋‚˜๋ˆ„์–ด ์‚ญ์ œ ๋™์ž‘์„ ๋ถ„์„ํ•ด ๋ณผ ๊ฒƒ์ด๋‹ค.

 

 

์‚ฌ์ „ ์„ธํŒ…

DB๋Š” MySQL์„ ์‚ฌ์šฉํ–ˆ์œผ๋ฉฐ, MySQL์—์„œ ๋ฐฐ์น˜ ์‚ญ์ œ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ JDBC URL์ด ํ•„์š”ํ•˜๋‹ค.

jdbc:mysql:localhost:3306/db
?cachePreStmts=true
&useServerPrepStmts=true
&rewriteBatchedStatements=true

๊ฐ ์„ค์ •์— ๋Œ€ํ•œ ๊ฐ„๋‹จํ•œ ์„ค๋ช…์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • cachePrepStmts: PreparedStatement์˜ ์บ์‹ฑ์„ ํ™œ์„ฑํ™”ํ•œ๋‹ค. cachePrepStmts=true๋กœ ์„ค์ •ํ•˜๋ฉด, ๋™์ผํ•œ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ˜๋ณต์ ์œผ๋กœ ์‹คํ–‰ํ•  ๋•Œ ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋œ๋‹ค. ์ด ์บ์‹œ๋Š” prepStmtCacheSize ๋ฐ prepStmtCacheSqlLimit๊ณผ ํ•จ๊ป˜ ์ž‘๋™ํ•˜์—ฌ, ์บ์‹œ ํ•  PreparedStatement์˜ ์ˆ˜์™€ SQL ๋ฌธ์žฅ์˜ ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • useServerPrepStmts: ์„œ๋ฒ„ ์ธก์—์„œ PreparedStatement๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •ํ•œ๋‹ค. MySQL์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ PreparedStatement๋ฅผ ์ฒ˜๋ฆฌํ•˜์ง€๋งŒ, useServerPrepStmts=true๋กœ ์„ค์ •ํ•˜๋ฉด ์„œ๋ฒ„์—์„œ PreparedStatement๋ฅผ ๊ด€๋ฆฌํ•˜๊ฒŒ ๋˜์–ด, ์„œ๋ฒ„์˜ ๋ฆฌ์†Œ์Šค๋ฅผ ํ™œ์šฉํ•œ ํšจ์œจ์ ์ธ ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด์ง„๋‹ค.
  • rewriteBatchedStatements: ์ด ์†์„ฑ์ด ํ™œ์„ฑํ™”๋˜๋ฉด, ์—ฌ๋Ÿฌ ๊ฐœ์˜ SQL ๋ฌธ์„ ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด ๋ฒ„ํผ๋กœ ์žฌ์ž‘์„ฑํ•˜์—ฌ ํ•˜๋‚˜์˜ ์š”์ฒญ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ „ํ•œ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ฐฐ์น˜ ์ž‘์—…์˜ ์„ฑ๋Šฅ์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋  ์ˆ˜ ์žˆ๋‹ค.

์—ฌ๊ธฐ์„œ ํŠนํžˆ rewriteBatchedStatements ์†์„ฑ์„ ํ™œ์„ฑํ™”ํ•˜๋ฉด, ์—ฌ๋Ÿฌ SQL ๋ฌธ์ด ํ•˜๋‚˜์˜ ๋ฌธ์ž์—ด ๋ฒ„ํผ๋กœ ์žฌ์ž‘์„ฑ๋˜์–ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋Œ€ํ•œ ํ•˜๋‚˜์˜ ์š”์ฒญ์œผ๋กœ ์ „์†ก๋˜๋Š”๋ฐ, ์ด ์„ค์ •์˜ ํ™œ์„ฑํ™” ์—ฌ๋ถ€์— ๋”ฐ๋ผ ์ฟผ๋ฆฌ๋ฌธ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‹ฌ๋ผ์ง€๊ฒŒ ๋œ๋‹ค.

-- rewriteBatchedStatements = false
insert into author (name, id) values ('Jane Austen', 1);
insert into author (name, id) values ('Nathaniel Hawthorne', 2);
...

-- rewriteBatchedStatements = true
insert into author (name, id) values ('Jane Austen', 1), ('Nathaniel Hawthorne', 2), ...;

์ถ”๊ฐ€๋กœ ๋ฐฐ์น˜ ํฌ๊ธฐ๋ฅผ ์„ค์ •ํ•˜๋ ค๋ฉด spring.jpa.properties.hibernate.jdbc.batch_size ์†์„ฑ์„ ์‚ฌ์šฉํ•˜๊ณ , ๋ฒ„์ „์„ ๊ฐ€์ง„ ์—”ํ‹ฐํ‹ฐ์˜ ๊ฒฝ์šฐ spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true๋กœ ์ง€์ •ํ•˜์—ฌ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

 

 

์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ์—†๋Š” ๊ฐ„๋‹จํ•œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ 1,000๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ง€๊ณ  ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ๋‹ค.

@Entity
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
}
...
SET @i = 1;
WHILE @i <= 1000 DO
    INSERT INTO author (name) VALUES (CONCAT('Author ', @i));
    SET @i = @i + 1;
END WHILE;

 

1. deleteAllInBatch() ์‚ญ์ œ

@Transactional
public void deleteAuthorsDeleteAllInBatch() {
    authorRepository.deleteAllInBatch();
}
delete a1_0 from author a1_0

๊ฐ„๋‹จํ•˜๊ฒŒ ํ•˜๋‚˜์˜ DELETE ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ์‚ญ์ œํ•˜๋ฉฐ, ๋ฒŒํฌ ์ž‘์—…์œผ๋กœ ์ฒ˜๋ฆฌ๋˜์—ˆ๋‹ค.

  • ๋ฒŒํฌ(Bulk): ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ๋‹ค์ˆ˜์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹
  • ๋ฐฐ์น˜(Batch): ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ์— ๋‚˜๋ˆ„์–ด ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹

์ผ๋ฐ˜์ ์œผ๋กœ ๋ฒŒํฌ ์ž‘์—…์€ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ณด๋‹ค ๋น ๋ฅด๊ณ  ์ธ๋ฑ์Šค๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ์ „์ด ๋ฉ”์ปค๋‹ˆ์ฆ˜(cascadeType.ALL)์ด๋‚˜ ์ž๋™ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ˆ˜์ค€ ๋‚™๊ด€์  ์ž ๊ธˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜(@Version)์˜ ์žฅ์ ์„ ์–ป์ง€ ๋ชปํ•˜๋ฉฐ, ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•œ ์ˆ˜์ • ์‚ฌํ•ญ์ด ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์— ์ž๋™์œผ๋กœ ๋ฐ˜์˜๋˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์ ๋„ ์กด์žฌํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, @Version ํ•„๋“œ๋ฅผ ๊ฐ€์ง€๋Š” ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด ์‚ญ์ œ๋ฅผ ์ง„ํ–‰ํ•  ๊ฒฝ์šฐ ํ•ด๋‹น ์—”ํ‹ฐํ‹ฐ์˜ ๋ฒ„์ „ ์ •๋ณด๋Š” ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์œผ๋ฉฐ, ๋‹ค๋ฅธ ํŠธ๋žœ์žญ์…˜์—์„œ ์ด ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋Š” ๊ฒฝ์šฐ ๋‚™๊ด€์  ์ž ๊ธˆ ์ถฉ๋Œ์„ ๊ฐ์ง€ํ•˜์ง€ ๋ชปํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

 

2. deleteInBatch(Iterable<T> entities) ์‚ญ์ œ

SimpleJpaRepository.java#L298

@Transactional
public void deleteAuthorsDeleteInBatch() {
    List<Author> authors = authorRepository.findAll();
    
    authorRepository.deleteInBatch(authors);
}
delete a1_0 from author a1_0 where a1_0.id=? or a1_0.id=? or a1_0.id=? ...

์ผ๋‹จ ์ด deleteInBatch() ๋ฉ”์„œ๋“œ๋Š” ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ๊ฐ€ ํ•˜๋‚˜ ์žˆ๋Š”๋ฐ, IN ์ ˆ์ด ์•„๋‹Œ OR ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ WHERE ์ ˆ์— ํ•ด๋‹นํ•˜๋Š” ID๋“ค์„ ๋‹จ์ˆœํžˆ ์—ฐ๊ฒฐํ•œ๋‹ค. MySQL 8 ๋ฒ„์ „ ๊ธฐ์ค€์œผ๋กœ IN์ ˆ์€ 500,000๊ฐœ์˜ row๋ฅผ ๋ฌธ์ œ์—†์ด ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ˜๋ฉด, OR ์—ฐ์‚ฐ์ž๋Š” 10,000๊ฐœ ์ด์ƒ์˜ ํ–‰์„ ์‚ญ์ œํ•˜๋ ค๊ณ  ํ•  ๋•Œ StackOverflowError๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

์‹ค์ œ๋กœ 10,000๊ฐœ ์ด์ƒ์˜ row ์‚ญ์ œ๋ฅผ ์‹œ๋„ํ•˜์ž ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

java.lang.StackOverflowError: null
    at org.antlr.v4.runtime.ParserRuleContext.getRuleContext(ParserRuleContext.java:277) ~[antlr4-runtime-4.13.0.jar:4.13.0]
    at org.hibernate.grammars.hql.HqlParser$OrPredicateContext.predicate(HqlParser.java:6509) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitOrPredicate(SemanticQueryBuilder.java:2382) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitOrPredicate(SemanticQueryBuilder.java:275) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.grammars.hql.HqlParser$OrPredicateContext.accept(HqlParser.java:6523) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    at org.hibernate.query.hql.internal.SemanticQueryBuilder.visitOrPredicate(SemanticQueryBuilder.java:2382) ~[hibernate-core-6.5.2.Final.jar:6.5.2.Final]
    ...

deleteAllInBatch()์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ deleteInBatch()๋„ executeUpdate()๋ฅผ ํ†ตํ•ด ๋ฒŒํฌ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•ด์•ผ ํ•  ๋•Œ๋Š” deleteInBatch() ๋Œ€์‹  deleteAllInBatch()๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. ๋‹ค๋งŒ ํŠน์ • ํ•„ํ„ฐ๋ง ๊ธฐ์ค€์— ๋”ฐ๋ผ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•  ๋•Œ๋Š” ์ด ๋ฐฉ๋ฒ•๋„ ์ž๋™ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ˆ˜์ค€ ๋‚™๊ด€์  ์ž ๊ธˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์˜ ์ด์ ์„ ์–ป์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์— ์ฃผ์˜ํ•ด์•ผ ํ•œ๋‹ค.

๋งŒ์•ฝ ์ƒ์„ฑ๋œ ์ฟผ๋ฆฌ์˜ ๊ธธ์ด์™€ ๊ด€๋ จ๋œ ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ๋Œ€์•ˆ์œผ๋กœ ์•„๋ž˜์ฒ˜๋Ÿผ ์ง์ ‘ IN ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•œ ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ์„ ํƒ์ด ๋  ์ˆ˜ ์žˆ๋‹ค.

@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("delete from Author a WHERE a IN ?1")
int deleteInBulk(List<Author> authors);

 

3. deleteAll() ์‚ญ์ œ

@Transactional
public void deleteAuthorsDeleteAll() {   
    authorRepository.deleteAll();
}
delete from author where id=?
delete from author where id=?
delete from author where id=?
...

์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋ฐ˜๋ณต์ ์ธ delete()๊ฐ€ ์ˆ˜ํ–‰๋˜์—ˆ๋‹ค. ๊ทธ ์ด์œ ๋Š” ์œ„์— ๋งํฌ๋œ ์‹ค์ œ ๊ตฌํ˜„์ฝ”๋“œ๋ฅผ ๋ณด๋ฉด ์•Œ ์ˆ˜ ์žˆ๋Š”๋ฐ, deleteAll(Iterable <? extends T> extends) ๋ฉ”์„œ๋“œ๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ delete(T entity) ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์ธ์ˆ˜๋ฅผ ๋ฐ›์ง€ ์•Š๋Š” deleteAll() ๋ฉ”์„œ๋“œ๋„ ๋‚ด๋ถ€์ ์œผ๋กœ findAll()์„ ํ˜ธ์ถœํ•˜์—ฌ, ๋ฐ˜ํ™˜๋œ ์—”ํ‹ฐํ‹ฐ ์ง‘ํ•ฉ์„ ๋ฐ˜๋ณตํ•˜๋ฉด์„œ ๊ฐ ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•ด delete(T entity) ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค. ๊ฐœ๋ฐœ์ž๊ฐ€ deleteAll()๋‚˜ deleteAll(Iterable entities) ๋ฉ”์„œ๋“œ ๋˜๋Š” delete(T entity)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๊ฐ€ ์‚ฌ์šฉ๋œ๋‹ค.

๊ฒฐ๋ก ์ ์œผ๋กœ ๋ชจ๋“  ๋ ˆ์ฝ”๋“œ๋ฅผ ์‚ญ์ œํ•ด์•ผ ํ•  ๊ฒฝ์šฐ์—๋Š” deleteAllInBatch()๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์ด๋‹ค. ๊ทธ ์™ธ์˜ deleteInBatch()๋‚˜ deleteAll() ์‚ฌ์ด์˜ ๊ฒฐ์ •์€ ๊ฐ๊ฐ์˜ ๋ฉ”์„œ๋“œ๊ฐ€ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ์‚ญ์ œ ๋ฐฉ์‹(์ „์ด ๋ฉ”์ปค๋‹ˆ์ฆ˜), ์„ฑ๋Šฅ ์ฐจ์ด, ๊ทธ๋ฆฌ๊ณ  ๋‚™๊ด€์  ์ž ๊ธˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜์˜ ์ ์šฉ ์—ฌ๋ถ€๋“ค์— ๋Œ€ํ•ด ๊ณ ๋ คํ•ด์„œ ์„ ํƒํ•ด์•ผ ํ•œ๋‹ค.

 

 

์—ฐ๊ด€๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ

๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ง€์—ฐ ์–‘๋ฐฉํ–ฅ @OneToMany ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ๊ฐ–๊ณ  ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ…Œ์ด๋ธ”์€ ๋ฌผ๋ฆฌ์ ์œผ๋กœ FK๊ฐ€ ๊ฑธ๋ ค์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์ง€๋งŒ, ๊ฐ ๋‚ด์šฉ์—์„œ ๊ฑธ๋ ค์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ์— ์ˆ˜ํ–‰๋˜๋Š” ๋™์ž‘๋„ ์‚ดํŽด๋ณผ ๊ฒƒ์ด๋‹ค.

@Entity
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private Author author;
 
    private String title;
    private String isbn;
}
@Entity
@NoArgsConstructor(access = lombok.AccessLevel.PROTECTED)
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    private String name;
    private int age;
 
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "author", orphanRemoval = true)
    private List<Book> books = new ArrayList<>();
  }
SET @i = 1;
WHILE @i <= 1000 DO
    INSERT INTO author (name) VALUES (CONCAT('Author ', @i));
    SET @i = @i + 1;
END WHILE;

SET @author_id = 1;
WHILE @author_id <= 1000 DO
    INSERT INTO book (author_id, title, isbn) VALUES (@author_id, CONCAT('Book Title ', @author_id, '-1'), CONCAT('ISBN', @author_id, '-1'));
    INSERT INTO book (author_id, title, isbn) VALUES (@author_id, CONCAT('Book Title ', @author_id, '-2'), CONCAT('ISBN', @author_id, '-2'));
    INSERT INTO book (author_id, title, isbn) VALUES (@author_id, CONCAT('Book Title ', @author_id, '-3'), CONCAT('ISBN', @author_id, '-3'));
    INSERT INTO book (author_id, title, isbn) VALUES (@author_id, CONCAT('Book Title ', @author_id, '-4'), CONCAT('ISBN', @author_id, '-4'));
    INSERT INTO book (author_id, title, isbn) VALUES (@author_id, CONCAT('Book Title ', @author_id, '-5'), CONCAT('ISBN', @author_id, '-5'));
    
    SET @author_id = @author_id + 1;
END WHILE;

ํ…Œ์ด๋ธ” ๊ด€๊ณ„

์ €์ž๋ฅผ ์‚ญ์ œํ•˜๋ฉด ์—ฐ๊ด€๋œ ๋ชจ๋“  ๋„์„œ๋„ ํ•จ๊ป˜ ์‚ญ์ œ๋˜์–ด์•ผ ํ•œ๋‹ค. ๋ฐ์ดํ„ฐ๋Š” ์ €์ž 1,000๋ช…์— ์ €์ž๋งˆ๋‹ค 5๊ฐœ์˜ ์ฑ… ๋ฐ์ดํ„ฐ๋ฅผ ๋“ฑ๋กํ–ˆ๋‹ค.

 

1. deleteAllInBatch() ์‚ญ์ œ

@Transactional
public void deleteAuthorsAndBooksDeleteAllInBatch() {
    authorRepository.deleteAllInBatch();
}
delete a1_0 from author a1_0

java.sql.SQLIntegrityConstraintViolationException: (conn=109) Cannot delete or update a parent row: a foreign key constraint fails (`test`.`book`, CONSTRAINT `FKklnrv3weler2ftkweewlky958` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`))
...

์ด ๊ฒฝ์šฐ author ํ…Œ์ด๋ธ”์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋Š” DELETE ์ฟผ๋ฆฌ๋Š” ์ƒ์„ฑ๋˜์ง€๋งŒ, book์—์„œ author_id๋ฅผ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด์œผ๋กœ ์ธํ•ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค. ์ด๋Š” deleteAllInBatch()๊ฐ€ orphanRemoval ๋˜๋Š” ์ „์ด(cascade)๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋ฐœ์ƒํ•˜๋Š”๋ฐ, deleteAllInBatch()๋Š” executeUpdate()๋ฅผ ํ†ตํ•ด ๋ฒŒํฌ ์‚ญ์ œ๋ฅผ ํŠธ๋ฆฌ๊ฑฐ ํ•˜์ง€๋งŒ, JPA์˜ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ„์˜ ๋™๊ธฐํ™”๋Š” ์ด๋ฃจ์–ด์ง€์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์‰ฝ๊ฒŒ ๋งํ•ด deleteAllInBatch()์™€ ๊ฐ™์€ ๋ฒŒํฌ ์—ฐ์‚ฐ์€ JPA์˜ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ฑด๋„ˆ๋›ฐ๊ณ  ์ง์ ‘์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ‘๊ทผํ•ด ์‚ญ์ œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, orphanRemoval ๋ฐ cascade ์˜ต์…˜์ด ๋ฌด์‹œ๋œ๋‹ค. ์ด๋กœ ์ธํ•ด ์‚ญ์ œํ•˜๋ ค๋Š” ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋‹ค๋ฅธ ์—”ํ‹ฐํ‹ฐ์— ์˜ํ•ด ์ฐธ์กฐ๋˜๊ณ  ์žˆ์„ ๊ฒฝ์šฐ ์ œ์•ฝ ์กฐ๊ฑด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฌผ๋ฆฌ์ ์œผ๋กœ ํ…Œ์ด๋ธ”์— ์™ธ๋ž˜ํ‚ค๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋Š” ์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด ๋ฌธ์ œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์ง€๋งŒ, Author ํ…Œ์ด๋ธ”์˜ ๋ ˆ์ฝ”๋“œ๋งŒ ์‚ญ์ œ๋˜๊ณ  Book ํ…Œ์ด๋ธ”์˜ ๋ ˆ์ฝ”๋“œ๋Š” ์‚ญ์ œ๋˜์ง€ ์•Š๋Š”๋‹ค.

๋”ฐ๋ผ์„œ ์ด ๊ฒฝ์šฐ ๋ชจ๋“  ๋„์„œ๋ฅผ ์‚ญ์ œํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ช…์‹œ์ ์œผ๋กœ book๋„ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ๋ฒ•๋ฟ์ด๋‹ค.

// ๋ฌผ๋ฆฌ์ ์œผ๋กœ FK ๊ฑธ๋ ค์žˆ์„ ์‹œ book ์‚ญ์ œ๊ฐ€ ์šฐ์„ 
@Transactional
public void deleteAuthorsAndBooksDeleteAllInBatch() {
    bookRepository.deleteAllInBatch();
    authorRepository.deleteAllInBatch();
}

// ๋ฌผ๋ฆฌ์ ์œผ๋กœ FK ๊ฑธ๋ ค์žˆ์ง€ ์•Š์„ ์‹œ ์ˆœ์„œ ์ƒ๊ด€ ์—†์Œ
@Transactional
public void deleteAuthorsAndBooksDeleteAllInBatch() {
    authorRepository.deleteAllInBatch();
    bookRepository.deleteAllInBatch();
}

 

2. deleteInBatch() ์‚ญ์ œ

@Transactional
public void deleteAuthorsAndBooksDeleteInBatch() {
    List<Author> authors = authorRepository.findAll();
    
    authorRepository.deleteInBatch(authors);
}
delete a1_0 from author a1_0 where a1_0.id=? or a1_0.id=? or a1_0.id=? or a1_0.id=? or a1_0.id=? or a1_0.id=? ...

java.sql.SQLIntegrityConstraintViolationException: (conn=109) Cannot delete or update a parent row: a foreign key constraint fails (`test`.`book`, CONSTRAINT `FKklnrv3weler2ftkweewlky958` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`))
...

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ IN ์ ˆ์ด ์•„๋‹Œ OR ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ WHERE ์ ˆ์— ํ•ด๋‹นํ•˜๋Š” ID๋“ค์„ ๋‹จ์ˆœํžˆ ์—ฐ๊ฒฐํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, deleteInBatch()๋„ deleteAllInBatch()์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ orphanRemoval ๋˜๋Š” ์ „์ด(cascade)๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด์œผ๋กœ ์ธํ•ด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

์ด ๊ฒฝ์šฐ์—๋„ ๋ฌผ๋ฆฌ์ ์œผ๋กœ ํ…Œ์ด๋ธ”์— ์™ธ๋ž˜ํ‚ค๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ์ง€ ์•Š์„ ์‹œ ์™ธ๋ž˜ ํ‚ค ์ œ์•ฝ ์กฐ๊ฑด ๋ฌธ์ œ๋Š” ๋ฐœ์ƒํ•˜์ง€ ์•Š์ง€๋งŒ, Author ํ…Œ์ด๋ธ”์˜ ๋ ˆ์ฝ”๋“œ๋งŒ ์‚ญ์ œ๋˜๊ณ  Book ํ…Œ์ด๋ธ”์˜ ๋ ˆ์ฝ”๋“œ๋Š” ์‚ญ์ œ๋˜์ง€ ์•Š๋Š”๋‹ค.

 

3. deleteAll() ์‚ญ์ œ

@Transactional
public void deleteAuthorsAndBooksDeleteAll() {
    List<Author> authors = authorRepository.findAll();
    
    authorRepository.deleteAll(authors);
}
delete from book where id=?
delete from book where id=?
delete from book where id=?
delete from book where id=?
delete from book where id=?
delete from author where id=?

delete from book where id=?
delete from book where id=?
delete from book where id=?
delete from book where id=?
delete from book where id=?
delete from author where id=?
...

deleteAll() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด, ๊ฐ Author ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ญ์ œํ•  ๋•Œ๋งˆ๋‹ค ๊ด€๋ จ๋œ Book ์—”ํ‹ฐํ‹ฐ๊ฐ€ ๋ชจ๋‘ ์ œ๊ฑฐ๋œ ํ›„ ํ•ด๋‹น Author ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์‚ญ์ œ๋œ๋‹ค. ์œ„์˜ ๊ฒฝ์šฐ ํ•˜๋‚˜์˜ Author๊ฐ€ 5๊ฐœ์˜ Book์„ ๊ฐ€์ง€๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ Book์„ ์‚ญ์ œํ•˜๋Š” ์ฟผ๋ฆฌ๊ฐ€ 5๋ฒˆ ์‹คํ–‰๋œ ํ›„ Author ์‚ญ์ œ ์ฟผ๋ฆฌ๊ฐ€ ์‹คํ–‰๋˜๋Š” ์‹์œผ๋กœ ๋ฐ˜๋ณต๋œ๋‹ค.

๋‹ค๋งŒ ์ด ๋ฐฉ์‹์€ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๊ฐ€ ์‚ฌ์šฉ๋˜์ง€๋งŒ ์ตœ์ ํ™”๋˜์–ด์žˆ์ง€๋Š” ์•Š๋‹ค. DELETE๋ฌธ์€ ์ •๋ ฌ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ์ด ์ž‘์—…์— ํ•„์š”ํ•œ ๊ฒƒ๋ณด๋‹ค ๋” ๋งŽ์€ ๋ฐฐ์น˜๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค. ๋ฐฐ์น˜๋Š” ํ•˜๋‚˜์˜ ํ…Œ์ด๋ธ”๋งŒ ๋Œ€์ƒ์œผ๋กœ ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— Book๊ณผ Author ํ…Œ์ด๋ธ”์„ ๋ฒˆ๊ฐˆ์•„ ์ฒ˜๋ฆฌํ•˜๋ฉด 5๊ถŒ์˜ ๋„์„œ๋ฅผ ๊ฐ€์ง„ 10๋ช…์˜ ์ €์ž๋ฅผ ์‚ญ์ œํ•  ๊ฒฝ์šฐ 10 * 2 = 20๊ฐœ์˜ ๋ฐฐ์น˜๊ฐ€ ํ•„์š”ํ•˜๊ฒŒ ๋œ๋‹ค. ๊ฐ ์ €์ž๊ฐ€ ๊ฐ์ž์˜ ๋ฐฐ์น˜์—์„œ ์‚ญ์ œ๋˜๊ณ  ๊ทธ์— ๋”ฐ๋ฅธ ๋„์„œ๊ฐ€ ๋‹ค๋ฅธ ๋ฐฐ์น˜์—์„œ ์‚ญ์ œ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋น„ํšจ์œจ์ ์ด๋‹ค.

๋”ฐ๋ผ์„œ ์กฐ๊ธˆ ๋” ๋ฐฐ์น˜ ์ˆ˜๋ฅผ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์—…ํ•  ์ˆ˜ ์žˆ๋‹ค.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Author {
    ...

    public void removeBooks() {
        Iterator<Book> iterator = this.books.iterator();
        while (iterator.hasNext()) {
            Book book = iterator.next();
            book.setAuthor(null);

            iterator.remove();
        }
    }
}
@Transactional
public void deleteAuthorsAndBooksDeleteAll() {
    List<Author> authors = authorRepository.findAll();

    authors.forEach(Author::removeBooks);
    authorRepository.flush();

    authorRepository.deleteAll(authors);
}
delete from book where id=?
delete from book where id=?
delete from book where id=?
delete from book where id=?
delete from book where id=?
...

delete from author where id=?
delete from author where id=?
delete from author where id=?
delete from author where id=?
...

์ž˜ ๋ณด๋ฉด ์ด์ „๊ณผ ๋‹ฌ๋ฆฌ ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  Book ๋ ˆ์ฝ”๋“œ๊ฐ€ ๋จผ์ € ์ œ๊ฑฐ๋œ ํ›„ Author๊ฐ€ ์‚ญ์ œ๋œ๋‹ค. ์—ฐ๊ด€๋œ ๋ชจ๋“  ๋„์„œ๋ฅผ ์‚ญ์ œํ•˜๋Š” ์ž‘์—…์ด ๋จผ์ € ์ˆ˜ํ–‰๋˜๋ฏ€๋กœ, orphanRemoval์ด true๋กœ ์„ค์ •๋˜์–ด ์žˆ๋Š” ๊ฒฝ์šฐ ์—ฐ๊ด€ ๊ด€๊ณ„๊ฐ€ ํ•ด์ œ๋œ ๋ชจ๋“  ๋„์„œ๊ฐ€ ์ž๋™์œผ๋กœ ์‚ญ์ œ๋˜๋ฉฐ, ์ƒ์„ฑ๋œ DELETE๋ฌธ์€ ๋ฐฐ์น˜๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค(orphanRemoval์ด false๋กœ ์„ค์ •๋œ ๊ฒฝ์šฐ, ์‚ญ์ œ ๋Œ€์‹  ์—ฌ๋Ÿฌ UPDATE ๋ฌธ์ด ์‹คํ–‰๋จ).

์ฆ‰ ์—ฐ๊ด€๊ด€๊ณ„๋ฅผ ๊ฐ–๋Š” ๋ชจ๋“  ๋„์„œ๋ฅผ ์‚ญ์ œํ•˜๋Š” ์ž‘์—…์ด ๋จผ์ € ์ˆ˜ํ–‰๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์ด ๋ฐฉ๋ฒ•์„ ํ†ตํ•ด ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์˜ ํšจ์œจ์„ฑ์„ ๋†’์—ฌ Author์™€ Book ๋ชจ๋‘์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

4. SQL, ON DELETE CASCADE ์‚ฌ์šฉ

์‚ฌ์‹ค ์—ฐ๊ด€ ๊ด€๊ณ„๊ฐ€ ์žˆ์„ ๋•Œ deleteAllInBatch()๋‚˜ deleteInBatch()๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Author๋งŒ ์‚ญ์ œํ•˜๋”๋ผ๋„ ์ž๋™์œผ๋กœ Book๊นŒ์ง€ ์‚ญ์ œ๋˜๋„๋ก ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์กด์žฌํ•˜๋Š”๋ฐ, SQL์˜ ์ „์ด ์‚ญ์ œ ์ง€์‹œ์ž์ธ ON DELETE CASCADE๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

ON DELETE CASCADE๋Š” ๋ถ€๋ชจ ํ–‰์ด ์‚ญ์ œ๋  ๋•Œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ž๋™์œผ๋กœ ์ž์‹ ํ–‰์„ ์‚ญ์ œํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ํ•˜์ด๋ฒ„๋„ค์ดํŠธ @OnDelete ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Author {
    ...
    
    @OneToMany(cascade = CascadeType.ALL, mappedBy = "author", orphanRemoval = true)
    @OnDelete(action = OnDeleteAction.CASCADE) // ์ถ”๊ฐ€
    private List<Book> books = new ArrayList<>();
}

ํ…Œ์ด๋ธ”์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝ๋œ๋‹ค.

ALTER TABLE book
ADD CONSTRAINT fk_author_book
FOREIGN KEY (author_id) REFERENCES author (id)
ON DELETE CASCADE;

์ด ์„ค์ •์„ ํ†ตํ•ด Author๊ฐ€ ์‚ญ์ œ๋  ๋•Œ ์—ฐ๊ด€๋œ Book๋„ ์ž๋™์œผ๋กœ ์‚ญ์ œ๋˜๋ฏ€๋กœ, ์ด์ „์— deleteAllInBatch()๋‚˜ deleteInBatch()๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ฝ”๋“œ ๋ ˆ๋ฒจ์—์„œ ๋ช…์‹œ์ ์œผ๋กœ Book ์—”ํ‹ฐํ‹ฐ๋ฅผ ์‚ญ์ œํ•  ํ•„์š”๊ฐ€ ์—†๊ฒŒ๋œ๋‹ค.