MySQL ๊ธฐ๋ฐ์ผ๋ก ๋๋ ์ ๋ฐ์ดํธ๋ฅผ ์งํํ๋ ๊ฒฝ์ฐ JPA, Exposed ํ๋ ์์ํฌ ๊ธฐ๋ฐ์ผ๋ก ํ ์คํธ๋ฅผ ์งํํ์ต๋๋ค. ๊ฒฐ๋ก ๋ถํฐ ๋ง์๋๋ฆฌ๋ฉด Exposed ๊ธฐ๋ฐ Batch Update๊ฐ ๊ฐ์ฅ ๋นจ๋์ต๋๋ค. ๋ฌผ๋ก JPA์์๋ addBatch ๋ฐฉ์์ ์งํํ๋ฉด ์ ์๋ฏธํ ์๋ ์ฐจ์ด๋ ์์ ๊ฒ ๊ฐ์ ๋ณด์ด๋ Exposed๊ฐ addBatch ๊ธฐ๋ฅ์ ์ง๊ด์ ์ผ๋ก ์ง์ํ๊ณ ์์ด addBatch ๋ฐฉ์์ Exposed๋ฅผ ์ฌ์ฉํ์ผ๋ฉฐ JPA๋ ์์์ฑ ์ปจํ ์คํธ ๊ธฐ๋ฐ์ธ Dirty Checking Update, ์์์ฑ ์ปจํ ์คํธ๊ฐ ํ์ ์๋ ID ๊ธฐ๋ฐ ์ ๋ฐ์ดํธ๋ฅผ ์งํํ์ต๋๋ค.
// JPA Object
@Entity
@Table(name = "writer")
class Writer(
@Column(name = "name", nullable = false)
var name: String,
@Column(name = "email", nullable = false)
var email: String,
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
internal set
@CreationTimestamp
@Column(name = "created_at", nullable = false, updatable = false)
lateinit var createdAt: LocalDateTime
internal set
@UpdateTimestamp
@Column(name = "updated_at", nullable = false)
lateinit var updatedAt: LocalDateTime
internal set
}
internal class WriterTest(
private val writerService: WriterService
) : SpringBootTestSupport() {
@Test
internal fun `dirty checking update test`() {
// ์
๋ฐ์ดํธ ๋์ rows, 50, 100, 500, 1,000, 5,000, 10,000, 50,000, 100,000
val total = 500
val map = (1..total).map {
Writer(
name = "old",
email = "old"
)
}
// ๋ฐ์ดํฐ ์
์
, ์๋ ์ธก์ ํฌํจ X
setup(map)
// ๋ฐ์ดํฐ ์กฐํ, ์๋ ํน์ X
val writers = writerService.findAll()
val stopWatch = StopWatch()
// ์
๋ฐ์ดํธ ์๋ ์ธก์
stopWatch.start()
writerService.update(writers)
stopWatch.stop()
println("${map.size}, ${stopWatch.lastTaskTimeMillis}")
}
@Test
internal fun `none persistence context update test`() {
// ์
๋ฐ์ดํธ ๋์ rows, 50, 100, 500, 1,000, 5,000, 10,000, 50,000, 100,000
val total = 500
val map = (1..total).map {
Writer(
name = "old",
email = "old"
)
}
// ๋ฐ์ดํฐ ์
์
, ์๋ ์ธก์ ํฌํจ X
setup(map)
val findAll = writerService.findAll()
// ์
๋ฐ์ดํธ ์๋ ์ธก์
val stopWatch = StopWatch()
stopWatch.start()
writerService.nonPersistContestUpdate(findAll.map { it.id!! })
stopWatch.stop()
println("${map.size}, ${stopWatch.lastTaskTimeMillis}")
}
}
class WriterCustomRepositoryImpl : QuerydslCustomRepositorySupport(Writer::class.java), WriterCustomRepository {
// ์์์ฑ ์ปจํ
์คํธ ์๋ ์
๋ฐ์ดํธ
@Transactional
override fun update(ids: List<Long>) {
for (id in ids) {
update(qWriter)
.set(qWriter.name, "update")
.where(qWriter.id.eq(id))
.execute()
}
}
}
JPA์์๋ Persistence Context ๊ธฐ๋ฐ์ธ Dirty Checking์ ํตํ ์ ๋ฐ์ดํธ์, Persistence Context ์์ด ์ํ์ ์ ๋ฐ์ดํธ๋ฅผ ์งํํ์ต๋๋ค.
class BatchInsertServiceTest(
...
) : ExposedTestSupport() {
@Test
fun `update`() {
// ์
๋ฐ์ดํธ ๋์ rows, 50, 100, 500, 1,000, 5,000, 10,000, 50,000, 100,000
val totalCount = 500
val ids = (1..totalCount).map { it.toLong() }
// ๋ฐ์ดํฐ ์
์
, ์๋ ์ธก์ ํฌํจ X
setup(ids)
// ๋ฐ์ดํฐ ์
์
, ์๋ ์ธก์ ํฌํจ X
val stopWatch = StopWatch()
stopWatch.start()
for (writerId in ids) {
Writers
.update({ Writers.id eq writerId })
{
it[email] = "update"
}
}
stopWatch.stop()
println("${ids.size} update, ${stopWatch.lastTaskTimeMillis}")
}
@Test
fun `bulk update`() {
// ์
๋ฐ์ดํธ ๋์ rows, 50, 100, 500, 1,000, 5,000, 10,000, 50,000, 100,000
val totalCount = 500
val ids = (1..totalCount).map { it.toLong() }
// ๋ฐ์ดํฐ ์
์
, ์๋ ์ธก์ ํฌํจ X
setup(ids)
// ์
๋ฐ์ดํธ ์๋ ์ธก์
val stopWatch = StopWatch()
stopWatch.start()
BatchUpdateStatement(Writers).apply {
ids.forEach {
addBatch(EntityID(it, Writers))
this[Writers.email] = "update"
}
}
.execute(TransactionManager.current())
stopWatch.stop()
println("${ids.size} update, ${stopWatch.lastTaskTimeMillis}")
}
}
Exposed๋ ์ผ๋ฐ ์ ๋ฐ์ดํธ์, addBatch๋ฅผ ํตํ batch update๋ฅผ ์งํ ํํ์ต๋๋ค.
JDBC ๋๋ผ์ด๋ฒ์์๋ addBatch()๋ฅผ ์ ๊ณตํ๊ณ ์์ต๋๋ค. ์ด ๊ธฐ๋ฅ์ rewriteBatchedStatements
์ต์
์ ํ์ฑํํ๋ฉด MySQL Connector/J๊ฐ addBatch() ํจ์๋ก ๋ ์ฝ๋๋ฅผ ๋ชจ์ MySQL ์๋ฒ๋ก ์ ๋ฌํฉ๋๋ค. ์ผ๋ฐ์ ์ผ๋ก Batch Insert๋ฅผ ์งํํ ๋ ๋ง์ด ์ฌ์ฉํ๋ ์ต์
์ผ๋ก Batch Insert ์ฑ๋ฅ ํฅ์๊ธฐ 1ํธ - With JPA, Batch Insert ์ฑ๋ฅ ํฅ์๊ธฐ 2ํธ - ์ฑ๋ฅ ์ธก์ ์์ ๋ค๋ฃฌ ์ ์์ต๋๋ค. Insert ์ฟผ๋ฆฌ ๊ฐ์ ๊ฒฝ์ฐ๋ addBatch()๋ฅผ ์ฌ์ฉํ๋ฉด ๋ค์๊ณผ ๊ฐ์ ํํ๋ก ๋ฌถ์ด์ ์คํ์์ผ ์ค๋๋ค.
-- addBatch() ์ฌ์ฉ์ ๋จ์ผ insert์์ ์๋ SQL ํํ๋ก ๋ณ๊ฒฝ
insert into writer (`name`, `email`, `created_at`, `updated_at`)
values ('old', 'old', '2022-11-06 13:48:14.135442', '2022-11-06 13:48:14.135442'),
('old', 'old', '2022-11-06 13:48:14.135442', '2022-11-06 13:48:14.135442'),
...
('old', 'old', '2022-11-06 13:48:14.135442', '2022-11-06 13:48:14.135442');
Update ์ฟผ๋ฆฌ๋ ํ์์ ๋ณ๊ฒฝ์ ์์ง๋ง ๋ ์ฝ๋๋ฅผ ๋ชจ์์ ํ ๋ฒ์ MySQL ์๋ฒ๋ก ์ ๋ฌํ์ฌ ๋คํธ์ํฌ ํต์ ์ ์ต์ํํ ์ ์์ต๋๋ค.
rows | JPA Dirty Checking Update | JPA None Persistence Context | Exposed Update | Exposed Bulk Update |
---|---|---|---|---|
50 | 115 ms | 167 ms | 80 ms | 23 ms |
100 | 206 ms | 242 ms | 130 ms | 40 ms |
500 | 71 8ms | 994 ms | 596 ms | 135 ms |
1,000 | 1,388 ms | 1,540 ms | 1,130 ms | 381 ms |
5,000 | 6,204 ms | 6,441 ms | 5,121 ms | 1,127 ms |
10,000 | 12,151 ms | 12,209 ms | 10,094 ms | 2,227 ms |
50,000 | 65,309 ms | 56,295 ms | 46,506 ms | 10,355 ms |
100,000 | 120,906 ms | 11,3194 ms | 99,349 ms | 21,370 ms |
ํด๋น ํ ์คํธ ํ๊ฒฝ์ ๋ก์ปฌ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ก์ปฌ MySQL ํต์ ์ผ๋ก ์งํํ๊ธฐ ๋๋ฌธ์ ๋คํธ์ํฌ ๋ฆฌ์์ค ๋น์ฉ์ด ํฌ๊ฒ ๋ฐ์ํ์ง ์์์์๋ Exposed ๊ธฐ๋ฐ์ Batch Update ์ฑ๋ฅ์ด ๊ฐ์ฅ ์ข์์ต๋๋ค. ์ค์ ์ด์ ํ๊ฒฝ์์๋ ๋ฌผ๋ก Exposed Bulk Update๋ ์๊ฐ์ด ๋ ์ค๋ ๊ฑธ๋ฆฌ๊ฒ ์ง๋ง ๋ค๋ฅธ ์ ๋ฐ์ดํธ ๋ฐฉ๋ฒ๋ค์ ๋คํธ์ํฌ ๋ฆฌ์์ค๊ฐ ๋์์ง์ ๋ฐ๋ผ ๋ ๋ง์ ์๊ฐ์ด ๋ฐ์ํ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค.
๊ทธ๋ฆฌ๊ณ JPA์์๋ Dirty Checking Update, None Persistence Context์ ์ฑ๋ฅ ์ฐจ์ด๋ ์๊ฐ๋ณด๋ค ํฌ๊ฒ ๋ฐ์ํ์ง ์์์ต๋๋ค. ๋ฌผ๋ก ์์์ฑ ์ปจํ ์คํธ๊ฐ ๋ฐ๋์ ํ์ํ๋ ์กฐํ์ ๋ํ ๋ถ๋ถ๊น์ง ํฌํจ ์ํค๋ฉด ์ ์๋ฏธํ ์ฐจ์ด๊ฐ ์์ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค. ํ์ง๋ง ์ด๋ฐ ๋๋ ์กฐํ์ ๊ฒฝ์ฐ ์์์ฑ ์ปจํ ์คํธ๋ฅผ ํตํ์ง ์๊ณ Projections์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด๋ผ ๊ทธ ๋ถ๋ถ๊น์ง ํ ์คํธํ์ง ์์์ต๋๋ค. JPA ๊ธฐ๋ฐ์ผ๋ก ๋๋ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ๋ ๊ฒฝ์ฐ ๊ฐ๋ฅํ๋ฉด Projections์ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅ ๋๋ฆฝ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ด๋ฐ ๋๋ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ํน์ฑ์ ๋ฐฐ์น ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๊ตฌ์ฑํ๊ณ Chunk ๋จ์๋ก ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฌธ์ 100,000 ์ ๋์ ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๊ฒ์ ๊ถ์ฅํ์ง ์์ต๋๋ค. ๋ฐ์ดํฐ ๋ชจ์์ ์ฒ๋ฆฌ ์๊ฐ์ ๋ํ ์๊ด๊ด๊ณ๋ฅผ ํ์ธํ๊ธฐ ์ํด ์์ ํ์ต๋๋ค.
์ค์ ์ด์ ํ๊ฒฝ์์์ ๋คํธ์ํฌ ํต์ ๋น์ฉ์ ๋ฐ๋ผ์ addBatch() ๋ฐฉ์๊ณผ, ๊ทธ๋ ์ง ์์ ๋จ๊ฑด ์ ๋ฐ์ดํธ ๋ฐฉ์์ ์ฒ๋ฆฌ ์๊ฐ์ ๋ ์ฐจ์ด๊ฐ ๋ ๊ฒ์ผ๋ก ๋ณด์ด๋ฉฐ, ๊ตฌ์กฐ์ ์ผ๋ก ํฐ ๋ณ๊ฒฝ ์์ด ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ ๋ฐฉ์๋ง ๋ฐ๊พธ๋ ๊ฒ์ผ๋ก 6๋ฐฐ ๊ฐ๊น์ด ํฅ์์ด ์๊ธฐ ๋๋ฌธ์ ๋์ฉ๋ ์ ๋ฐ์ดํธ ์ฒ๋ฆฌ๋ฅผ ํ๊ณ ์๋ค๋ฉด ๊ถ์ฅ ๋๋ฆฝ๋๋ค. JPA๋ ์ ๋ง ์ข์ ORM ํ๋ ์์ํฌ๊ฐ ์๊ฐ์ด ๋ค์ง๋ง ๋๋ ์ฒ๋ฆฌ์ ๋ํ ๋๊ตฌ๋ก๋ ์ ์ ํ์ง ์๋ค๋ ์๊ฐ์ด ๋ง์ด ๋ญ๋๋ค. MySQL ๊ธฐ๋ฐ์ ๋์ฉ๋ ์ฒ๋ฆฌ๋ฅผ ์งํํ๋ ๊ฒฝ์ฐ ๋ค๋ฅธ ์ ์ ํ ๋๊ตฌ๋ฅผ ์ฐพ์๋ณด๋ ๊ฒ์ด ์ข์ ๊ฑฐ ๊ฐ์ต๋๋ค.