-
Notifications
You must be signed in to change notification settings - Fork 2
๐ ํธ๋ฌ๋ธ ์ํ
๋ฌธ์๋ฅผ ์ญ์ ํ ๋, RDB(MySQL)๊ณผ MongoDB์ ๋ถ์ฐ๋ ๋ฐ์ดํฐ๊ฐ ํจ๊ป ์ญ์ ๋์ด์ผ ํ๋ค.
ํ์ง๋ง ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ ๋จ์ผ ํธ๋์ญ์
์ ๋ฌถ๋ ๊ฒ์ ๋ถ๊ฐ๋ฅํ๋ฉฐ(ํ๋์ @Transactional๋ก ์ฒ๋ฆฌ X), ์ฒ๋ฆฌ ์์๋ ์คํจ ๋ณต๊ตฌ๋ฅผ ์ ์ค๊ณํ์ง ์์ผ๋ฉด ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ๋ถ์ผ์น๊ฐ ๋ฐ์ํ ์ ์๋ค.
ํ์ฌ ๊ตฌ์กฐ:
| db | ์ ์ฅ ๋ด์ฉ |
|---|---|
| RDB(JPA ๊ธฐ๋ฐ) | Doc, Branch, Edge์ Save, Commit์ ๋ฉํ๋ฐ์ดํฐ (์ ๋ชฉ, ์ค๋ช , ์์ฑ/์์ ์ผ์ etc) |
| MongoDB | SaveContent, Block, CommitsBlockSequence (Save์ Commit์ ๋ณธ๋ฌธ, Commit์ ๋ธ๋ก ๋ฆฌ์คํธ) |
์ ์ฅ์ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ ์ํ์ด ๋ฐ์ํ ์ ์๋ค:
- Mongo ๋จผ์ ์ ์ฅ โ ์ดํ RDB ์คํจ: Mongo์ ์ฐ๋ ๊ธฐ ๋ฐ์ดํฐ ๋จ์
- RDB ๋จผ์ ์ ์ฅ โ ์ดํ Mongo ์คํจ: RDB์ ๋ด์ฉ์ด ์๋ ์ ์ฅ์ด ์๊น
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ์ ์ฅ ๋ก์ง์ ๊ณ ์ํ์๋ค:
โ MongoDB ์ ์ฅ ํ โ ์ฑ๊ณต ์ ID๋ฅผ ๋ฐ์ โ RDB์ ์ ์ฅ
์์ ํ๋ก์ธ์ค: RDB์ @Transactional ์์์ ๋ค์ ์ฒ๋ฆฌ ์คํ
try {
1. [RDB] MongoID๋ฅผ ์ ์ธํ Save ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ฐ์ RDB์ ์ ์ฅ
2. [MongoDB] SaveContent๋ฅผ MongoDB์ ์ ์ฅ โ saveContentId ๋ฐํ
3. [RDB] ๋ฐํ๋ฐ์ saveContentId๋ฅผ ๊ธฐ์กด Save(RDB)์ ์
๋ฐ์ดํธ
4. ์ต์ข
์ ์ผ๋ก ๋ชจ๋ ์ ์ฅ ์ฑ๊ณต โ ํธ๋์ญ์
์ปค๋ฐ
} catch (์์ธ ๋ฐ์ ์) {
- [MongoDB ์๋ ๋กค๋ฐฑ] ์คํจํ Mongo ์ ์ฅ ๋ด์ฉ ์ง์ ์ญ์ (saveContentId ๋ฑ)
- ์์ธ ์ข
๋ฅ์ ๋ฐ๋ผ ์ ์ ํ CustomException ๋ฐ์ ํ
- ์ ์ฒด ํธ๋์ญ์
๋กค๋ฐฑ
}
์ญ์ ์ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ ์ํ์ด ๋ฐ์ํ ์ ์๋ค:
Mongo ์ญ์ ์ค ์์ธ โ RDB๋ ์ด๋ฏธ ์ญ์ ์๋ฃ ์ํ โ ๋กค๋ฐฑ ๋ถ๊ฐ๋ก ๋ฐ์ดํฐ ์ ํฉ์ฑ ๊นจ์ง
๋ฐ๋๋ก Mongo๋ ์ญ์ ๋๋๋ฐ RDB ํธ๋์ญ์ ์ด ๋กค๋ฐฑ๋๋ค๋ฉด โ MongoDB์ ๊ณ ์ ๋ฐ์ดํฐ ๋ฐ์
์ญ์ ๋ ์ ์ฅ๊ณผ ๋ฌ๋ฆฌ RDB๋ง ๋จผ์ ์ญ์ ๋๊ณ MongoDB์ ๊ณ ์ ๋ฐ์ดํฐ๊ฐ ๋จ๋๋ผ๋ UX ๊ด์ ์์ ๋ฌธ์ ๊ฐ ์์ผ๋ฏ๋ก, Mongo ์ญ์ ๋ฅผ RDB ํธ๋์ญ์ ์ดํ๋ก ๋ถ๋ฆฌํ๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ ์ญ์ ๋ก์ง์ ๊ณ ์ํ์๋ค:
โ ์ด๋ฒคํธ ๊ธฐ๋ฐ ์ฒ๋ฆฌ + ํธ๋์ญ์ ๋ถ๋ฆฌ + ์ฌ์๋ + ๋น๋๊ธฐ
- RDB ์ญ์
- Doc, Branch, Commit, Save ๋ฑ(RDB)์ ํ๋์
@Transactional๋ด์์ ์ญ์ - ๋์์ ์ญ์ ํด์ผํ Mongo ๊ด๋ จ ID๋ฅผ ์์งํ์ฌ
DocDeleteMongoIdsDto์ ๋ด๊ณ -
eventPublisher.publishEvent()๋ก ์ญ์ ์ด๋ฒคํธ ๋ฐํ
- MongoDB ์ญ์
-
@TransactionalEventListener(phase = AFTER_COMMIT)๋ฅผ ํตํด RDB ํธ๋์ญ์ ์ด ์ฑ๊ณต์ ์ผ๋ก ์ปค๋ฐ๋ ํ์๋ง Mongo ์ญ์ ๋ฅผ ์๋ -
@Retryable์ AOP ๊ธฐ๋ฐ์ผ๋ก ํ๋ก์๋ฅผ ํตํด ํธ์ถ๋ผ์ผ ์๋ํ๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ๋ฆฌ์ค๋์์ ์ง์ ์ญ์ ํ์ง ์์ (๋ฆฌ์ค๋์์ ์ง์ ํธ์ถ ์ ๋์ ์ ํจ)
- Mongo ์ญ์ : ์ฌ์๋
- MongoDB๋ Atlas ๊ธฐ์ค ๊ธฐ๋ณธ์ ์ผ๋ก Replica Set์ ์ง์ํ๋ฏ๋ก, ํธ๋์ญ์ ์ฌ์ฉ์ด ๊ฐ๋ฅํจ
-
MongoTransactionManager๋ฅผ@Bean์ผ๋ก ๋ฑ๋กํด ํธ๋์ญ์ ๋ถ๋ฆฌ
- ์ญ์ ์คํจ โ
@Recover์ง์
- ์ญ์ ๊ฐ 3ํ ๋ชจ๋ ์คํจํ๋ฉด
@Recover ๋ฉ์๋๋ก ์ด๋ - ์ด ์์ ์๋ ํธ๋์ญ์
์ปจํ
์คํธ๊ฐ ์ข
๋ฃ๋์ด ์์ผ๋ฏ๋ก, ์ง์ RDB ์ ์ฅ ์๋ ์ ์์ธ ๋ฐ์ (
no transaction is in progress) -
@Async๋ก ์์ํ์ฌ ํธ๋์ญ์ ์ ์๋ก ์์ฑํ๊ณ Mongo ์ญ์ ์คํจ ๋ด์ญ(MongoDeleteFailure)์ ์ ์ฅ
- Mongo ์ญ์ ์คํจ ๊ธฐ๋ก ์ ์ฅ
-
@Async + @Transactional์กฐํฉ์ผ๋ก ์ ํธ๋์ญ์ ์ปจํ ์คํธ๋ฅผ ์์ฑํ์ฌ RDB์ Mongo ์ญ์ ์คํจ ๋ด์ญ ์ ์ฅ
- ์คํจ ๋ด์ญ ์ฌ์๋: ์ค์ผ์ค๋ฌ ๊ธฐ๋ฐ
- ์ผ์ ์ฃผ๊ธฐ(1์๊ฐ?)๋ก
MongoDeleteFailure๋ด์ญ ์กฐํ - ์ญ์ ์ฑ๊ณต ์
resolved = true๋ก soft delete์ฒ๋ฆฌ
- ๋ฌธ์(Doc) ์ญ์ ์ ํ์ Branch, Commit, Edge๋ฅผ cascade = CascadeType.ALL๋ก ์ ๊ฑฐํ๋๋ก ๊ตฌ์ฑ๋์ด ์์์
- ๊ทธ๋ฌ๋ Doc ์ญ์ ์ Edge์์ SQL Error: 1048 ๋ฐ์ํจ
Column 'next_commit_id' cannot be null
๋ค์์ ๊ธฐ์กด์ ๊ด๋ จ ์ฝ๋์ ERD์ด๋ค.
- Doc ์ญ์ ๋ฉ์๋ (๊ธฐ์กด)
public void delete(Long docId, Long userId) {
User user = getUserOrThrow(userId);
Doc doc = getDocByIdAndUserId(docId, userId);
List<Branch> branches = doc.getBranches();
MongoIdsDto docDeleteMongoIds = mongoIdsCollector.collectFrom(branches);
user.removeDocument(doc);
eventPublisher.publishEvent(docDeleteMongoIds);- Edge ์ํฐํฐ
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "document_id", nullable = false)
private Doc doc;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "prev_commit_id", nullable = false)
private Commit prevCommit;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "next_commit_id", nullable = false)
private Commit nextCommit;- Doc ์ํฐํฐ
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@OneToMany(mappedBy = "doc", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Branch> branches;
@OneToMany(mappedBy = "doc", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Edge> edges;- ERD
- ์๋น์ค ๋ ์ด์ด์์ Doc ์ญ์ ์ Edge๋ฅผ ๋ช ์์ ์ผ๋ก ์ญ์ ํ๋๋ก ์์ :
public void delete(Long docId, Long userId) {
User user = getUserOrThrow(userId);
Doc doc = getDocByIdAndUserId(docId, userId);
List<Edge> edges = doc.getEdges(); // ๊ฐ์ ์์ง
List<Branch> branches = doc.getBranches();
MongoIdsDto docDeleteMongoIds = mongoIdsCollector.collectFrom(branches);
edgeRepository.deleteAll(edges); //๊ฐ์ ์ญ์
user.removeDocument(doc);
eventPublisher.publishEvent(docDeleteMongoIds);๊ธฐ์กด doc ์ญ์ ํ
์คํธ์ฝ๋์์๋ given์ ์ฃผ์ด์ง doc์ edge ์์ฒด๊ฐ ์ธํ
๋์ง ์์ ์ด ์ค๋ฅ๋ฅผ ๋ฐ๊ฒฌํ์ง ๋ชปํ๋ ๊ฒ์ผ๋ก ๋ณด์ธ๋ค.
ํด๋น ์ค๋ฅ๋ ๋ฐฐํฌ ํ api ํ
์คํธ์์ ๋ฐ๊ฒฌํ๊ณ ์์ ๊ฐ์ด ๋ฐ๋ก ์์ ํด ๋ค์ ๋ฐฐํฌํ์์ง๋ง, ๋ณด๋ค ์ ํํ ์์ธ ํ์
์ ์ํด ์ค๋ฅ๋ฅผ ์ฌํํด๋ณด๊ณ ์ ํ๋ค.
๋ค์๊ณผ ๊ฐ์ ๊ฐ๋จํ ํ ์คํธ์ฝ๋๋ก ์ฟผ๋ฆฌ๋ฅผ ํ์ธํ์๋ค.
- H2->MySQL ํ
์คํธ์ฉ ํ
์ด๋ธ์ ๋ง๋ ํ ๊ธฐ์กด์
delete()์๋น์ค ๋ฉ์๋๋ฅผ ๊ทธ๋๋ก ํธ์ถ - edges์
cascade = CascadeType.ALL(ํน์ REMOVE)๊ฐ ๊ฑธ๋ ค์์ผ๋ฉด ํญ์ ๋ค์ ํ ์คํธ ํต๊ณผํจ (orphanRemoval = true์กด์ฌ์ ๋ฌด๊ด)
@Test
void ๋ฌธ์_์ญ์ ์_์ฃ์ง_์ญ์ ์์๋ก_FK์ ์ฝ์กฐ๊ฑด_์์ธ๋ฐ์() throws JsonProcessingException {
// given
User user = userRepository.saveAndFlush(DocTestUtils.createUser());
Doc doc = DocTestUtils.createDocWithEdgesForDeleteTest(user, saveContentRepository,
commitBlockSequenceRepository, blockRepository);
docRepository.saveAndFlush(doc);
assertThrows(DataIntegrityViolationException.class, () -> {
docService.delete(doc.getId(), user.getId());
docRepository.flush(); // ์ฌ๊ธฐ์ MySQL์ด FK ์๋ฐ ์์ธ ๋์ง
});
}
}// ์ด์ ๋ก๊ทธ ์๋ต
Hibernate:
/* update
for io.ejangs.docsa.domain.branch.entity.Branch */update branches
set
document_id=?,
from_commit_id=?,
leaf_commit_id=?,
name=?,
root_commit_id=?,
updated_at=?
where
id=?
Hibernate:
/* update
for io.ejangs.docsa.domain.doc.entity.Edge */update edges
set
document_id=?,
next_commit_id=?,
prev_commit_id=?
where
id=?
2025-08-16T18:25:11.482+09:00 WARN 7556 --- [ Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1048, SQLState: 23000
2025-08-16T18:25:11.482+09:00 ERROR 7556 --- [ Test worker] o.h.engine.jdbc.spi.SqlExceptionHelper : Column 'next_commit_id' cannot be null
๋ก๊ทธ๋ฅผ ๋ณด๋ฉด, doc ์ญ์ ์ ๋ ๋ฒ์ UPDATE ์ฟผ๋ฆฌ๊ฐ ๋๊ฐ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
-
Hibernate๋ FK ๋ฌด๊ฒฐ์ฑ์ ์งํค๊ธฐ ์ํด DELETE ์ด์ ์ UPDATE๋ฅผ ๋ ๋ฆผ
- doc ์ญ์ ์ commit์ ์ญ์ ํ๊ธฐ ์ํด ๋จผ์ ์ด commit์ ์ฐธ์กฐํ๋ edge์ FK๋ฅผ ๋์
- edge์
next_commit_id๋ฅผ null๋ก UPDATEํ๋ ๊ณผ์ ์์ NOT NULL์ด ๊ฑธ๋ ค์๊ธฐ์MySQL:1048์์ธ ๋ฐ์
๋ณต์กํ ์ฐ๊ด๊ด๊ณ์์๋ CASCADE ์ ์ ์ ์ผ๋ก ์์กดํ๊ธฐ๋ณด๋ค, Hibernate์ ์์์น ๋ชปํ ๋ด๋ถ ๋์ ๊ฐ๋ฅ์ฑ์ ๊ณ ๋ คํ์ฌ ์ผ์ ์์ค์ ๋ช ์์ ์ฒ๋ฆฌ๋ฅผ ๋ณํํ๋ ๊ฒ์ด ์์คํ ์ ์์ ์ฑ ํ๋ณด์ ๋ ๋ฐ๋์งํ ๊ฒ์ผ๋ก ์๊ฐ๋๋ค.
๊ทธ๋ํ ์กฐํ API๋ UI ์ข์ธก ์ฌ์ด๋๋ฐ์ ์ปค๋ฐ ํ์คํ ๋ฆฌ๋ฅผ ํ์ํ๊ธฐ ์ํด ๋ฌธ์ ์กฐํ ์๋ง๋ค ์คํ๋๋ค. ์ด ๊ณผ์ ์์ ๋ฌธ์์ ๋ธ๋์น, ์ปค๋ฐ, ์ ์ฅ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์ค ๋ค์๊ณผ ๊ฐ์ ์๋ฌ๊ฐ ๋ฐ์ํ์๋ค.
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [io.ejangs.docsa.domain.branch.entity.Branch.commits, io.ejangs.docsa.domain.doc.entity.Doc.branches]
- ๊ทธ๋ํ ์กฐํ ๋ฐฉ์ (๊ธฐ์กด)
- Hibermate๋ ์ฑ๋ฅ ์ ํ ๋ฐฉ์ง๋ฅผ ์ํด ๋์์ 2๊ฐ ์ด์์ bag(๋งํฌ)๋ฅผ FETCH JOIN ํ์ง ๋ชปํ๋๋ก MultipleBagFetchException(๋งํฌ) ๋ฐ์์ํด
- branches์ edges ๋ ๊ฐ์ List ์ปฌ๋ ์ ์ FETCH JOIN
- branches ๋ด๋ถ์์ commits๋ผ๋ ๋ ๋ค๋ฅธ List ์ปฌ๋ ์ ๊น์ง FETCH JOIN์ ์๋ํ๋ฉฐ ํด๋น ์๋ฌ ๋ฐ์
public interface DocRepository extends JpaRepository<Doc, Long> {
@Query("""
SELECT DISTINCT d
FROM Doc d
LEFT JOIN FETCH d.branches b
LEFT JOIN FETCH b.commits
LEFT JOIN FETCH d.edges
WHERE d.id = :id
""")
Optional<Doc> findByIdWithBranchesAndEdges(@Param("id") Long id); - ํ
์คํธ ๋ฐฉ๋ฒ
- ์ฟผ๋ฆฌ ๊ฐ์, ์คํ ์๊ฐ(ms), ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋(MB) ๊ธฐ๋ก
- ํ๋์ ๋ฌธ์(doc)์ ๋ํด ๋ฐ์ดํฐ ํฌ๊ธฐ๋ณ ์กฐํ ์ฑ๋ฅ ์ธก์
- case 1) branches=2, commits=40, edges=40
- case 2) branches=2, commits=100, edges=100
- case 3) branches=2, commits=200, edges=200
- case 4) branches=3, commits=1500, edges=1501
- ํ๊ฒฝ
- MySQL ํ ์คํธ์ฉ DB
- Hibernate SQL ๋ก๊ทธ ๋ฐ EXPLAIN์ ํ์ฉํด ์ฟผ๋ฆฌ ์์ ์คํ ๊ณํ ํ์ธ