Skip to content

Commit 965667c

Browse files
authored
Merge: 문서 목록 CQRS Read Model 전환 및 Domain Event Outbox 추가
Refactor: 문서 목록 CQRS Read Model 전환 및 Domain Event Outbox 추가
2 parents a465a63 + 0dfd1a8 commit 965667c

130 files changed

Lines changed: 3360 additions & 1548 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ repositories {
2525

2626
test {
2727
testLogging {
28-
events "passed", "skipped", "failed", "standardOut", "standardError"
28+
events "skipped", "failed"
29+
showStandardStreams = false
2930
exceptionFormat "full"
3031
showCauses true
3132
showExceptions true
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
5+
ENV_FILE="${ENV_FILE:-"$ROOT_DIR/infra/.local.env"}"
6+
7+
if [[ ! -f "$ENV_FILE" ]]; then
8+
echo "env file not found: $ENV_FILE" >&2
9+
exit 1
10+
fi
11+
12+
set -a
13+
# shellcheck disable=SC1090
14+
source "$ENV_FILE"
15+
set +a
16+
17+
export SPRING_PROFILES_ACTIVE="${BACKFILL_PROFILES:-local,backfill}"
18+
export DDL_AUTO="${BACKFILL_DDL_AUTO:-none}"
19+
export MONGO_LOCAL_CLEANUP_ENABLED="false"
20+
export PERF_SEED_USER_COUNT="0"
21+
export DOC_LIST_READ_MODEL_BACKFILL_BATCH_SIZE="${DOC_LIST_READ_MODEL_BACKFILL_BATCH_SIZE:-100}"
22+
23+
bash "$ROOT_DIR/gradlew" bootRun

infra/scripts/cert_renew.sh

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,21 @@ ENV_FILE="$ROOT/docsa-alert.env"
1111
notify_slack () {
1212
local msg="$1"
1313
[[ -z "${SLACK_WEBHOOK_URL:-}" ]] && return 0
14+
15+
local payload
16+
payload="$(python3 -c 'import json, sys; print(json.dumps({"text": sys.argv[1]}))' "$msg")"
17+
1418
curl -sS -X POST -H 'Content-type: application/json' \
15-
--data "{\"text\":${msg@Q}}" \
19+
--data "$payload" \
1620
"$SLACK_WEBHOOK_URL" >/dev/null 2>&1 || true
1721
}
1822

1923
# ---- 1) certbot renew 실행 ----
2024
out="$(
2125
docker compose run --rm --no-deps certbot_renew \
22-
certbot renew --webroot -w /var/www/certbot 2>&1 || true
26+
renew --webroot -w /var/www/certbot 2>&1 || true
2327
)"
2428

25-
echo "$out"
26-
2729
# ---- 2) 실패 감지 ----
2830
# certbot이 실패할 때 자주 나오는 문구 위주로 (오탐 줄임)
2931
if echo "$out" | grep -Eqi \

src/main/java/io/ejangs/docsa/DocsaApplication.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import io.swagger.v3.oas.annotations.info.Info;
55
import org.springframework.boot.SpringApplication;
66
import org.springframework.boot.autoconfigure.SpringBootApplication;
7-
import org.springframework.scheduling.annotation.EnableAsync;
87
import org.springframework.scheduling.annotation.EnableScheduling;
98

109
@OpenAPIDefinition(
@@ -14,7 +13,6 @@
1413
description = "Docsa의 백엔드 API 명세입니다."
1514
)
1615
)
17-
@EnableAsync
1816
@EnableScheduling
1917
@SpringBootApplication
2018
public class DocsaApplication {

src/main/java/io/ejangs/docsa/domain/branch/app/BranchQueryService.java renamed to src/main/java/io/ejangs/docsa/domain/branch/app/BranchReader.java

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,16 @@
44
import io.ejangs.docsa.domain.branch.entity.Branch;
55
import io.ejangs.docsa.domain.commit.entity.Commit;
66
import io.ejangs.docsa.domain.edge.dto.graph.BranchGraphDto;
7-
import io.ejangs.docsa.domain.doc.entity.Doc;
87
import io.ejangs.docsa.global.exception.CustomException;
98
import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode;
10-
import io.ejangs.docsa.global.util.RenewUpdatedAtHelper;
119
import java.util.List;
1210
import lombok.RequiredArgsConstructor;
1311
import org.springframework.stereotype.Service;
1412
import org.springframework.transaction.annotation.Transactional;
1513

1614
@Service
1715
@RequiredArgsConstructor
18-
public class BranchQueryService {
16+
public class BranchReader {
1917

2018
private final BranchRepository branchRepository;
2119

@@ -29,31 +27,11 @@ public List<BranchGraphDto> getBranchGraphList(Long docId) {
2927
return branchRepository.findBranchGraphDtoList(docId);
3028
}
3129

32-
public Branch save(Branch branch) {
33-
return branchRepository.save(branch);
34-
}
35-
36-
public Branch createBranch(Doc doc, String branchName) {
37-
Branch branch = branchRepository.save(Branch.builder().name(branchName).doc(doc).build());
38-
RenewUpdatedAtHelper.touch(branch);
39-
return branch;
40-
}
41-
42-
public Branch createBranch(Doc doc, String branchName, Commit fromCommit) {
43-
Branch branch = Branch.builder().name(branchName).doc(doc).fromCommit(fromCommit).build();
44-
return branchRepository.save(branch);
45-
}
46-
4730
@Transactional(readOnly = true)
4831
public boolean existsSubBranchByFromCommitIds(List<Long> commitIds) {
4932
return branchRepository.existsByFromCommitIdIn(commitIds);
5033
}
5134

52-
public void delete(Branch branch) {
53-
branchRepository.delete(branch);
54-
}
55-
56-
//TODO: 검증하는 메소드는 별도의 ex.ValidationService로 분리(모든 도메인)
5735
public void checkBranchInDocOwnedByUser(Long documentId, Long branchId, Long userId) {
5836
boolean exists =
5937
branchRepository.existsByIdAndDocIdAndDocUserId(branchId, documentId, userId);

src/main/java/io/ejangs/docsa/domain/branch/app/BranchService.java

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,17 @@
77
import io.ejangs.docsa.domain.branch.dto.response.BranchRenameResponse;
88
import io.ejangs.docsa.domain.branch.entity.Branch;
99
import io.ejangs.docsa.domain.branch.util.BranchMapper;
10-
import io.ejangs.docsa.domain.commit.app.CommitQueryService;
10+
import io.ejangs.docsa.domain.commit.app.CommitReader;
1111
import io.ejangs.docsa.domain.commit.dao.mongodb.CommitBlockSequenceRepository;
1212
import io.ejangs.docsa.domain.commit.entity.Commit;
13-
import io.ejangs.docsa.domain.doc.app.create.DocQueryService;
13+
import io.ejangs.docsa.domain.doc.app.DocReader;
1414
import io.ejangs.docsa.domain.edge.app.EdgeService;
1515
import io.ejangs.docsa.domain.doc.entity.Doc;
1616
import io.ejangs.docsa.global.exception.CustomException;
1717
import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode;
1818
import io.ejangs.docsa.global.exception.errorcode.DocErrorCode;
1919
import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto;
20-
import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType;
21-
import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType;
22-
import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType;
23-
import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory;
20+
import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteJobEnqueuer;
2421
import io.ejangs.docsa.global.outbox.mongo.util.MongoDeleteMapper;
2522
import io.ejangs.docsa.global.util.RenewUpdatedAtHelper;
2623

@@ -37,13 +34,14 @@
3734
@RequiredArgsConstructor
3835
public class BranchService {
3936

40-
private final DocQueryService docQueryService;
41-
private final BranchQueryService branchQueryService;
42-
private final CommitQueryService commitQueryService;
37+
private final DocReader docReader;
38+
private final BranchReader branchReader;
39+
private final BranchWriter branchWriter;
40+
private final CommitReader commitReader;
4341
private final CommitBlockSequenceRepository commitBlockSequenceRepository;
4442
private final EdgeService edgeService;
4543
private final BranchCreateOrchestrator branchCreateOrchestrator;
46-
private final MongoDeleteOutboxFactory mongoDeleteOutboxFactory;
44+
private final MongoDeleteJobEnqueuer mongoDeleteJobEnqueuer;
4745

4846
public BranchCreateResponse createBranch(Long documentId, BranchCreateRequest request,
4947
Long userId) {
@@ -55,18 +53,18 @@ public BranchCreateResponse createBranch(Long documentId, BranchCreateRequest re
5553

5654
private BranchCreateContext prepareBranchCreateContext(Long documentId,
5755
BranchCreateRequest request, Long userId) {
58-
docQueryService.checkByIdAndUserId(documentId, userId);
56+
docReader.checkByIdAndUserId(documentId, userId);
5957

6058
Long fromCommitId = request.fromCommitId();
6159

62-
Commit fromCommit = commitQueryService.getById(fromCommitId);
60+
Commit fromCommit = commitReader.getById(fromCommitId);
6361
Branch fromBranch = fromCommit.getBranch();
6462

6563
if (!fromBranch.getDoc().getId().equals(documentId)) {
6664
throw new CustomException(DocErrorCode.COMMIT_NOT_IN_DOCUMENT);
6765
}
6866

69-
branchQueryService.checkDuplicatedWithBranchName(documentId, request.name());
67+
branchReader.checkDuplicatedWithBranchName(documentId, request.name());
7068

7169

7270
return new BranchCreateContext(
@@ -83,8 +81,8 @@ public BranchRenameResponse renameBranch(Long documentId, Long branchId, String
8381
Long userId) {
8482

8583
// 1. 브랜치 검증
86-
branchQueryService.checkBranchInDocOwnedByUser(documentId, branchId, userId);
87-
Branch branch = branchQueryService.getById(branchId);
84+
branchReader.checkBranchInDocOwnedByUser(documentId, branchId, userId);
85+
Branch branch = branchReader.getById(branchId);
8886
checkDefaultBranch(branch);
8987

9088
// 2. 브랜치 이름 수정 후 브랜치와 문서의 수정시각 갱신
@@ -105,8 +103,8 @@ public BranchRenameResponse renameBranch(Long documentId, Long branchId, String
105103
public void deleteBranch(Long documentId, Long branchId, Long userId) {
106104

107105
// 1. 브랜치 검증
108-
branchQueryService.checkBranchInDocOwnedByUser(documentId, branchId, userId);
109-
Branch branch = branchQueryService.getById(branchId);
106+
branchReader.checkBranchInDocOwnedByUser(documentId, branchId, userId);
107+
Branch branch = branchReader.getById(branchId);
110108

111109
// 2. main브랜치는 삭제가 불가능하도록 함
112110
checkDefaultBranch(branch);
@@ -115,7 +113,7 @@ public void deleteBranch(Long documentId, Long branchId, Long userId) {
115113
List<Commit> branchCommits = branch.getCommits();
116114
List<Long> commitsIds = branchCommits.stream().map(Commit::getId).toList();
117115

118-
if (branchQueryService.existsSubBranchByFromCommitIds(commitsIds)) {
116+
if (branchReader.existsSubBranchByFromCommitIds(commitsIds)) {
119117
throw new CustomException(BranchErrorCode.SUB_BRANCH_DELETE_UNAVAILABLE);
120118
}
121119

@@ -133,15 +131,9 @@ public void deleteBranch(Long documentId, Long branchId, Long userId) {
133131
doc.getBranches().remove(branch);
134132

135133
// 8. 브랜치, 나머지 RDB 브랜치 메타데이터 CASCADE 삭제
136-
branchQueryService.delete(branch);
137-
138-
mongoDeleteOutboxFactory.create(
139-
TriggerType.DELETE,
140-
DomainType.BRANCH,
141-
OriginType.BRANCH_ID,
142-
branchId,
143-
deletableMongoIds
144-
);
134+
branchWriter.delete(branch);
135+
136+
mongoDeleteJobEnqueuer.enqueueBranchDeletion(branchId, deletableMongoIds);
145137

146138
}
147139

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.ejangs.docsa.domain.branch.app;
2+
3+
import io.ejangs.docsa.domain.branch.dao.mysql.BranchRepository;
4+
import io.ejangs.docsa.domain.branch.entity.Branch;
5+
import io.ejangs.docsa.domain.commit.entity.Commit;
6+
import io.ejangs.docsa.domain.doc.entity.Doc;
7+
import io.ejangs.docsa.global.util.RenewUpdatedAtHelper;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.stereotype.Service;
10+
11+
@Service
12+
@RequiredArgsConstructor
13+
public class BranchWriter {
14+
15+
private final BranchRepository branchRepository;
16+
17+
public Branch save(Branch branch) {
18+
return branchRepository.save(branch);
19+
}
20+
21+
public Branch createBranch(Doc doc, String branchName) {
22+
Branch branch = branchRepository.save(Branch.builder().name(branchName).doc(doc).build());
23+
RenewUpdatedAtHelper.touch(branch);
24+
return branch;
25+
}
26+
27+
public Branch createBranch(Doc doc, String branchName, Commit fromCommit) {
28+
Branch branch = Branch.builder().name(branchName).doc(doc).fromCommit(fromCommit).build();
29+
return branchRepository.save(branch);
30+
}
31+
32+
public void delete(Branch branch) {
33+
branchRepository.delete(branch);
34+
}
35+
}
Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package io.ejangs.docsa.domain.branch.app.create;
22

3-
import io.ejangs.docsa.domain.branch.app.BranchQueryService;
3+
import io.ejangs.docsa.domain.branch.app.BranchWriter;
44
import io.ejangs.docsa.domain.branch.dto.BranchCreateContext;
55
import io.ejangs.docsa.domain.branch.dto.response.BranchCreateResponse;
66
import io.ejangs.docsa.domain.branch.entity.Branch;
77
import io.ejangs.docsa.domain.branch.util.BranchMapper;
8-
import io.ejangs.docsa.domain.save.app.SaveQueryService;
8+
import io.ejangs.docsa.domain.doc.readmodel.util.DocPayloadFactory;
9+
import io.ejangs.docsa.domain.save.app.SaveWriter;
910
import io.ejangs.docsa.domain.save.entity.Save;
11+
import io.ejangs.docsa.global.outbox.event.app.DomainEventOutboxPublisher;
12+
import io.ejangs.docsa.global.outbox.event.model.AggregateType;
13+
import io.ejangs.docsa.global.outbox.event.model.DomainEventType;
1014
import io.ejangs.docsa.global.util.RenewUpdatedAtHelper;
1115
import lombok.RequiredArgsConstructor;
1216
import org.springframework.stereotype.Service;
@@ -16,17 +20,21 @@
1620
@RequiredArgsConstructor
1721
public class BranchCreateMySqlTxService {
1822

19-
private final BranchQueryService branchQueryService;
20-
private final SaveQueryService saveQueryService;
23+
private final BranchWriter branchWriter;
24+
private final SaveWriter saveWriter;
25+
private final DomainEventOutboxPublisher domainEventOutboxPublisher;
2126

2227
@Transactional(rollbackFor = Exception.class)
2328
public BranchCreateResponse createMySqlPart(BranchCreateContext context, String saveContentId) {
2429

25-
Branch newBranch = branchQueryService.createBranch(context.doc(), context.branchName(), context.fromCommit());
30+
Branch newBranch = branchWriter.createBranch(context.doc(), context.branchName(), context.fromCommit());
2631

27-
Save save = saveQueryService.createSave(newBranch, saveContentId);
32+
Save save = saveWriter.createSave(newBranch, saveContentId);
2833
RenewUpdatedAtHelper.touch(save);
2934

35+
domainEventOutboxPublisher.publish(DomainEventType.DOC_ACTIVITY_CHANGED, AggregateType.DOC, context.doc()
36+
.getId(), DocPayloadFactory.activityChanged(context.doc().getId(), save));
37+
3038
return BranchMapper.toBranchCreateResponse(newBranch, save);
3139
}
3240
}

src/main/java/io/ejangs/docsa/domain/branch/app/create/BranchCreateOrchestrator.java

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
import io.ejangs.docsa.global.exception.CustomException;
66
import io.ejangs.docsa.global.exception.errorcode.BranchErrorCode;
77
import io.ejangs.docsa.global.outbox.mongo.dto.MongoIdsDto;
8-
import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.DomainType;
9-
import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.OriginType;
10-
import io.ejangs.docsa.global.outbox.mongo.entity.MongoDeleteOutbox.TriggerType;
11-
import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteOutboxFactory;
8+
import io.ejangs.docsa.global.outbox.mongo.app.MongoDeleteJobEnqueuer;
129
import java.util.List;
1310
import lombok.RequiredArgsConstructor;
1411
import lombok.extern.slf4j.Slf4j;
@@ -21,7 +18,7 @@ public class BranchCreateOrchestrator {
2118

2219
private final BranchCreateMongoTxService branchCreateMongoTxService;
2320
private final BranchCreateMySqlTxService branchCreateMySqlTxService;
24-
private final MongoDeleteOutboxFactory mongoDeleteOutboxFactory;
21+
private final MongoDeleteJobEnqueuer mongoDeleteJobEnqueuer;
2522

2623
public BranchCreateResponse create(BranchCreateContext context) {
2724
String saveContentId = branchCreateMongoTxService.createSaveContentFromCommit(
@@ -32,13 +29,7 @@ public BranchCreateResponse create(BranchCreateContext context) {
3229
} catch (Exception e) {
3330
log.warn("[SAGA] 브랜치/저장 생성 실패 -> Mongo 삭제 Outbox 기록.", e);
3431
MongoIdsDto compensateTarget = new MongoIdsDto(List.of(saveContentId), null, null);
35-
mongoDeleteOutboxFactory.create(
36-
TriggerType.COMPENSATE,
37-
DomainType.BRANCH,
38-
OriginType.SAVE_CONTENT_ID,
39-
saveContentId,
40-
compensateTarget
41-
);
32+
mongoDeleteJobEnqueuer.enqueueBranchCreateCompensation(saveContentId, compensateTarget);
4233
throw new CustomException(BranchErrorCode.FAIL_CREATE_BRANCH);
4334
}
4435
}

src/main/java/io/ejangs/docsa/domain/branch/dao/mysql/BranchRepository.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import io.ejangs.docsa.domain.edge.dto.graph.BranchGraphDto;
44
import io.ejangs.docsa.domain.branch.entity.Branch;
5+
import io.ejangs.docsa.domain.doc.dto.LatestSaveIdDto;
56
import org.springframework.data.jpa.repository.JpaRepository;
67
import org.springframework.data.jpa.repository.Query;
78
import org.springframework.data.repository.query.Param;
@@ -28,6 +29,26 @@ public interface BranchRepository extends JpaRepository<Branch, Long> {
2829
""")
2930
List<BranchGraphDto> findBranchGraphDtoList(@Param("docId") Long docId);
3031

32+
@Query("""
33+
SELECT new io.ejangs.docsa.domain.doc.dto.LatestSaveIdDto(
34+
b.doc.id,
35+
s.id
36+
)
37+
FROM Branch b
38+
JOIN b.save s
39+
WHERE b.doc.id IN :docIds
40+
AND NOT EXISTS (
41+
SELECT 1
42+
FROM Branch newer
43+
WHERE newer.doc.id = b.doc.id
44+
AND (
45+
newer.updatedAt > b.updatedAt
46+
OR (newer.updatedAt = b.updatedAt AND newer.id > b.id)
47+
)
48+
)
49+
""")
50+
List<LatestSaveIdDto> findLatestSaveIdsByDocIds(@Param("docIds") List<Long> docIds);
51+
3152
boolean existsByIdAndDocIdAndDocUserId(Long branchId, Long documentId, Long userId);
3253

3354
boolean existsByFromCommitIdIn(List<Long> commitIds);

0 commit comments

Comments
 (0)