Conversation
- operation type별 요청 shape validation을 추가했습니다. - 문서 단위 transaction 저장 API와 내부 orchestration 흐름을 연결했습니다. - BLOCK_CREATE와 BLOCK_REPLACE_CONTENT를 연동했습니다. - 요청 블록 참조 필드를 blockRef로 통일하고, 응답에는 매핑용 tempId 및 blockId를 유지했습니다.
- temp 참조 지원 범위를 blockRef에서 parentRef, afterRef, beforeRef까지 넓혔습니다. - 연속된 부모/자식 생성과 sibling 기준 삽입을 지원하는 방향으로 문서를 정리했습니다.
- transaction 위치 필드를 parentRef, afterRef, beforeRef로 확장했습니다. - 같은 batch 안의 temp block을 위치 ref로 해석하도록 저장 로직을 보강했습니다.
- temp parent와 temp sibling anchor 시나리오를 테스트로 추가했습니다.
- 블록 삭제 기능을 연동하였습니다. - 기존 BlockService.delete() 로직에서 Block을 넘겨주어, 이를 DocumentTransactionServiceImpl에서 사용할 수 있도록 수정하였습니다. - 관련 테스트코드를 수정 및 추가하고 테스트 통과를 확인하였습니다.
- 기존 BLOCK_REPLACE_CONTENT, BLOCK_MOVE에서, 소속된 문서와 관련한 검증을 진행하는 로직이 누락되어있었습니다. - 이제 블록 수정 및 이동 시에도 소속 Document를 확인합니다. - 에디터 저장 기능 테스트코드에서 몇 가지 edge case를 추가로 검증하였습니다.
- 요청으로 인해 해당 블록 데이터의 데이터가 변경되지 않는다면 NO_OP 전용 응답을 내보내도록 변경하였습니다.
- 프론트와 백엔드 사이의 block version 전달 정책을 다시 정리했습니다. - 기존에는 프론트가 request에 담는 version과 서버가 batch 내부에서 실제로 이어가야 하는 version 흐름이 분리되지 않아, 같은 batch 안의 연속 작업에서 충돌이 발생하기 쉬운 구조였습니다. - 프론트는 batch 생성 시점의 base version만 보내고, 서버는 base version과 current version을 분리해 동시성 검증과 후속 operation chaining을 수행하도록 수정했습니다. - 블록 삭제는 @Modifying bulk update 경로라 JPA @Version 낙관적 락이 자동 적용되지 않습니다. - soft delete query에 root block version 조건을 추가해, stale delete를 실제 삭제 시점까지 막도록 보강했습니다. - 기존에는 프론트 collapse를 전제로 create -> delete temp block 시퀀스를 서버에서 에러로 처리했습니다. - 이제는 해당 시퀀스도 서버에서 자연스럽게 처리할 수 있도록 허용했습니다. - 다만 프론트의 queue collapse 전처리 원칙은 그대로 유지합니다.
f24fa48 to
22647f1
Compare
- 프론트는 에디터 저장 API 호출 시, 현재 보고 있는 시점의 DocumentVersion을 추가로 넘겨줍니다. - 서버는 에디터 저장 로직 초반부에 DocumentVersion 검증을 진행합니다. - 에디터 저장 로직이 성공적으로 실행되면 DocumentVersion을 1 증가시키고, 증가된 DocumentVersion을 응답에 포함시킵니다.
82b54e9 to
033a579
Compare
jho951
approved these changes
Mar 23, 2026
- Block 단위 동시성 제어 정책과 맞지 않던, 기존의 문서 단위 선검증 부분을 제거했습니다. - 기존 로직 중, 저장 성공 시 DocumentVersion + 1 후 응답에 반영하는 부분은 그대로 유지했습니다. - 관련 선검증 관련 테스트를 삭제하고, 동시성 테스트는 DocumentTransactionServiceImpl.apply() 기준으로 통일했습니다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
📝 Part (해당되는 것만 체크)
#️⃣ 연관된 이슈
closes #30
🔎 작업 내용
1. 주요 변경 사항 요약
/v1/documents/{documentId}/transactions)Block단위CRUD가 아닌, Batch 단위 저장 모델로 정리2. 상세 내용 (선택)
1. 에디터 표준 저장 API를 추가하였습니다.
BLOCK_CREATEBLOCK_REPLACE_CONTENTBLOCK_MOVEBLOCK_DELETE2. 에디터 저장 API의 request는 다음과 같은 형식입니다.
{ "clientId": "web-editor", "batchId": "batch-1", "operations": [ { "opId": "op-1", "type": "BLOCK_CREATE", "blockRef": "tmp:block:1", "parentRef": null, "afterRef": null, "beforeRef": null }, { "opId": "op-2", "type": "BLOCK_REPLACE_CONTENT", "blockRef": "tmp:block:1", "content": { "format": "rich_text", "schemaVersion": 1, "segments": [ { "text": "새 블록", "marks": [] } ] } }, { "opId": "op-3", "type": "BLOCK_MOVE", "blockRef": "6e8c2c2d-3d55-4f74-a1d0-111111111111", "version": 4, "parentRef": "tmp:block:1" }, { "opId": "op-4", "type": "BLOCK_DELETE", "blockRef": "6e8c2c2d-3d55-4f74-a1d0-222222222222", "version": 2 } ] }blockRef에는tempId값이나 실제blockId값이UUID가 아닌String으로 들어갑니다.Block을 temp ref를 통해 참조할 수 있습니다.blockRefparentRefafterRefbeforeRefBlock이면 temp ref, 기존Block이면 real block id를 사용합니다.create -> replacecreate -> movecreate -> replace -> move3. 에디터 저장 API의 response는 다음과 같은 형식입니다.
{ "httpStatus": "OK", "success": true, "message": "요청 응답 성공", "code": 200, "data": { "documentId": "d290f1ee-6c54-4b01-90e6-aaaaaaaaaaaa", "documentVersion": 4, "batchId": "batch-1", "appliedOperations": [ { "opId": "op-1", "status": "APPLIED", "tempId": "tmp:block:1", "blockId": "6e8c2c2d-3d55-4f74-a1d0-bbbbbbbbbbbb", "version": 0, "sortKey": "000000000001000000000000", "deletedAt": null }, { "opId": "op-2", "status": "APPLIED", "tempId": null, "blockId": "6e8c2c2d-3d55-4f74-a1d0-bbbbbbbbbbbb", "version": 1, "sortKey": "000000000001000000000000", "deletedAt": null }, { "opId": "op-3", "status": "NO_OP", "tempId": null, "blockId": "6e8c2c2d-3d55-4f74-a1d0-111111111111", "version": 4, "sortKey": "000000000002000000000000", "deletedAt": null }, { "opId": "op-4", "status": "APPLIED", "tempId": null, "blockId": "6e8c2c2d-3d55-4f74-a1d0-222222222222", "version": null, "sortKey": null, "deletedAt": "2026-03-23T16:30:00" } ] } }appliedOperations는 각 operation별 적용 결과입니다.BLOCK_CREATE는 temp ref와 생성된 real block id를 함께 반환합니다.BLOCK_REPLACE_CONTENT,BLOCK_MOVE는 최신Block의version,sortKey를 반환합니다.BLOCK_DELETE는deletedAt을 반환합니다.NO_OP이 반환됩니다.NO_OP은 다음과 같은 경우에 반환됩니다.BLOCK_REPLACE_CONTENT-> 현재 저장 문자열과 요청 문자열이 같은 경우BLOCK_MOVE->parent나sortKey변화가 없는 경우NO_OP이 응답될 때block.version과document.version은 기존 version으로 유지됩니다.4. 블록 단위 동시성 검증을 진행하고, 실패 시 트랜잭션 전체 rollback을 수행합니다.
Block.version이 요청 version과 다르면,409 CONFLICT가 발생합니다.BLOCK_DELETE의 경우, root block version까지 실제 삭제 시점에 원자적으로 검증합니다.BLOCK_DELETE는 실제 bulk soft delete 시점까지 root block version을 함께 검증합니다.GlobalExceptionHandler를 통해GlobalResponse에ErrorCode가 담겨서 들어오지만, 이후 재시도 로직이 추가될 경우 이 부분은 변경될 수 있습니다.5. 최대한 많은 테스트 커버리지를 구성하도록 테스트코드를 보강하였습니다.
serializingAnswer(...)기반DocumentTransactionServiceImpl.apply()진입 시점을 맞춰, transaction 전체 경쟁을 결정적으로 재현합니다.CountDownLatch기반6. 로직과 관련된 부분들을 각 용도별로 문서화하고, 서버의 포트를 8083으로 고정하였습니다.
3. 프롬프트 경로
4. 참조 문서 경로 (ADR, discussions, roadMap ...)
💬 집중 리뷰 요청
DocumentTransactionServiceImpl를 집중적으로 확인해주시기 바랍니다.🧪 테스트 방법
1. 로컬 실행 방법
env GRADLE_USER_HOME=/tmp/gradle-home ./gradlew :documents-infrastructure:test --tests 'com.documents.service.DocumentTransactionServiceImplTest' --tests 'com.documents.service.BlockServiceImplTest'env GRADLE_USER_HOME=/tmp/gradle-home ./gradlew :documents-api:test --tests 'com.documents.api.block.BlockControllerWebMvcTest' --tests 'com.documents.api.document.DocumentControllerWebMvcTest'env GRADLE_USER_HOME=/tmp/gradle-home ./gradlew :documents-boot:test --tests 'com.documents.api.block.BlockApiIntegrationTest' --tests 'com.documents.api.document.DocumentTransactionApiIntegrationTest' --tests 'com.documents.api.document.DocumentTransactionConcurrencyIntegrationTest'2. 테스트 시나리오
✅ PR 체크리스트
📈 이미지 / 캡처 (필요 시)
No response