Skip to content

feat(configuration): S3 및 files 테이블 어댑터와 유스케이스 구현 #26 - #57

Open
tlgms wants to merge 4 commits into
feat(document)-파일-도메인-모델-#26from
feat(document)-파일-저장소-어댑터-#26

Hidden character warning

The head ref may contain hidden characters: "feat(document)-\ud30c\uc77c-\uc800\uc7a5\uc18c-\uc5b4\ub311\ud130-#26"
Open

feat(configuration): S3 및 files 테이블 어댑터와 유스케이스 구현 #26#57
tlgms wants to merge 4 commits into
feat(document)-파일-도메인-모델-#26from
feat(document)-파일-저장소-어댑터-#26

Conversation

@tlgms

@tlgms tlgms commented Jul 27, 2026

Copy link
Copy Markdown

스택 PR 2/4 — base: feat(document)-파일-도메인-모델-#26
PR 1이 먼저 머지되어야 합니다. 머지 후 base가 develop으로 자동 전환됩니다.

Summary

  • StoragePort를 AWS S3로, FileDocumentRepository를 JPA로 구현합니다.
  • 두 포트를 조합하는 FileDocumentService 유스케이스를 추가합니다.

Related Issue

Scope

  • In scope: configuration-adapter-out, configuration-application, kotlin.MODULE.bazel
  • Out of scope: REST / gRPC 진입점, 버킷 생성·IAM 정책, 마이그레이션 도구 도입

Implementation

바이너리는 S3에, 메타데이터는 DB(files)에 둡니다. 응답으로 바이너리를 스트리밍하지 않고 presigned URL을 반환해 서버 부하를 줄입니다.

어댑터

  • S3StorageAdapter — 업로드 시 SHA-256 체크섬 수신, presigned URL 발급, 존재 확인, 삭제
  • S3ConfigS3Client / S3Presigner 빈. 자격증명은 기본 provider chain
  • FileDocumentJpaEntity / JpaRepository / PersistenceAdapter — ERD files 스키마 대응, object_key에 유니크 제약
  • SdkExceptionSTORAGE_UPLOAD_FAILED / PRESIGN_FAILED 도메인 예외로 변환해 AWS 타입이 어댑터 밖으로 새지 않게 합니다

유스케이스 — 순서가 핵심입니다

S3 업로드는 DB 트랜잭션에 참여하지 않습니다. 순서를 잘못 잡으면 검증 실패인데 S3를 호출하거나, 메타데이터 없이 객체만 남습니다.

1. 검증(형식·용량)  → 실패 시 S3 호출 없이 400/413
2. S3 업로드        → 실패 시 502, DB 미변경
3. 메타데이터 저장   → 실패 시 업로드된 객체를 보상 삭제

보상 삭제가 또 실패하면 로깅만 하고 원래 예외를 덮지 않습니다. 삭제 실패로 진짜 원인을 가리지 않기 위해서입니다.

Testing

  • Unit tests — 이 PR 범위에는 없습니다 (후속 이슈)
  • Integration tests — 후속 이슈
  • Manual verification
bazel build //systems/configuration/...
bazel test  //systems/configuration/...

컴파일과 기존 테스트 통과만 확인했습니다. S3StorageAdapter실제 S3에 대해 검증되지 않았습니다.

Deployment Notes

  • Feature flag: 없음
  • Migration required: files 테이블이 필요합니다. ddl-auto: validate이므로 없으면 기동 실패합니다. DDL은 PR 4에 포함되어 있습니다
  • Rollout considerations: S3_BUCKET, AWS_REGION 환경변수 주입 필요 (PR 4)

Checklist

  • Matches product/tech requirements
  • Backward compatibility considered
  • Docs updated if applicable

리뷰 시 봐주셨으면 하는 것

  1. RequestBody.fromInputStream + checksumAlgorithm(SHA256) 조합 — 스트리밍 체크섬이 실제로 동작하는지 목으로는 확인이 안 됩니다. 통합 테스트를 후속 이슈로 잡아두었습니다. 체크섬이 null이면 eTag로 폴백합니다.
  2. AWS SDK 버전 2.31.0 — One-Version Rule에 따라 kotlin.MODULE.bazel에만 기재했습니다. 팀에서 쓰는 버전이 따로 있으면 알려주세요.
  3. 보상 삭제까지 실패하면 고아 객체가 남습니다. 주기적 정리는 이 PR 범위 밖입니다.

tlgms and others added 4 commits July 27, 2026 20:46
One-Version Rule에 따라 kotlin.MODULE.bazel에 버전을 고정하고
configuration-adapter-out에서만 S3를 참조한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ERD의 files 스키마에 대응하는 JPA 엔티티와 FileDocumentRepository
아웃바운드 포트 구현을 추가한다. object_key에 유니크 제약을 둔다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
StoragePort를 AWS SDK v2로 구현한다. 업로드 시 SHA-256 체크섬을 받아
files 메타데이터에 남기고, 다운로드는 presigned URL로 발급한다.

SdkException을 STORAGE_UPLOAD_FAILED / PRESIGN_FAILED 도메인 예외로
변환해 어댑터 밖으로 AWS 타입이 새지 않게 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FileDocumentService가 UploadFileUseCase, IssueDownloadUrlUseCase,
ReadFileUseCase를 구현한다.

검증(형식/용량) → S3 업로드 → 메타데이터 저장 순서를 지키고,
메타데이터 저장이 실패하면 업로드된 객체를 보상 삭제한다.
보상 삭제 실패는 로깅만 하고 원래 예외를 덮지 않는다.

파일명은 API마다 결정 방식이 달라 커맨드의 fileName을 필수로 바꾸고
컨트롤러가 결정하도록 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
📝 Walkthrough

사용자는 파일 바이너리를 S3에 저장하고 메타데이터를 DB에서 관리할 수 있습니다. 다운로드 요청은 presigned URL을 반환합니다.

아키텍처 변경

  • S3StorageAdapter를 추가했습니다.
    • 파일 업로드, 삭제, 존재 여부 확인을 처리합니다.
    • 다운로드용 presigned URL을 발급합니다.
    • 업로드 시 SHA-256 체크섬을 설정합니다.
    • AWS SDK 예외를 도메인 예외로 변환합니다.
  • FileDocumentJpaEntityFileDocumentJpaRepository를 추가했습니다.
    • files 테이블의 파일 메타데이터를 관리합니다.
    • objectKey 기반 조회와 삭제를 지원합니다.
  • FileDocumentPersistenceAdapter를 추가했습니다.
    • 도메인 저장소와 JPA 저장소를 연결합니다.
  • FileDocumentService를 추가했습니다.
    • 파일 형식, 카테고리, 파일명, 용량을 검증합니다.
    • S3 업로드 후 메타데이터를 저장합니다.
    • 메타데이터 저장에 실패하면 S3 객체를 보상 삭제합니다.
  • AWS S3 SDK 의존성과 S3 Client, Presigner 설정을 추가했습니다.
  • UploadFileCommand.fileName을 필수 String으로 변경했습니다.

위험 영역

  • 실제 S3 환경 검증과 신규 테스트는 포함하지 않았습니다.
  • 메타데이터 저장 실패 시 보상 삭제가 실패하면 고아 S3 객체가 남을 수 있습니다.
  • S3와 DB 저장은 단일 트랜잭션이 아니므로 상태 불일치 가능성이 있습니다.
  • presigned URL 만료 시간과 버킷 접근 정책을 운영 환경에서 확인해야 합니다.
  • AWS 자격 증명과 권한 설정이 올바르지 않으면 업로드와 URL 발급이 실패합니다.

마이그레이션 및 호환성

  • files 테이블 마이그레이션은 포함하지 않았습니다.
  • S3 버킷, IAM 정책, 환경변수 설정은 후속 작업이 필요합니다.
  • UploadFileCommand 호출부는 필수 fileName 인자에 맞게 수정해야 합니다.
  • REST 및 gRPC 진입점은 포함하지 않았습니다.

검증 체크리스트 및 롤아웃

  • 컴파일 확인
  • 기존 테스트 확인
  • 실제 S3 업로드 및 삭제 검증
  • presigned URL 다운로드 검증
  • AWS 예외 변환 검증
  • 메타데이터 저장 실패 시 보상 삭제 검증
  • 신규 단위 테스트와 통합 테스트 추가
  • files 테이블 마이그레이션 적용
  • 버킷, IAM, 환경변수 설정 적용
  • 운영 배포 전 S3와 DB 상태 불일치 모니터링 준비

Walkthrough

S3 기반 파일 저장소와 presigned URL 발급을 추가했다. 파일 메타데이터의 JPA 영속화 계층을 추가했다. FileDocumentService가 파일 검증, 객체 저장, 메타데이터 저장, 조회 흐름을 조정한다.

Changes

파일 저장 및 문서 관리

Layer / File(s) Summary
파일 문서 영속화
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt, systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt, systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt
FileDocumentJpaEntityfiles 테이블과 파일 메타데이터를 매핑한다. JPA 저장소가 ID와 objectKey 기반 작업을 제공한다. persistence adapter가 도메인 객체 변환과 저장소 호출을 위임한다.
S3 저장소 연결
systems/configuration/configuration-adapter-out/deps.bzl, systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/config/S3Config.kt, systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapter-out/S3StorageAdapter.kt
AWS SDK S3 의존성을 추가했다. S3Config가 리전 기반 S3ClientS3Presigner를 생성한다. S3StorageAdapter가 업로드, 체크섬 처리, presigned URL 발급, 존재 확인, 삭제를 구현한다.
파일 문서 서비스 흐름
systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt, systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt
UploadFileCommand의 파일명을 필수 값으로 변경했다. 서비스가 확장자·크기·파일명을 검증하고, S3 객체와 메타데이터를 저장한다. 메타데이터 저장 실패 시 고아 객체를 삭제한다. 다운로드 URL 발급과 파일 조회도 제공한다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: feature, kotlin, bazel

Suggested reviewers: wlyoon921, kusuri12-09

Sequence Diagram(s)

sequenceDiagram
  participant UploadFileCommand
  participant FileDocumentService
  participant S3StorageAdapter
  participant FileDocumentPersistenceAdapter
  UploadFileCommand->>FileDocumentService: 업로드 명령 전달
  FileDocumentService->>S3StorageAdapter: 파일 객체 업로드
  S3StorageAdapter-->>FileDocumentService: StoredObject 반환
  FileDocumentService->>FileDocumentPersistenceAdapter: 파일 메타데이터 저장
  FileDocumentPersistenceAdapter-->>FileDocumentService: FileDocument 반환
Loading
🚥 Pre-merge checks | ✅ 8 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 85.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Behavior Change Needs Tests ⚠️ Warning 변경된 Kotlin 로직은 application과 adapter-out에 추가됐지만 해당 모듈에는 moduleLoads 테스트만 있습니다. 설명은 테스트를 후속 이슈로 미뤘을 뿐 불필요 사유를 제시하지 않습니다. configuration-application과 configuration-adapter-out에 FileDocumentService, S3StorageAdapter, JPA 위임 로직의 정상·실패 경로를 검증하는 Kotlin 테스트를 추가하세요.
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 Conventional Commit 형식인 feat(configuration): <subject>을 따르며 S3·files 어댑터 구현을 설명하고 이슈 번호 #26을 포함합니다.
Description check ✅ Passed 설명이 S3 스토리지 어댑터, JPA 영속성 어댑터, 파일 문서 유스케이스 구현과 테스트 및 배포 범위를 구체적으로 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Kotlin Layer Boundary ✅ Passed 변경된 domain 파일에는 adapter/bootstrap import가 없습니다. 새 application 서비스의 Spring 의존성은 기존 EnvironmentVariableService와 사전 존재한 Spring 의존성 선언을 따릅니다.
Go Error Context ✅ Passed 확인한 PR 범위의 변경 파일은 Kotlin(.kt), Bzl(.bzl), Bazel(.bazel)뿐이며 Go(.go) 변경이 없습니다. 따라서 이 검사는 적용되지 않습니다.
Bazel Formatting ✅ Passed PR의 deps.bzl와 BUILD.bazel 변경은 기존 4칸 들여쓰기와 후행 쉼표 형식을 유지하며, 타깃 이름도 고정된 main·test·document_test 문자열을 사용합니다.
Todo Must Reference Issue ✅ Passed 기준 커밋과 HEAD에서 TODO/FIXME는 변경되지 않은 .coderabbit.yaml 설정 문구만 존재하며, PR의 추가 라인에는 해당 마커가 없습니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat(document)-파일-저장소-어댑터-#26
  • 🛠️ cleanup stale imports
  • 🛠️ harden error messages
  • 🛠️ test clarity pass

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tlgms
tlgms requested review from kusuri12-09 and wlyoon921 July 27, 2026 13:58
@tlgms

tlgms commented Aug 12, 2026

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@EntryDSM EntryDSM deleted a comment from coderabbitai Bot Aug 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt`:
- Around line 12-38: FileDocumentJpaEntity and FileDocumentPersistenceAdapter
require the files table schema to exist in the same deployment. Add a migration
defining all FileDocumentJpaEntity columns, the unique constraint on object_key,
and the required indexes, or prevent the upload functionality from being exposed
until that migration is applied.

In
`@systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt`:
- Around line 6-9: Update FileDocumentJpaRepository.deleteByObjectKey or the
corresponding FileDocumentService deletion method to explicitly declare a
write-enabled `@Transactional` boundary. Ensure the annotation overrides
FileDocumentService’s class-level readOnly = true setting so the derived delete
query executes within a transaction.

In
`@systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt`:
- Around line 74-94: Update S3StorageAdapter.exists and delete to translate
storage failures into the adapter’s domain exception, preserving the original
SdkException as the cause. In exists, return false for both NoSuchKeyException
and S3Exception responses with status code 404, while translating other
SdkException failures; translate deleteObject SdkException failures as well. Add
adapter-module tests covering success, 404, and other exception paths for both
methods.

In
`@systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt`:
- Around line 18-29: Remove the direct Spring annotations and `@Value`
configuration binding from FileDocumentService, keeping it as a plain Kotlin
application service. Move bean registration, presignExpirySeconds configuration,
and the read-only transaction boundary to the bootstrap or adapter composition
layer, then inject the resolved expiry value through the constructor while
preserving existing behavior.
- Around line 36-105: Add comprehensive tests for the changed file-storage flow:
in
systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt:36-105,
test validation, successful upload, metadata-save compensation, and download URL
flows; in
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt:30-94,
mock upload, presign, existence, deletion, and SDK exception translation; in
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt:14-29,
test mapping and persistence contracts; in
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt:12-61,
verify schema constraints and Instant mapping; in
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/config/S3Config.kt:15-25,
verify the configured region reaches both AWS clients; and in
systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt:5-10,
test uploads using the required fileName contract.
- Around line 43-58: Update the upload flow around FileDocumentService and
storagePort.upload so an existing objectKey is never overwritten or deleted on
metadata-save failure. Generate a unique object key or use conditional upload
semantics with explicit collision handling, and ensure orphan cleanup only
removes an object created by the current request; also cover object-key
collisions and metadata persistence failures with deterministic tests.
- Around line 46-60: FileDocumentService의 save 처리에서 JpaRepository.save의
flush·commit 실패까지 감지하도록 트랜잭션 완료 콜백을 등록하고, 완료 상태가 STATUS_COMMITTED가 아니면 S3 객체를
삭제하십시오. deleteOrphan 실패는 재시도 가능한 영속 보상 작업으로 기록하도록 보상 흐름을 확장하고, flush 실패·commit
실패·보상 실패를 검증하는 통합 테스트를 추가하십시오.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: bbaff4be-a60c-49db-9790-fd8528e6c20d

📥 Commits

Reviewing files that changed from the base of the PR and between eeddabd and 4b9254c.

⛔ Files ignored due to path filters (1)
  • kotlin.MODULE.bazel is excluded by none and included by none
📒 Files selected for processing (8)
  • systems/configuration/configuration-adapter-out/deps.bzl
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/config/S3Config.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt
  • systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt
  • systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
**/{BUILD.bazel,*.bzl}

📄 CodeRabbit inference engine (Custom checks)

In BUILD.bazel and .bzl files, require buildifier-compatible formatting and stable target naming

Files:

  • systems/configuration/configuration-adapter-out/deps.bzl
**/*.bzl

⚙️ CodeRabbit configuration file

**/*.bzl: Apply Bazel Starlark (.bzl) style guidance.

Readability and docs:

  • Keep file/module docstrings and docstrings for public functions/macros.
  • Use descriptive parameter names and document attribute intent.

API design:

  • Macros should take a name argument and derive generated target names from it.
  • Prefer keyword arguments when calling macros for clarity and stability.
  • Keep macro side effects predictable and visible.

Encapsulation:

  • Use private visibility for helper targets created by macros unless explicitly public.
  • Avoid exposing internal implementation targets unintentionally.

Tooling:

  • Enforce buildifier formatting and lint compliance.

Files:

  • systems/configuration/configuration-adapter-out/deps.bzl
**/*.{kt,go}

📄 CodeRabbit inference engine (Custom checks)

If production logic is changed in Kotlin or Go files, require corresponding test updates in the same subsystem unless the PR description explicitly justifies why tests are unnecessary

Files:

  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/config/S3Config.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt
  • systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt
  • systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt
**/*.{java,kt,scala,groovy,go,js,ts,tsx,jsx,py,rb,rs,cpp,c,h,hpp,cs}

📄 CodeRabbit inference engine (Custom checks)

Flag TODO/FIXME comments introduced by this PR that do not include an issue reference in the form #123 or a full tracker key like PROJ-123

Files:

  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/config/S3Config.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt
  • systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt
  • systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt
**/*.kt

⚙️ CodeRabbit configuration file

**/*.kt: Apply Kotlin Official Coding Conventions.

Formatting and structure:

  • Use 4 spaces for indentation; no tabs.
  • Keep files focused and readable; avoid horizontal alignment for spacing.
  • Place related declarations together and keep overloads adjacent.
  • Keep implementation member order stable and logical for readability.

Naming:

  • Package names are lowercase and do not use underscores.
  • Class/object names use UpperCamelCase.
  • Functions/properties/local variables use lowerCamelCase.
  • Constants use UPPER_SNAKE_CASE only for true constants.

API and null-safety:

  • Avoid platform type leakage in public APIs.
  • Use explicit types in public APIs when inference obscures meaning.
  • Prefer immutable values (val) over mutable values (var) unless mutation is required.
  • Flag nullable flows that can be replaced with safer modeling.

Imports and idioms:

  • Avoid wildcard imports unless justified by language/tooling conventions.
  • Prefer expression bodies for short, clear functions.
  • Prefer standard library idioms over custom utility wrappers when equivalent.

Architecture and tests:

  • Respect module boundaries (domain/application/adapter/bootstrap layering).
  • Highlight behavior-changing code that lacks corresponding unit/integration tests.
  • Ask for deterministic tests and meaningful assertions, not only happy-path checks.

Files:

  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/config/S3Config.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt
  • systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt
  • systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt
**/*-domain/**/*.{java,kt,scala,groovy}

📄 CodeRabbit inference engine (Custom checks)

For files under *-domain modules, fail if imports reference adapter or bootstrap packages

Files:

  • systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt
**/*-application/**/*.{java,kt,scala,groovy}

📄 CodeRabbit inference engine (Custom checks)

For files under *-application modules, flag direct dependency on infrastructure-specific framework classes unless justified

Files:

  • systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt
🪛 detekt (1.23.8)
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt

[warning] 83-83: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt

[warning] 57-57: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)

🔇 Additional comments (1)
systems/configuration/configuration-adapter-out/deps.bzl (1)

4-4: LGTM!

Comment on lines +12 to +38
@Entity
@Table(name = "files")
class FileDocumentJpaEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,

@Column(name = "original_name", nullable = false, length = 255)
val originalName: String,

@Column(name = "object_key", unique = true, nullable = false, length = 255)
val objectKey: String,

@Column(name = "bucket", nullable = false, length = 100)
val bucket: String,

@Column(name = "content_type", nullable = false, length = 100)
val contentType: String,

@Column(name = "size_bytes", nullable = false)
val sizeBytes: Long,

@Column(name = "checksum", nullable = false, length = 64)
val checksum: String,

@Column(name = "created_at", nullable = false)
val createdAt: Instant,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

files 테이블 마이그레이션을 이 변경과 함께 추가해야 합니다.

이 엔티티와 FileDocumentPersistenceAdapter는 즉시 files 테이블을 읽고 씁니다. 현재 PR 목표대로 마이그레이션을 후속 PR로 분리하면, 배포된 애플리케이션의 업로드 경로는 테이블 없음 오류로 실패합니다.

이 엔티티의 컬럼, object_key 고유 제약, 인덱스를 포함한 마이그레이션을 같은 배포 단위에 추가하거나, 마이그레이션 적용 전까지 기능을 노출하지 마십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt`
around lines 12 - 38, FileDocumentJpaEntity and FileDocumentPersistenceAdapter
require the files table schema to exist in the same deployment. Add a migration
defining all FileDocumentJpaEntity columns, the unique constraint on object_key,
and the required indexes, or prevent the upload functionality from being exposed
until that migration is applied.

Comment on lines +6 to +9
interface FileDocumentJpaRepository : JpaRepository<FileDocumentJpaEntity, Long> {
fun findByObjectKey(objectKey: String): FileDocumentJpaEntity?
fun deleteByObjectKey(objectKey: String)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

file='systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt'
printf '%s\n' '--- repository ---'
cat -n "$file"

printf '%s\n' '--- related files ---'
rg -n --glob '*.kt' 'FileDocumentJpaRepository|deleteByObjectKey|findByObjectKey|class FileDocumentPersistenceAdapter|interface .*Persistence' systems/configuration

printf '%s\n' '--- configuration module files ---'
git ls-files 'systems/configuration' | sed -n '1,160p'

Repository: EntryDSM/entrydsm-platform

Length of output: 10692


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- persistence adapter ---'
cat -n systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt

printf '%s\n' '--- application service ---'
cat -n systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt

printf '%s\n' '--- transaction annotations and dependency declarations ---'
rg -n --glob '*.kt' --glob '*.bzl' --glob '*.xml' --glob '*.yaml' --glob '*.yml' \
  '`@Transactional`|spring-tx|spring-data-jpa|JpaRepository|deleteBy' systems/configuration

Repository: EntryDSM/entrydsm-platform

Length of output: 13435


🌐 Web query:

Spring Data JPA reference transactionality declared query methods default transaction configuration derived delete query method

💡 Result:

In Spring Data JPA, the default transaction configuration applies only to methods inherited from repository interfaces like CrudRepository or JpaRepository [1][2]. Custom query methods, whether they are derived query methods (method name-based) or declared query methods (using @Query), do not receive any default transactional configuration [1][3][4]. To run these custom methods transactionally, you must explicitly annotate the method or the repository interface with @Transactional [1][2][5]. Key details regarding transactionality and delete operations: 1. Default Transaction Configuration: - Inherited methods (e.g., save, findById, delete) inherit transactional attributes from SimpleJpaRepository [1][2][5]. - Methods marked as read-only (like findAll) have the readOnly flag set to true [1][6]. Other inherited operations use a plain @Transactional [1][2]. - Custom methods (derived or declared) are not transactional by default to avoid unnecessary overhead for simple select operations [1][4]. 2. Derived Delete Query Methods: - These methods (e.g., void deleteByRoleId(long roleId)) are not inherently transactional [1][4]. If you need them to participate in a transaction, you must apply @Transactional [1]. - Behaviorally, derived delete methods perform a search first to identify the entities, then call the persistence provider's remove method for each, ensuring JPA lifecycle callbacks (such as @PreRemove) are invoked [7][3][8]. This process can be memory-intensive if many entities are deleted [7]. 3. Declared Query Methods: - Methods annotated with @Query (especially those with @Modifying) also lack default transaction configuration [1][2]. - When using @Modifying for bulk updates or deletes, it is required to annotate the method with @Transactional to ensure the operation executes with readOnly=false, allowing the database modification to persist [1][2][5]. To ensure consistency, it is recommended to explicitly declare transaction boundaries for custom repository methods that perform write operations [3][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- analogous repository and service deletion ---'
cat -n systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/EnvironmentVariableJpaRepository.kt
cat -n systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/EnvironmentVariableService.kt

printf '%s\n' '--- all FileDocumentRepository call sites ---'
rg -n --glob '*.kt' 'FileDocumentRepository|deleteByObjectKey' .

Repository: EntryDSM/entrydsm-platform

Length of output: 5805


🏁 Script executed:

#!/bin/bash
set -e

python3 - <<'PY'
from pathlib import Path
import re

repo = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt")
adapter = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt")
service = Path("systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt")

repo_text = repo.read_text()
adapter_text = adapter.read_text()
service_text = service.read_text()

method = re.search(r'(?P<annotations>(?:\s*@\w+(?:\([^)]*\))?\s*)*)\s*fun deleteByObjectKey', repo_text)
adapter_method = re.search(r'(?P<annotations>(?:\s*@\w+(?:\([^)]*\))?\s*)*)\s*override fun deleteByObjectKey', adapter_text)

print("repository_delete_method_has_transactional=",
      bool(method and "`@Transactional`" in method.group("annotations")))
print("adapter_delete_method_has_transactional=",
      bool(adapter_method and "`@Transactional`" in adapter_method.group("annotations")))
print("service_delete_call_count=",
      len(re.findall(r'\bdeleteByObjectKey\s*\(', service_text)))
print("adapter_delete_call_count=",
      len(re.findall(r'\bdeleteByObjectKey\s*\(', adapter_text)))
print("service_class_transactional_read_only=",
      bool(re.search(r'`@Transactional`\s*\(\s*readOnly\s*=\s*true\s*\)', service_text)))
PY

printf '%s\n' '--- focused diff summary ---'
git diff --stat -- systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapter-out systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application

Repository: EntryDSM/entrydsm-platform

Length of output: 392


deleteByObjectKey에 쓰기 트랜잭션 경계를 추가하세요.

파생 쿼리 메서드는 기본 트랜잭션을 받지 않습니다. 트랜잭션 없이 호출하면 삭제가 TransactionRequiredException으로 실패할 수 있습니다. FileDocumentJpaRepository.deleteByObjectKey 또는 애플리케이션 서비스의 삭제 메서드에 @Transactional을 선언하세요. FileDocumentService의 클래스 기본값은 readOnly = true이므로 삭제 메서드에 명시적으로 선언해야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt`
around lines 6 - 9, Update FileDocumentJpaRepository.deleteByObjectKey or the
corresponding FileDocumentService deletion method to explicitly declare a
write-enabled `@Transactional` boundary. Ensure the annotation overrides
FileDocumentService’s class-level readOnly = true setting so the derived delete
query executes within a transaction.

Comment on lines +74 to +94
override fun exists(objectKey: String): Boolean =
try {
s3Client.headObject(
HeadObjectRequest.builder()
.bucket(bucket)
.key(objectKey)
.build()
)
true
} catch (e: NoSuchKeyException) {
false
}

override fun delete(objectKey: String) {
s3Client.deleteObject(
DeleteObjectRequest.builder()
.bucket(bucket)
.key(objectKey)
.build()
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

file="systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt"

printf '%s\n' '--- outline ---'
ast-grep outline "$file" || true

printf '%s\n' '--- source ---'
cat -n "$file"

printf '%s\n' '--- related storage symbols ---'
rg -n --glob '*.kt' 'interface .*Storage|class .*Storage|StorageException|NoSuchKeyException|headObject|deleteObject|exists\(objectKey|delete\(objectKey' systems

printf '%s\n' '--- related tests ---'
rg --files | rg '(^|/)(test|tests)(/|$)|S3StorageAdapter|Storage' | head -200

printf '%s\n' '--- diff summary ---'
git diff --stat -- systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt

Repository: EntryDSM/entrydsm-platform

Length of output: 10445


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- StoragePort ---'
cat -n systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/port/out/StoragePort.kt

printf '%s\n' '--- storage exceptions ---'
for f in systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/exception/*.kt; do
    printf '\n--- %s ---\n' "$f"
    cat -n "$f"
done

printf '%s\n' '--- FileDocumentService relevant methods ---'
cat -n systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt | sed -n '45,120p'

printf '%s\n' '--- adapter-out test files and contents ---'
find systems/configuration/configuration-adapter-out/src/test -type f -maxdepth 20 -print -exec sh -c 'echo "--- $1 ---"; cat -n "$1"' _ {} \;

printf '%s\n' '--- repository state and recent commit summary ---'
git status --short
git diff --name-status
git log -5 --oneline -- systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt

Repository: EntryDSM/entrydsm-platform

Length of output: 6923


🌐 Web query:

AWS SDK for Java 2.x S3 HeadObject NoSuchKeyException 404 behavior deleteObject SdkException

💡 Result:

When using the AWS SDK for Java 2.x, calling headObject on a non-existent S3 object does not always throw a NoSuchKeyException as might be expected [1][2][3]. Because the S3 HEAD operation does not return a response body, the service cannot provide the detailed XML error message typically used to map specific error types [1][4]. As a result, the SDK often throws a generic S3Exception containing a 404 status code instead of the more specific NoSuchKeyException [1][2][3]. To reliably handle this behavior in your code, you should catch S3Exception and inspect the status code rather than relying exclusively on catching NoSuchKeyException [1]. Example approach: try { s3Client.headObject(request); } catch (S3Exception e) { if (e.statusCode == 404) { // Handle the case where the object does not exist } else { // Handle other S3 errors throw e; } } Regarding your mention of deleteObject, this operation also throws an SdkException (or a subclass) if the request fails [5][6]. However, note that S3 deleteObject is designed to be idempotent; if you attempt to delete a key that does not exist, S3 typically returns a 204 No Content success response, meaning it will not throw an exception solely because the key was already missing. If an exception is thrown during deleteObject, it is usually due to network issues, permission errors, or other service-side failures, which are captured as an SdkException [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

adapter = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt").read_text()
port = Path("systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/port/out/StoragePort.kt").read_text()
exceptions = sorted(Path("systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/exception").glob("*.kt"))

print("adapter methods:")
for method in ("upload", "issueDownloadUrl", "exists", "delete"):
    match = re.search(rf"override fun {method}\b.*?(?=\n    override fun |\n}})", adapter, re.S)
    body = match.group(0) if match else ""
    catches = re.findall(r"catch\s*\(\s*\w+\s*:\s*([A-Za-z0-9_.<>?]+)", body)
    throws = re.findall(r"throw\s+([A-Za-z0-9_]+)", body)
    print(f"{method}: catches={catches or []}, throws={throws or []}, sdk_calls={re.findall(r's3(?:Client|Presigner)\.\w+', body)}")

print("StoragePort operations:", re.findall(r"^\s*fun\s+(\w+)\s*\(", port, re.M))
print("domain exception classes:", [
    re.search(r"class\s+([A-Za-z0-9_]+)", p.read_text()).group(1)
    for p in exceptions
])
print("adapter tests:", [
    str(p) for p in Path("systems/configuration/configuration-adapter-out/src/test").rglob("*.kt")
])
PY

printf '%s\n' '--- exception handling references ---'
rg -n --glob '*.kt' 'PresignFailedException|StorageUploadFailedException|FileDocumentNotFoundException|`@ExceptionHandler`|ResponseStatusException|RuntimeException' systems/configuration

printf '%s\n' '--- implementing adapters ---'
rg -n --glob '*.kt' ': StoragePort|implements StoragePort|StoragePort' systems/configuration

Repository: EntryDSM/entrydsm-platform

Length of output: 521


🏁 Script executed:

#!/bin/bash
set -eu

commit="$(git log -1 --format=%H -- systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt)"
printf '%s\n' "--- commit $commit ---"
git show --stat --oneline "$commit" -- systems/configuration
printf '%s\n' '--- S3 adapter commit diff ---'
git show --format= --no-ext-diff "$commit" -- systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt

Repository: EntryDSM/entrydsm-platform

Length of output: 4609


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

adapter = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt").read_text()
port = Path("systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/port/out/StoragePort.kt").read_text()
exception_dir = Path("systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/exception")

print("adapter methods:")
for method in ("upload", "issueDownloadUrl", "exists", "delete"):
    match = re.search(rf"override fun {method}\b.*?(?=\n    override fun |\n}})", adapter, re.S)
    body = match.group(0) if match else ""
    catches = re.findall(r"catch\s*\(\s*\w+\s*:\s*([A-Za-z0-9_.<>?]+)", body)
    throws = re.findall(r"throw\s+([A-Za-z0-9_]+)", body)
    sdk_calls = re.findall(r"s3(?:Client|Presigner)\.\w+", body)
    print(f"{method}: catches={catches or []}, throws={throws or []}, sdk_calls={sdk_calls}")

print("StoragePort operations:", re.findall(r"^\s*fun\s+(\w+)\s*\(", port, re.M))
print("domain exception classes:", sorted(
    re.search(r"class\s+([A-Za-z0-9_]+)", p.read_text()).group(1)
    for p in exception_dir.glob("*.kt")
))
print("adapter tests:", sorted(
    str(p) for p in Path("systems/configuration/configuration-adapter-out/src/test").rglob("*.kt")
))
PY

printf '%s\n' '--- configuration exception handling references ---'
rg -n --glob '*.kt' 'PresignFailedException|StorageUploadFailedException|FileDocumentNotFoundException|`@ExceptionHandler`|ResponseStatusException|RuntimeException' systems/configuration || true

printf '%s\n' '--- StoragePort implementations ---'
rg -n --glob '*.kt' ': StoragePort|implements StoragePort|StoragePort' systems/configuration

Repository: EntryDSM/entrydsm-platform

Length of output: 5862


existsdelete의 스토리지 예외를 도메인 예외로 변환하십시오.

headObject의 객체 없음 응답은 NoSuchKeyException이 아닌 상태 코드 404의 S3Exception으로 발생할 수 있습니다. 404를 false로 처리하고, 그 외 SdkException은 원인을 보존한 도메인 예외로 변환하십시오. deleteObjectSdkException도 도메인 예외로 변환하십시오. 두 메서드의 성공, 404, 기타 예외 경로를 검증하는 테스트를 같은 어댑터 모듈에 추가하십시오.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 83-83: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt`
around lines 74 - 94, Update S3StorageAdapter.exists and delete to translate
storage failures into the adapter’s domain exception, preserving the original
SdkException as the cause. In exists, return false for both NoSuchKeyException
and S3Exception responses with status code 404, while translating other
SdkException failures; translate deleteObject SdkException failures as well. Add
adapter-module tests covering success, 404, and other exception paths for both
methods.

Comment on lines +18 to +29
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.io.InputStream

@Service
@Transactional(readOnly = true)
class FileDocumentService(
private val storagePort: StoragePort,
private val fileDocumentRepository: FileDocumentRepository,
@Value("\${aws.s3.presign-expiry-seconds:600}") private val presignExpirySeconds: Long,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

애플리케이션 계층에서 Spring 의존성을 제거해야 합니다.

FileDocumentService@Service, @Transactional, @Value를 직접 사용합니다. 애플리케이션 서비스는 일반 Kotlin 객체로 유지하십시오. Spring 빈 등록, 설정 바인딩, 트랜잭션 경계는 bootstrap 또는 adapter 계층의 구성 요소로 이동하십시오.

As per coding guidelines, “For files under *-application modules, flag direct dependency on infrastructure-specific framework classes unless justified”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt`
around lines 18 - 29, Remove the direct Spring annotations and `@Value`
configuration binding from FileDocumentService, keeping it as a plain Kotlin
application service. Move bean registration, presignExpirySeconds configuration,
and the read-only transaction boundary to the bootstrap or adapter composition
layer, then inject the resolved expiry value through the constructor while
preserving existing behavior.

Source: Coding guidelines

Comment on lines +36 to +105
@Transactional
override fun upload(command: UploadFileCommand, content: InputStream): FileDocument {
val extension = resolveExtension(command)
if (command.category.exceedsMaxSize(command.sizeBytes)) {
throw FileTooLargeException(command.sizeBytes, command.category.maxSizeBytes)
}

val objectKey = command.category.objectKeyOf(FileNaming.requireSafeFileName(command.fileName))
val stored = storagePort.upload(objectKey, extension.contentType, command.sizeBytes, content)

return try {
fileDocumentRepository.save(
FileDocument(
originalName = command.originalName,
objectKey = stored.objectKey,
bucket = stored.bucket,
contentType = extension.contentType,
sizeBytes = command.sizeBytes,
checksum = stored.checksum,
)
)
} catch (e: RuntimeException) {
deleteOrphan(objectKey)
throw e
}
}

override fun issueByCommand(command: IssueDownloadUrlCommand): DownloadUrl {
val fileName = FileNaming.requireSafeFileName(command.fileName)
val objectKey = command.category.objectKeyOf(fileName)
if (!storagePort.exists(objectKey)) throw FileDocumentNotFoundException(objectKey)
return DownloadUrl(
fileName = fileName,
downloadUrl = storagePort.issueDownloadUrl(objectKey, presignExpirySeconds),
expiresIn = presignExpirySeconds,
)
}

override fun issueById(id: Long): DownloadUrl {
val fileDocument = findById(id)
return DownloadUrl(
fileName = fileDocument.originalName,
downloadUrl = storagePort.issueDownloadUrl(fileDocument.objectKey, presignExpirySeconds),
expiresIn = presignExpirySeconds,
)
}

override fun findById(id: Long): FileDocument =
fileDocumentRepository.findById(id) ?: throw FileDocumentNotFoundException("id=$id")

override fun findByFileName(category: FileCategory, fileName: String): FileDocument? =
fileDocumentRepository.findByObjectKey(
category.objectKeyOf(FileNaming.requireSafeFileName(fileName))
)

override fun existsById(id: Long): Boolean =
fileDocumentRepository.existsById(id)

private fun resolveExtension(command: UploadFileCommand): FileExtension {
val extension = FileExtension.fromFileName(command.originalName)
?: throw InvalidFileFormatException(command.originalName, command.category)
if (!command.category.supports(extension)) {
throw InvalidFileFormatException(command.originalName, command.category)
}
return extension
}

private fun deleteOrphan(objectKey: String) {
runCatching { storagePort.delete(objectKey) }
.onFailure { log.warn("Failed to delete orphaned object: {}", objectKey, it) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

변경된 파일 저장 흐름의 결정적 테스트를 추가해야 합니다.

PR 목표는 신규 테스트를 포함하지 않았다고 명시합니다. 이 변경은 외부 S3 호출, DB 영속화, 보상 삭제, 공개 명령 계약을 함께 변경합니다. 병합 전 테스트가 필요합니다.

  • systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt#L36-L105: 형식·크기 검증, 성공 저장, 메타데이터 실패 보상, 다운로드 URL 흐름을 테스트하십시오.
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt#L30-L94: 업로드, presign, 존재 확인, 삭제와 SDK 예외 변환을 mock 기반으로 테스트하십시오.
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt#L14-L29: 도메인-엔티티 변환과 저장·조회·삭제 계약을 테스트하십시오.
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt#L12-L61: files 스키마의 제약과 Instant 매핑을 영속성 테스트로 검증하십시오.
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/config/S3Config.kt#L15-L25: 설정 리전이 두 AWS 클라이언트에 적용되는지 테스트하십시오.
  • systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt#L5-L10: 필수 fileName 계약을 사용하는 업로드 호출을 테스트하십시오.

As per coding guidelines, “If production logic is changed in Kotlin or Go files, require corresponding test updates in the same subsystem unless the PR description explicitly justifies why tests are unnecessary”.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 57-57: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)

📍 Affects 6 files
  • systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt#L36-L105 (this comment)
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt#L30-L94
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt#L14-L29
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt#L12-L61
  • systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/config/S3Config.kt#L15-L25
  • systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt#L5-L10
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt`
around lines 36 - 105, Add comprehensive tests for the changed file-storage
flow: in
systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt:36-105,
test validation, successful upload, metadata-save compensation, and download URL
flows; in
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt:30-94,
mock upload, presign, existence, deletion, and SDK exception translation; in
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt:14-29,
test mapping and persistence contracts; in
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt:12-61,
verify schema constraints and Instant mapping; in
systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/config/S3Config.kt:15-25,
verify the configured region reaches both AWS clients; and in
systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt:5-10,
test uploads using the required fileName contract.

Source: Coding guidelines

Comment on lines +43 to +58
val objectKey = command.category.objectKeyOf(FileNaming.requireSafeFileName(command.fileName))
val stored = storagePort.upload(objectKey, extension.contentType, command.sizeBytes, content)

return try {
fileDocumentRepository.save(
FileDocument(
originalName = command.originalName,
objectKey = stored.objectKey,
bucket = stored.bucket,
contentType = extension.contentType,
sizeBytes = command.sizeBytes,
checksum = stored.checksum,
)
)
} catch (e: RuntimeException) {
deleteOrphan(objectKey)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target service ---'
cat -n systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt | sed -n '1,130p'

printf '%s\n' '--- entity definitions and schema references ---'
rg -n -C 5 'class FileDocumentJpaEntity|objectKey|object_key|unique|`@Column`' systems/configuration

printf '%s\n' '--- storage port and adapters ---'
rg -n -C 5 'interface .*Storage|upload\(|deleteOrphan|delete\(' systems/configuration

printf '%s\n' '--- transaction and repository usage ---'
rg -n -C 4 '`@Transactional`|FileDocumentRepository|save\(' systems/configuration/configuration-application systems/configuration/configuration-adapter systems/configuration 2>/dev/null || true

printf '%s\n' '--- related tests ---'
git ls-files | rg '(^|/)(test|src/test)/|FileDocument|configuration' | rg 'Test|test' || true

Repository: EntryDSM/entrydsm-platform

Length of output: 50383


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- persistence adapter ---'
cat -n systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt

printf '%s\n' '--- repository declarations ---'
rg -n -C 4 'interface FileDocumentJpaRepository|findByObjectKey|deleteByObjectKey' systems/configuration/configuration-adapter-out/src/main

printf '%s\n' '--- key generation and commands ---'
cat -n systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/FileNaming.kt
cat -n systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/FileCategory.kt
cat -n systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/command/UploadFileCommand.kt

printf '%s\n' '--- schema/migrations ---'
rg -n -C 3 'create table.*files|object_key|files' --glob '*.sql' --glob '*.xml' --glob '*.yaml' --glob '*.yml' .

printf '%s\n' '--- focused tests ---'
git ls-files | rg 'FileDocument|configuration.*test|test.*configuration' | head -100

Repository: EntryDSM/entrydsm-platform

Length of output: 19776


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path

roots = [
    Path("systems/configuration"),
]
terms = ("FileDocumentService", "storagePort.upload", "fileDocumentRepository.save",
         "FileDocumentPersistenceAdapter", "S3StorageAdapter")
for root in roots:
    for path in root.rglob("*"):
        if path.is_file() and path.suffix in {".kt", ".java", ".sql", ".yml", ".yaml", ".xml"}:
            try:
                text = path.read_text()
            except UnicodeDecodeError:
                continue
            if any(term in text for term in terms):
                print(path)
                for i, line in enumerate(text.splitlines(), 1):
                    if any(term in line for term in terms):
                        print(f"  {i}: {line.strip()}")
PY
PYTHONUNBUFFERED=1

Repository: EntryDSM/entrydsm-platform

Length of output: 796


🌐 Web query:

AWS S3 PutObject existing object same key overwrites object conditional If-None-Match documentation

💡 Result:

When performing a PutObject operation in Amazon S3, you can use the If-None-Match header to prevent the overwriting of an existing object with the same key [1][2]. To use this conditional write, you must set the If-None-Match header value to the asterisk (*) character [1][2]. The behavior of this operation is as follows: - If no object with the same key currently exists in the bucket, the upload succeeds with a 200 OK response [1]. - If an object with the same key already exists, the operation fails and returns a 412 Precondition Failed error [1][2]. - In cases where a conflicting operation occurs during the upload process (such as a concurrent write), S3 may return a 409 ConditionalRequestConflict error, in which case you should retry the request [1][2]. Note that this functionality is not supported for S3 on Outposts [2]. For buckets with versioning enabled, the operation evaluates against the current object version; if no current version exists or the current version is a delete marker, the upload succeeds [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path

service = Path("systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt").read_text()
adapter = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt").read_text()
persistence = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt").read_text()
entity = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt").read_text()

checks = {
    "key_is_category_plus_filename": 'command.category.objectKeyOf(FileNaming.requireSafeFileName(command.fileName))' in service,
    "upload_precedes_save": service.index("storagePort.upload") < service.index("fileDocumentRepository.save"),
    "compensation_deletes_same_key": 'storagePort.delete(objectKey)' in service,
    "s3_upload_uses_put_object": 's3Client.putObject(' in adapter,
    "s3_upload_has_no_if_none_match": "ifNoneMatch" not in adapter and "If-None-Match" not in adapter,
    "object_key_unique": '`@Column`(name = "object_key", unique = true' in entity,
    "save_does_not_flush": '.save(' in persistence and '.saveAndFlush(' not in persistence,
}
for name, result in checks.items():
    print(f"{name}={result}")

# Show the transaction-sensitive structure without executing repository code.
try_start = service.index("return try")
try_end = service.index("        } catch", try_start)
print("try_scope_contains_save=", "fileDocumentRepository.save" in service[try_start:try_end])
print("try_scope_contains_delete=", "deleteOrphan(objectKey)" in service[try_start:try_end])
print("transaction_annotation_present=", "`@Transactional`" in service)
PY

Repository: EntryDSM/entrydsm-platform

Length of output: 475


객체 키 충돌 시 기존 객체를 삭제하지 않도록 수정하십시오.

FileNaming.requireSafeFileName은 고유성을 보장하지 않습니다. 동일한 카테고리와 파일명은 동일한 objectKey를 생성하며, S3StorageAdapterputObject는 기존 객체를 덮어씁니다. 이후 저장 예외가 발생하면 deleteOrphan(objectKey)가 기존 객체까지 삭제할 수 있습니다. 트랜잭션 커밋 시 발생하는 제약 조건 예외는 이 try/catch 범위 밖에서 발생할 수도 있습니다.

고유 키를 생성하거나 If-None-Match: * 조건부 업로드와 충돌 처리를 사용하십시오. 기존 키 정책을 유지하면 충돌 및 메타데이터 저장 실패를 검증하는 결정적 테스트도 추가하십시오.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 57-57: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt`
around lines 43 - 58, Update the upload flow around FileDocumentService and
storagePort.upload so an existing objectKey is never overwritten or deleted on
metadata-save failure. Generate a unique object key or use conditional upload
semantics with explicit collision handling, and ensure orphan cleanup only
removes an object created by the current request; also cover object-key
collisions and metadata persistence failures with deterministic tests.

Comment on lines +46 to +60
return try {
fileDocumentRepository.save(
FileDocument(
originalName = command.originalName,
objectKey = stored.objectKey,
bucket = stored.bucket,
contentType = extension.contentType,
sizeBytes = command.sizeBytes,
checksum = stored.checksum,
)
)
} catch (e: RuntimeException) {
deleteOrphan(objectKey)
throw e
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

target='systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt'

printf '%s\n' '--- target outline ---'
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline "$target" || true
fi

printf '%s\n' '--- target source ---'
cat -n "$target"

printf '%s\n' '--- related files ---'
git ls-files | grep -Ei '(^|/)(FileDocument|file.?document|configuration).*' | head -200

printf '%s\n' '--- transaction annotations and symbols ---'
rg -n -C 3 'FileDocumentService|fileDocumentRepository|`@Transactional`|deleteOrphan|objectKey|class FileDocument' systems/configuration systems 2>/dev/null | head -400

Repository: EntryDSM/entrydsm-platform

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -eu

files=(
  'systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt'
  'systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt'
  'systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/repository/FileDocumentJpaRepository.kt'
  'systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/port/out/FileDocumentRepository.kt'
  'systems/configuration/configuration-domain/src/main/kotlin/hs/kr/entrydsm/configuration/domain/document/StoredObject.kt'
  'systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/S3StorageAdapter.kt'
)

for file in "${files[@]}"; do
  printf '\n--- %s ---\n' "$file"
  test -f "$file" && cat -n "$file" || printf 'MISSING\n'
done

printf '\n--- application tests and upload callers ---\n'
git ls-files 'systems/configuration/*/src/test/**' | sort
rg -n -C 4 'upload\(|UploadFileUseCase|ConfigurationGrpcService|FileDocumentService' \
  systems/configuration/configuration-adapter-in \
  systems/configuration/configuration-application \
  systems/configuration/configuration-bootstrap \
  systems/configuration/configuration-adapter-out 2>/dev/null | head -300

printf '\n--- transaction configuration ---\n'
rg -n -C 3 'PlatformTransactionManager|JpaTransactionManager|`@EnableTransactionManagement`|spring.jpa|flush-mode|ddl-auto|transaction' \
  systems/configuration build.gradle.kts settings.gradle.kts gradle.properties 2>/dev/null | head -300

Repository: EntryDSM/entrydsm-platform

Length of output: 24951


🌐 Web query:

Spring Framework official TransactionSynchronization afterCompletion STATUS_COMMITTED STATUS_ROLLED_BACK STATUS_UNKNOWN transaction synchronization documentation

💡 Result:

In the Spring Framework, the TransactionSynchronization interface provides callback methods for transaction lifecycle events [1][2]. The afterCompletion(int status) method is invoked after a transaction has been committed or rolled back [1][2]. The status parameter indicates the outcome of the transaction using the following constant integer values defined in the TransactionSynchronization interface [1][2]: STATUS_COMMITTED (value: 0): Indicates the transaction completed via a proper commit [1][2]. STATUS_ROLLED_BACK (value: 1): Indicates the transaction completed via a proper rollback [1][2]. STATUS_UNKNOWN (value: 2): Indicates the transaction completion state is unknown, such as in cases of heuristic mixed completion or system errors [1][2]. Important Considerations: Transactional resources may still be active and accessible when afterCompletion is called [1][2]. However, as the transaction has already finished, data access code executed at this stage will typically participate in the original transaction context, meaning no further commit will occur for those operations [1][2]. If you need to perform transactional operations within this callback that require their own transaction, you must use propagation behavior PROPAGATION_REQUIRES_NEW [1][2]. Exceptions thrown within afterCompletion are logged by the framework but are not propagated to the caller [1][2]. Note that throwing TransactionException subclasses should be avoided in this method [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

service = Path("systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt").read_text()
adapter = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt").read_text()
entity = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt").read_text()
tests = list(Path("systems/configuration/configuration-application/src/test").rglob("*.kt"))

checks = {
    "class_transactional_read_only": "`@Transactional`(readOnly = true)" in service,
    "upload_transactional": bool(re.search(r"`@Transactional`\s+override fun upload", service)),
    "upload_before_save": service.indexOf("storagePort.upload") if False else service.index("storagePort.upload") < service.index("fileDocumentRepository.save"),
    "save_inside_try": bool(re.search(r"return try\s*\{.*?fileDocumentRepository\.save", service, re.S)),
    "delete_only_inside_catch": bool(re.search(r"catch\s*\(e: RuntimeException\)\s*\{\s*deleteOrphan\(objectKey\)", service, re.S)),
    "adapter_uses_save_not_flush": "fileDocumentJpaRepository.save(" in adapter and "saveAndFlush" not in adapter,
    "object_key_unique": "object_key" in entity and "unique = true" in entity,
    "application_behavior_tests": len(tests) > 1,
}

for name, result in checks.items():
    print(f"{name}={result}")
print("application_test_files=" + ",".join(str(p) for p in tests))
PY

Repository: EntryDSM/entrydsm-platform

Length of output: 503


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

service = Path("systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt").read_text()
adapter = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/FileDocumentPersistenceAdapter.kt").read_text()
entity = Path("systems/configuration/configuration-adapter-out/src/main/kotlin/hs/kr/entrydsm/configuration/adapterout/entity/FileDocumentJpaEntity.kt").read_text()
tests = list(Path("systems/configuration/configuration-application/src/test").rglob("*.kt"))

checks = {
    "class_transactional_read_only": "`@Transactional`(readOnly = true)" in service,
    "upload_transactional": bool(re.search(r"`@Transactional`\s+override fun upload", service)),
    "upload_before_save": service.index("storagePort.upload") < service.index("fileDocumentRepository.save"),
    "save_inside_try": bool(re.search(r"return try\s*\{.*?fileDocumentRepository\.save", service, re.S)),
    "delete_only_inside_catch": bool(re.search(r"catch\s*\(e: RuntimeException\)\s*\{\s*deleteOrphan\(objectKey\)", service, re.S)),
    "adapter_uses_save_not_flush": "fileDocumentJpaRepository.save(" in adapter and "saveAndFlush" not in adapter,
    "object_key_unique": "object_key" in entity and "unique = true" in entity,
    "application_behavior_tests": len(tests) > 1,
}

for name, result in checks.items():
    print(f"{name}={result}")
print("application_test_files=" + ",".join(str(p) for p in tests))
PY

Repository: EntryDSM/entrydsm-platform

Length of output: 503


트랜잭션 완료 후 DB 실패에도 S3 보상을 실행하십시오.

JpaRepository.save의 flush 또는 commit 실패는 현재 catch 범위 밖에서 발생할 수 있습니다. 트랜잭션 완료 콜백에서 STATUS_COMMITTED가 아닌 경우 S3 삭제를 실행하고, 삭제 실패는 재시도 가능한 영속 보상 작업으로 기록하십시오. 해당 flush·commit 실패와 보상 실패를 검증하는 통합 테스트도 추가하십시오.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 57-57: The caught exception is too generic. Prefer catching specific exceptions to the case that is currently handled.

(detekt.exceptions.TooGenericExceptionCaught)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@systems/configuration/configuration-application/src/main/kotlin/hs/kr/entrydsm/configuration/application/FileDocumentService.kt`
around lines 46 - 60, FileDocumentService의 save 처리에서 JpaRepository.save의
flush·commit 실패까지 감지하도록 트랜잭션 완료 콜백을 등록하고, 완료 상태가 STATUS_COMMITTED가 아니면 S3 객체를
삭제하십시오. deleteOrphan 실패는 재시도 가능한 영속 보상 작업으로 기록하도록 보상 흐름을 확장하고, flush 실패·commit
실패·보상 실패를 검증하는 통합 테스트를 추가하십시오.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant