Skip to content

version 2.17.0#345

Merged
rlagkswn00 merged 4 commits intomainfrom
develop
Feb 13, 2026
Merged

version 2.17.0#345
rlagkswn00 merged 4 commits intomainfrom
develop

Conversation

@rlagkswn00
Copy link
Member

@rlagkswn00 rlagkswn00 commented Feb 13, 2026

version 2.17.0

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 클라우드 스토리지 지원 추가 (S3 호환 스토리지로 파일 업로드, 다운로드, 삭제 가능)
    • 파일 업로드 크기 제한 설정 (파일당 10MB, 요청당 50MB)
    • 개발/운영 환경별 스토리지 설정 지원
  • Tests

    • 스토리지 어댑터 테스트 케이스 추가
  • Chores

    • 의존성 업데이트 (Testcontainers, AWS SDK)
    • 배포 환경 설정 개선

jiyun921 and others added 4 commits January 24, 2026 22:34
* [refactor]: hostprefix - rest도 부동산학과 조회 가능하도록 수정

* [refactor]: 학과 공지 조회 조건 null 방어 처리

* [test]: DepartmentName fromHostPrefix (rest/kure) 변환 및 예외 테스트 추가

* [refactor]: DepartmentName legacy hostPrefix 분리

* [test]: hostPrefixMap 테스트 구현

* [refactor]: 변수명 fallbackHostPrefix로 변경

* [refactor]: 변수명 fallbackHostPrefix로 변경

* [refactor]: hostPrefix 매칭 책임 캡슐화

