Skip to content

feat: 에디터 Transaction 저장 API 구현#33

Merged
oneplast merged 22 commits intodevfrom
feat/#30_단위_저장
Mar 24, 2026

Hidden character warning

The head ref may contain hidden characters: "feat/#30_\ub2e8\uc704_\uc800\uc7a5"
Merged

feat: 에디터 Transaction 저장 API 구현#33
oneplast merged 22 commits intodevfrom
feat/#30_단위_저장

Conversation

@oneplast
Copy link
Copy Markdown
Collaborator

@oneplast oneplast commented Mar 20, 2026

📝 Part (해당되는 것만 체크)

  • BE
  • FE
  • Infra
  • Docs
  • Test

#️⃣ 연관된 이슈

closes #30


🔎 작업 내용

1. 주요 변경 사항 요약

  • 에디터 표준 저장 API 추가 (/v1/documents/{documentId}/transactions)
    • Block 단위 CRUD 가 아닌, Batch 단위 저장 모델로 정리
    • 블록 추가, 수정, 이동, 삭제 기능 API에 연동
    • 관련하여 여러가지 정책 수립 및 적용
  • 관련 테스트코드 작성
    • 기능 테스트코드 작성 및 동시성 고려 테스트코드 작성

2. 상세 내용 (선택)

1. 에디터 표준 저장 API를 추가하였습니다.

  • 에디터 저장을 단건 Block API 호출이 아닌, 하나의 batch 저장 요청으로 처리하도록 구현하였습니다.
  • v1에서의 해당 API는 아래와 같은 4개의 operation만 받습니다.
    • BLOCK_CREATE
    • BLOCK_REPLACE_CONTENT
    • BLOCK_MOVE
    • BLOCK_DELETE
  • 이를 통해 에디터에서 발생하는 블록 생성, 수정, 이동, 삭제를 하나의 Transaction 단위로 처리합니다.

2. 에디터 저장 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 으로 들어갑니다.
    • 이는 추후 버저닝 시 변경될 수 있습니다.
  • 현재 구조에서는, 같은 batch 안에서는 새로 생성한 Blocktemp ref를 통해 참조할 수 있습니다.
    • blockRef
    • parentRef
    • afterRef
    • beforeRef
  • 이 값들은 같은 batch 안의 새 Block 이면 temp ref, 기존 Block 이면 real block id를 사용합니다.
  • 이 방식을 통해 블록 생성으로 시작한 흐름하나의 batch로 처리할 수 있습니다.
    • create -> replace
    • create -> move
    • create -> replace -> move

3. 에디터 저장 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_CREATEtemp ref와 생성된 real block id를 함께 반환합니다.
  • BLOCK_REPLACE_CONTENT , BLOCK_MOVE 는 최신 Blockversion , sortKey 를 반환합니다.
  • BLOCK_DELETEdeletedAt 을 반환합니다.
  • 요청으로 인한 변경이 없을 경우, status에는 NO_OP 이 반환됩니다.
  • NO_OP 은 다음과 같은 경우에 반환됩니다.
    • BLOCK_REPLACE_CONTENT -> 현재 저장 문자열과 요청 문자열이 같은 경우
    • BLOCK_MOVE -> parentsortKey 변화가 없는 경우
    • NO_OP 이 응답될 때 block.versiondocument.version기존 version으로 유지됩니다.

4. 블록 단위 동시성 검증을 진행하고, 실패 시 트랜잭션 전체 rollback을 수행합니다.

  • 작업할 Block.version요청 version과 다르면, 409 CONFLICT 가 발생합니다.
  • 하나의 operation이라도 실패하는 경우, 해당 요청을 전체 rollback합니다.
  • BLOCK_DELETE 의 경우, root block version까지 실제 삭제 시점에 원자적으로 검증합니다.
    • BLOCK_DELETE 는 실제 bulk soft delete 시점까지 root block version을 함께 검증합니다.
  • 현재 rollback이 발생하면 GlobalExceptionHandler 를 통해 GlobalResponseErrorCode 가 담겨서 들어오지만, 이후 재시도 로직이 추가될 경우 이 부분은 변경될 수 있습니다.

5. 최대한 많은 테스트 커버리지를 구성하도록 테스트코드를 보강하였습니다.

  • 현재 테스트케이스에서는 기능 구현에 관련된 테스트 검증 뿐만 아니라, 요청 Validation, temp ref 순서, version 전파, retry, rollback 등과 관련된 테스트를 진행합니다.
  • 동시성 테스트는 크게 두 종류로 진행합니다.
  • 결정적 경합 테스트
    • serializingAnswer(...) 기반
    • DocumentTransactionServiceImpl.apply() 진입 시점을 맞춰, transaction 전체 경쟁을 결정적으로 재현합니다.
  • 경쟁형 테스트
    • CountDownLatch 기반
    • 요청 여러 개를 실제로 동시에 출발시켜, 선착순 형태 경쟁을 검증합니다.

6. 로직과 관련된 부분들을 각 용도별로 문서화하고, 서버의 포트를 8083으로 고정하였습니다.

3. 프롬프트 경로

4. 참조 문서 경로 (ADR, discussions, roadMap ...)

ADR

discussions

explainers

guides


💬 집중 리뷰 요청

  • 비즈니스의 가장 핵심적인 에디터 쓰기(저장) 구현입니다. 설명서프론트-백 가이드, 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. 테스트 시나리오

  • 실제 editor 변경이 있는 batch 성공 시 응답 documentVersion이 증가하는지 확인
  • no-op batch에서는 block version과 응답 documentVersion이 유지되는지 확인
  • temp ref 기반 create -> replace, create -> move, create -> delete(temp)가 정상 동작하는지 확인
  • 같은 batch 안에서 같은 real block의 후속 operation이 server current version을 이어받는지 확인
  • same-block replace/move/delete 동시 요청 시 정확히 1건만 성공하는지 확인
  • same-block replace vs move, replace vs delete, move vs delete, replace->move batch vs replace 경쟁 시 정합성이 유지되는지 확인
  • different-block replace/move/delete는 동시에 와도 각각 성공하는지 확인
  • same-block 3-way replace 경쟁 시 1건만 성공하는지 확인
  • create vs replace는 동시에 와도 각각 성공하는지 확인
  • different-block 3-way replace 경쟁 시 모두 성공하는지 확인
  • same-block replace/move/delete 10-way 경쟁 시 1건만 성공하는지 확인
  • different-block replace/move/delete 10-way 경쟁 시 모두 성공하는지 확인
  • create 10-way 경쟁 시 모두 성공하는지 확인
  • create 5개 + same-block replace 5개 mixed 경쟁 시 create는 모두 성공하고 replace는 1건만 성공하는지 확인
  • same-block replace 성공 응답의 documentVersion, 실패 응답의 충돌 코드가 계약대로 내려오는지 확인
  • 충돌 후 최신 block version으로 재요청하면 성공하는지 확인
  • same-content replace, same-position move no-op 경쟁 10건이 모두 성공하고 응답 documentVersion이 유지되는지 확인

✅ PR 체크리스트

  • 불필요한 디버그 로그 / 주석 제거
  • breaking change 여부 확인 및 문서화
  • 신규/변경된 기능에 대한 테스트 코드 추가 또는 기존 테스트 통과
  • 로컬에서 주요 시나리오 수동 테스트 완료
  • 관련 문서(노션, README, API 문서 등) 업데이트

📈 이미지 / 캡처 (필요 시)

No response

@oneplast oneplast self-assigned this Mar 20, 2026
@oneplast oneplast added the enhancement New feature or request label Mar 20, 2026
@oneplast oneplast moved this from Todo to In Progress in Block-server Mar 20, 2026
hellonaeunkim
hellonaeunkim approved these changes Mar 21, 2026
@hellonaeunkim hellonaeunkim self-requested a review March 21, 2026 10:59
oneplast added 16 commits March 22, 2026 19:46
- 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 전처리 원칙은 그대로 유지합니다.
@oneplast oneplast force-pushed the feat/#30_단위_저장 branch from f24fa48 to 22647f1 Compare March 22, 2026 19:25
- 프론트는 에디터 저장 API 호출 시, 현재 보고 있는 시점의 DocumentVersion을 추가로 넘겨줍니다.
- 서버는 에디터 저장 로직 초반부에 DocumentVersion 검증을 진행합니다.
- 에디터 저장 로직이 성공적으로 실행되면 DocumentVersion을 1 증가시키고, 증가된 DocumentVersion을 응답에 포함시킵니다.
@oneplast oneplast force-pushed the feat/#30_단위_저장 branch from 82b54e9 to 033a579 Compare March 23, 2026 09:30
Copy link
Copy Markdown
Collaborator

@hellonaeunkim hellonaeunkim left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!!

- Block 단위 동시성 제어 정책과 맞지 않던, 기존의 문서 단위 선검증 부분을 제거했습니다.
- 기존 로직 중, 저장 성공 시 DocumentVersion + 1 후 응답에 반영하는 부분은 그대로 유지했습니다.
- 관련 선검증 관련 테스트를 삭제하고, 동시성 테스트는 DocumentTransactionServiceImpl.apply() 기준으로 통일했습니다.
@oneplast oneplast merged commit 4a99f35 into dev Mar 24, 2026
@github-project-automation github-project-automation bot moved this from In Progress to Done in Block-server Mar 24, 2026
@oneplast oneplast deleted the feat/#30_단위_저장 branch March 24, 2026 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[FEATURE] 에디터 Transaction 저장 API 추가

3 participants