* [refactor]: HOST_PREFIX_MAP O(n)으로 로직 수정
… 업그레이드) (#343)

* fix : testcontainers 버전 2.0.2 migration

* fix : apache common-lang3 3.13.0 -> 3.18.0 업그레이드
* fix : FCM 메시지 공지 id 추가

* feat : 공지사항 JpaRepository SaveAll 사용하는 Port & Adapter 메서드 추가

* fix : 업데이트 시 DB에 저장한 내용 다시 return하도록 수정

* fix : 업데이트 시 DB에 저장한 내용 다시 return하도록 수정(대학원)

* fix : firebase notice 알림 보낼 시 로깅에 id 추가
* chore : AWS S3 SDK 의존성 추가

* feat : CloudStorageConfig 추가

* feat : StoragePort 추가

* feat : S3CompatibleStorageAdapter 추가

* feat : MockStorageAdapter 추가

* feat : CloudStorageException & ErrorCode 추가

* feat : application 환경설정 추가(common & test)

* feat : CloudStorageProperties 추가

* refactor : ErrorCode Enum 이름 변경(STORAGE_S3_SDK_PROBLEM)

* fix : MockStorageAdapter 파일 조회 시 에러 수정

* fix : StoragePort PresignedUrl String으로 반환받도록 수정

* fix : 불필요 throws 구문 수정

* test : S3CompatibleStorageAdapterTest 추가

* test : URL과 String 비교로 인한 테스트 실패하는 현상 수정

* refactor : CloudStorageConfig 중복 Builder 메서드화

* feat: dev.yml environment 추가

* fix : CloudStorageException 계층구조 변경

* feat : InfrastructureException 추가

* fix : Storage Upload간 byte 읽어오지 않고 바로 스트리밍 하도록 수정
@rlagkswn00 rlagkswn00 self-assigned this Feb 13, 2026
@rlagkswn00 rlagkswn00 added the 🚀 Deploy Production deploy production server label Feb 13, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Walkthrough

이 PR은 S3 호환 클라우드 스토리지 지원(AWS S3, OCI 포함), 새로운 인프라 예외 처리 체계, 테스트 컨테이너 및 AWS SDK 의존성 업데이트, 파일 업로드 및 스토리지 구성 추가, 공지사항 저장소 로직 개선을 포함합니다.

Changes

Cohort / File(s) Summary
클라우드 스토리지 인프라
src/main/java/com/kustacks/kuring/storage/application/port/out/StoragePort.java, src/main/java/com/kustacks/kuring/storage/adapter/out/S3CompatibleStorageAdapter.java, src/main/java/com/kustacks/kuring/storage/adapter/out/MockStorageAdapter.java, src/main/java/com/kustacks/kuring/storage/exception/CloudStorageException.java
StoragePort 인터페이스 추가, S3 호환 및 목업 스토리지 어댑터 구현, 클라우드 스토리지 예외 클래스 정의
예외 처리 확장
src/main/java/com/kustacks/kuring/common/exception/InfrastructureException.java, src/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.java, src/main/java/com/kustacks/kuring/common/exception/handler/CommonExceptionHandler.java
새 InfrastructureException, S3/파일 IO 에러 코드 추가, InfrastructureExceptionHandler 메서드 구현
스토리지 구성
src/main/java/com/kustacks/kuring/common/properties/CloudStorageProperties.java, src/main/java/com/kustacks/kuring/config/CloudStorageConfig.java
클라우드 스토리지 프로퍼티 레코드, 프로파일별 S3/OCI 클라이언트 빈 설정(prod/dev)
의존성 및 구성 파일
build.gradle, src/main/resources/config/environments/common.yml, src/test/resources/application-test.yml, .github/workflows/dev.yml
AWS SDK S3, Testcontainers 2.0.2 업그레이드, commons-lang3 추가, 파일 업로드 및 스토리지 구성 설정, Test-Server 배포 환경 추가
공지사항 저장소 개선
src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeCommandPort.java, src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java, src/main/java/com/kustacks/kuring/worker/update/notice/...
배치 저장 시 저장된 엔티티 반환하는 메서드 추가, 공지 업데이터들 반환 값 경로 변경
공지사항 도메인 확장
src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java
fallbackHostPrefix 필드 추가, 호스트 접두사 맵핑 로직 개선
조회 로직 개선
src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java
department 검색 조건 null/blank 안전성 강화
로깅 개선
src/main/java/com/kustacks/kuring/message/application/service/FirebaseNotificationService.java
로그 포맷에 ArticleId 추가
테스트
src/test/java/com/kustacks/kuring/notice/domain/DepartmentNameTest.java, src/test/java/com/kustacks/kuring/storage/adapter/out/S3CompatibleStorageAdapterTest.java
DepartmentName 호스트 접두사 매핑 테스트 확장, S3 호환 스토리지 어댑터 단위 테스트 추가

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Port as StoragePort
    participant Adapter as S3CompatibleStorageAdapter
    participant S3Client as S3Client/S3Presigner
    participant Exception as CloudStorageException

    rect rgba(100, 150, 200, 0.5)
        Note over App,Exception: 파일 업로드 흐름
        App->>Port: upload(stream, key, contentType, length)
        Port->>Adapter: upload(stream, key, contentType, length)
        Adapter->>Adapter: PutObjectRequest 빌드
        Adapter->>S3Client: putObject(request, RequestBody)
        alt 성공
            S3Client-->>Adapter: 응답
            Adapter-->>Port: 완료
            Port-->>App: 반환
        else SdkClientException
            S3Client-->>Adapter: 예외 발생
            Adapter->>Exception: 생성 (STORAGE_S3_SDK_PROBLEM)
            Exception-->>App: 예외 throw
        end
    end

    rect rgba(150, 100, 200, 0.5)
        Note over App,Exception: 사전 서명 URL 생성 흐름
        App->>Port: getPresignedUrl(key)
        Port->>Adapter: getPresignedUrl(key)
        Adapter->>Adapter: GetObjectRequest 빌드
        Adapter->>S3Client: presign(GetObjectPresignRequest, 1시간 만료)
        alt 성공
            S3Client-->>Adapter: PresignedGetObjectRequest
            Adapter->>Adapter: URL 추출
            Adapter-->>Port: URL 반환
            Port-->>App: URL
        else 예외
            S3Client-->>Adapter: 예외 발생
            Adapter->>Exception: 생성 (STORAGE_S3_SDK_PROBLEM)
            Exception-->>App: 예외 throw
        end
    end
Loading

Possibly related PRs

Suggested labels

⭐ Feat

Poem

☁️ 구름 위에 파일을 올려
S3의 바구니에 담고 🐰
사전 서명 URL로 선물 포장,
예외 처리는 꼼꼼히 하고
스토리지의 마법으로 데이터 보호! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 3
❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (15 files):

⚔️ .github/workflows/dev.yml (content)
⚔️ build.gradle (content)
⚔️ src/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.java (content)
⚔️ src/main/java/com/kustacks/kuring/common/exception/handler/CommonExceptionHandler.java (content)
⚔️ src/main/java/com/kustacks/kuring/message/application/service/FirebaseNotificationService.java (content)
⚔️ src/main/java/com/kustacks/kuring/notice/adapter/out/persistence/NoticePersistenceAdapter.java (content)
⚔️ src/main/java/com/kustacks/kuring/notice/application/port/out/NoticeCommandPort.java (content)
⚔️ src/main/java/com/kustacks/kuring/notice/application/service/NoticeQueryService.java (content)
⚔️ src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java (content)
⚔️ src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentGraduationNoticeUpdater.java (content)
⚔️ src/main/java/com/kustacks/kuring/worker/update/notice/DepartmentNoticeUpdater.java (content)
⚔️ src/main/java/com/kustacks/kuring/worker/update/notice/KuisHomepageNoticeUpdater.java (content)
⚔️ src/main/resources/config/environments/common.yml (content)
⚔️ src/test/java/com/kustacks/kuring/notice/domain/DepartmentNameTest.java (content)
⚔️ src/test/resources/application-test.yml (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
Title check ❓ Inconclusive The pull request title "version 2.17.0" is a version number rather than a description of the main changes, making it vague and non-descriptive. Consider using a more descriptive title that summarizes the primary change, such as "Add cloud storage (S3-compatible) support and improve department notice hostPrefix handling" or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch develop
  • Create stacked PR with resolved conflicts
  • Post resolved changes as copyable diffs in a comment

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/com/kustacks/kuring/worker/update/notice/KuisHomepageNoticeUpdater.java (1)

191-196: ⚠️ Potential issue | 🔴 Critical

ConcurrentModificationException 발생 가능 — for-each 루프 내에서 리스트 요소 제거

Line 194에서 newNormalNotices를 for-each로 순회하면서 remove(notice)를 호출하고 있습니다. 이는 ConcurrentModificationException을 발생시킵니다. 이 버그는 Line 182에서 변경된 saveAllCategoryNoticesAndReturn에 전달되는 newNotices 데이터에 직접 영향을 미칩니다.

🐛 Iterator 또는 removeIf를 사용한 수정 제안
-        List<String> changedImportantArticleIds = new ArrayList<>();
-        for (Notice notice : newNormalNotices) {
-            if (Collections.binarySearch(savedImportantArticleIds, notice.getArticleId()) >= 0) { // 정렬되어있다, 이진탐색으로 O(logN)안에 수행
-                changedImportantArticleIds.add(notice.getArticleId());
-                newNormalNotices.remove(notice);
-            }
-        }
+        List<String> changedImportantArticleIds = new ArrayList<>();
+        newNormalNotices.removeIf(notice -> {
+            if (Collections.binarySearch(savedImportantArticleIds, notice.getArticleId()) >= 0) {
+                changedImportantArticleIds.add(notice.getArticleId());
+                return true;
+            }
+            return false;
+        });
🤖 Fix all issues with AI agents
In
`@src/main/java/com/kustacks/kuring/common/exception/handler/CommonExceptionHandler.java`:
- Around line 83-88: The InfrastructureExceptionHandler method can NPE if
exception.getErrorCode().getHttpStatus() is null; update
CommonExceptionHandler.InfrastructureExceptionHandler to defensively handle a
null HttpStatus by resolving a safe fallback (e.g.,
HttpStatus.INTERNAL_SERVER_ERROR) before building the ResponseEntity and log
that a default status was used, and additionally consider enforcing non-null
httpStatus in the InfrastructureException constructor or its factory to prevent
future misuse (reference: InfrastructureException, ErrorCode.getHttpStatus(),
and CommonExceptionHandler.InfrastructureExceptionHandler).

In `@src/main/java/com/kustacks/kuring/config/CloudStorageConfig.java`:
- Around line 47-52: CloudStorageConfig's oracleStorageClient() and
oracleS3Presigner() call URI.create(properties.endpoint()) which can NPE when
properties.endpoint() is null; add a null check (e.g. Objects.requireNonNull or
an explicit if and throw IllegalStateException with a clear message) at the
start of oracleStorageClient() and oracleS3Presigner() (or validate once in the
CloudStorageConfig constructor) to fail fast when properties.endpoint() is
missing for the dev/OCI profile, so you never call URI.create with a null value.

In
`@src/main/java/com/kustacks/kuring/storage/exception/CloudStorageException.java`:
- Around line 6-11: Add a constructor to CloudStorageException that accepts a
cause and forwards it to the super constructor (e.g., public
CloudStorageException(ErrorCode errorCode, Throwable cause) { super(errorCode,
cause); }), and update the S3CompatibleStorageAdapter catch sites that currently
create new CloudStorageException(errorCode) (the catch blocks around the S3 SDK
exceptions) to pass the caught exception (e) into the new constructor so the
original cause is preserved.

In
`@src/test/java/com/kustacks/kuring/storage/adapter/out/S3CompatibleStorageAdapterTest.java`:
- Around line 119-130: The test getPresignedUrlWithS3Exception currently
simulates an SdkClientException but should simulate an S3Exception to exercise
that branch; update the mock for mockS3Presigner.presignGetObject(...) in that
test to throw an S3Exception (constructed via
S3Exception.builder().message(...).build() or equivalent) instead of
SdkClientException so the S3Exception handling path in
S3CompatibleStorageAdapter.getPresignedUrl is validated.
- Around line 145-156: The test method deleteFileWithS3Exception incorrectly
throws SdkClientException instead of an S3Exception; update the mock setup in
S3CompatibleStorageAdapterTest (method deleteFileWithS3Exception) to throw an
S3Exception from mockS3Client.deleteObject(...) so the code path matching S3
errors is exercised (e.g., construct an S3Exception with a message via its
builder and use doThrow(...) on mockS3Client). Ensure the thrown exception type
aligns with the adapter's S3-specific handling so assertThatThrownBy(() ->
s3CompatibleStorageAdapter.delete(fileKey)) still expects CloudStorageException.
- Around line 82-95: The test uploadFileWithS3Exception is currently throwing
SdkClientException instead of S3Exception; change the stub on
mockS3Client.putObject to throw an S3Exception (e.g., build one with
S3Exception.builder().message("S3 error").build()) and add the import for
software.amazon.awssdk.services.s3.model.S3Exception so the S3Exception path in
S3CompatibleStorageAdapter.upload(...) is exercised; keep the existing assertion
that a CloudStorageException is thrown.
🧹 Nitpick comments (8)
src/main/java/com/kustacks/kuring/message/application/service/FirebaseNotificationService.java (1)

106-108: 기존 코드: notificationDtoList.get(0) 빈 리스트 방어 코드 부재

현재 sendNotifications에서 빈 리스트를 걸러주고 있어 실질적 문제는 없지만, loggingNoticeSendInfo가 직접 호출될 경우 IndexOutOfBoundsException이 발생할 수 있습니다. 방어적으로 빈 리스트 체크를 추가하는 것을 고려해 보세요.

🛡️ 방어 코드 제안
 private void loggingNoticeSendInfo(List<NoticeMessageDto> notificationDtoList) {
+    if (notificationDtoList.isEmpty()) {
+        return;
+    }
     log.info("FCM에 {}카테고리에 {}개의 공지 메세지를 전송.",
             notificationDtoList.get(0).getCategory(), notificationDtoList.size());
src/main/java/com/kustacks/kuring/notice/domain/DepartmentName.java (1)

108-118: HOST_PREFIX_MAP의 충돌 병합 전략 (a, b) -> a는 구성 오류를 무시할 수 있습니다.

현재는 hostPrefixfallbackHostPrefix 값이 모두 고유하여 충돌이 발생하지 않지만, 향후 누군가 다른 학과의 hostPrefix와 동일한 fallbackHostPrefix를 추가하면 기존 매핑이 조용히 유지되어 디버깅이 어려운 버그가 발생할 수 있습니다.

충돌 시 예외를 던지도록 변경하면 구성 오류를 빠르게 감지할 수 있습니다.

♻️ 제안 변경
                         .collect(Collectors.toMap(
                                 Map.Entry::getKey, Map.Entry::getValue,
-                                (a, b) -> a
+                                (a, b) -> {
+                                    throw new IllegalStateException(
+                                            "Duplicate HOST_PREFIX_MAP key found: " + a + " vs " + b);
+                                }
                         ))
src/test/java/com/kustacks/kuring/notice/domain/DepartmentNameTest.java (1)

103-124: hostPrefixMap_test는 전체 enum을 순회하며 매핑 정합성을 검증하여 좋습니다.

한 가지 선택적 개선 사항: 현재 테스트는 모든 hostPrefix가 올바르게 매핑되는지 확인하지만, HOST_PREFIX_MAP크기까지 검증하면 중복 키가 무시되었거나 의도치 않은 항목이 추가된 경우도 감지할 수 있습니다. 다만, 이 수준의 검증이 반드시 필요한 것은 아닙니다.

.github/workflows/dev.yml (1)

11-11: 환경 이름 "Test-Server"가 개발 배포 워크플로우와 불일치합니다.

이 워크플로우는 "Deploy to OCI (Develop)"인데, 환경 이름은 Test-Server입니다. 향후 혼동을 방지하기 위해 Dev-Server 등으로 통일하는 것을 고려해 주세요.

src/main/java/com/kustacks/kuring/common/exception/InfrastructureException.java (1)

10-17: super()에 메시지를 전달하지 않아 getMessage()가 null을 반환합니다.

첫 번째 생성자에서 super()에 메시지를 전달하지 않으면, 표준 로깅 프레임워크나 catch 블록에서 exception.getMessage()를 호출할 때 null이 반환됩니다. errorCode의 메시지를 RuntimeException에도 전달하는 것이 좋습니다.

♻️ 제안하는 수정
     public InfrastructureException(ErrorCode errorCode) {
+        super(errorCode.getMessage());
         this.errorCode = errorCode;
     }
 
     public InfrastructureException(ErrorCode errorCode, Exception e) {
-        super(e);
+        super(errorCode.getMessage(), e);
         this.errorCode = errorCode;
     }
src/main/java/com/kustacks/kuring/common/exception/code/ErrorCode.java (1)

110-112: 새 에러 코드가 "DomainLogicException" 섹션 아래에 위치해 있습니다.

STORAGE_S3_SDK_PROBLEMFILE_IO_EXCEPTION은 인프라 수준의 에러 코드인데, DomainLogicException 주석 블록 하단에 추가되어 있습니다. 가독성을 위해 별도의 InfrastructureException 섹션 주석을 추가하거나, 위치를 재조정하는 것을 고려해 주세요.

src/main/java/com/kustacks/kuring/storage/adapter/out/MockStorageAdapter.java (1)

17-20: Mock upload에서 InputStream을 소비하지 않음

Mock이므로 심각한 문제는 아니지만, upload 호출 후 InputStream이 소비되지 않아 실제 환경과 동작이 다릅니다. 통합 테스트에서 리소스 누수 패턴을 놓칠 수 있습니다. 필요 시 inputStream.close() 호출이나 drain 로직을 고려해 보세요.

src/main/java/com/kustacks/kuring/storage/adapter/out/S3CompatibleStorageAdapter.java (1)

46-48: 원본 예외 원인(cause)이 유실됩니다

CloudStorageException을 던질 때 원본 S3Exception/SdkClientException을 cause로 전달하지 않아, 프로덕션에서 S3 관련 장애 디버깅 시 근본 원인 추적이 불가능합니다. 이 패턴이 getPresignedUrl (Line 67-68)과 delete (Line 81-82)에서도 동일하게 반복됩니다.

CloudStorageException에 cause를 받는 생성자를 추가하고, 원본 예외를 전파하는 것을 권장합니다.

♻️ CloudStorageException에 cause 생성자 추가 및 적용

CloudStorageException.java:

public CloudStorageException(ErrorCode errorCode, Throwable cause) {
    super(errorCode, cause);
}

그 후 어댑터에서:

         } catch (S3Exception | SdkClientException e) {
-            throw new CloudStorageException(STORAGE_S3_SDK_PROBLEM);
+            throw new CloudStorageException(STORAGE_S3_SDK_PROBLEM, e);
         }

@github-actions
Copy link

Unit Test Results

  75 files  +  1    75 suites  +1   1m 26s ⏱️ -1s
545 tests +13  538 ✔️ +13  7 💤 ±0  0 ±0 
548 runs  +13  541 ✔️ +13  7 💤 ±0  0 ±0 

Results for commit 8c2e5d6. ± Comparison against base commit cabd73d.

@rlagkswn00 rlagkswn00 merged commit 5bc43a7 into main Feb 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🚀 Deploy Production deploy production server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants