Skip to content

feat: 공통 인프라 구축#6

Open
Soundbar91 wants to merge 4 commits intomainfrom
feature/5-add-common-infrastructure
Open

feat: 공통 인프라 구축#6
Soundbar91 wants to merge 4 commits intomainfrom
feature/5-add-common-infrastructure

Conversation

@Soundbar91
Copy link
Collaborator

@Soundbar91 Soundbar91 commented Mar 4, 2026

관련 이슈

Closes #5


작업 내용

  • 전 도메인에 일관된 응답 형식(ApiResponse<T>) 도입
  • 에러 코드 체계(ErrorCode) 및 BusinessException 기반 예외 처리 구조 확립
  • BaseEntity로 생성/수정 시각 및 softDelete 패턴 공통화
  • Track 도메인에 공통 인프라 적용 및 Validation 메시지 구체화

변경 사항 상세

신규 파일 (common 패키지)

  • ResultType.java — SUCCESS/ERROR enum
  • FieldError.java — 필드 에러 record
  • ErrorMessage.java — 에러 응답 본문 record (code, message, fieldErrors)
  • ApiResponse.java — 통합 응답 래퍼 record (@JsonInclude(NON_NULL))
  • ErrorCode.java — HttpStatus, message, logLevel을 갖는 에러 코드 enum
  • BusinessException.java — ErrorCode 기반 도메인 예외 베이스 클래스
  • BaseEntity.java — id, createdAt, updatedAt, deletedAt + softDelete/restore/isDeleted

수정 파일

  • BcsdInternalApiApplication.java@EnableJpaAuditing 추가
  • GlobalExceptionHandler.java — BusinessException / MethodArgumentNotValidException / Exception 핸들러로 재작성
  • ErrorResponse.java — 삭제 (ApiResponse + ErrorMessage로 대체)
  • Track.java — BaseEntity 상속, @SQLDelete 제거
  • TrackNotFoundException.java — BusinessException 상속, TRACK_NOT_FOUND 에러코드 사용
  • TrackService.java — delete 메서드를 softDelete 방식으로 변경
  • TrackController.java / TrackApi.java — 반환 타입을 ApiResponse<T>로 변경
  • TrackRequest.java — Validation 메시지 구체화
  • V2__add_audit_columns_to_track.sql — track 테이블에 created_at, updated_at 컬럼 추가

참고 사항

  • FieldErrororg.springframework.validation.FieldError와 이름이 충돌하므로 GlobalExceptionHandler에서 FQCN으로 구분
  • ApiResponse의 compact constructor에서 SUCCESS+error, ERROR+data, ERROR+error없음 조합을 모두 금지
  • softDelete는 @SQLDelete 대신 BaseEntity.softDelete() 메서드를 직접 호출하는 방식으로 변경

Summary by CodeRabbit

  • New Features
    • Standardized API response format with consistent success/error structure for all endpoints
    • Enhanced error handling providing descriptive error codes and detailed field-level validation feedback
    • Soft deletion support enabling safer data management with recovery capability
    • Automatic audit timestamps tracking record creation and modification times

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

This PR establishes common infrastructure by introducing standardized API response types (ApiResponse, ResultType), comprehensive error handling (ErrorCode, BusinessException), a reusable BaseEntity with JPA auditing, and rewrites GlobalExceptionHandler for unified error responses. The Track domain is refactored to use this new framework with soft-delete support.

Changes

Cohort / File(s) Summary
Common Infrastructure - Response & Error Types
common/ApiResponse.java, common/ResultType.java, common/FieldError.java, common/ErrorMessage.java
New record types and enum to standardize API responses and error metadata with validation constraints and factory methods.
Common Infrastructure - Exception Framework
common/ErrorCode.java, common/BusinessException.java
New enum and exception class for centralized error code management with HTTP status, messages, and log levels; runtime exception as base for domain-specific exceptions.
Common Infrastructure - Base Classes
BcsdInternalApiApplication.java, common/BaseEntity.java
Enables JPA auditing and introduces abstract superclass with auto-generated ID, audit timestamps (createdAt, updatedAt), and soft-delete support (deletedAt, softDelete(), restore(), isDeleted()).
Exception Handling Updates
common/GlobalExceptionHandler.java, common/ErrorResponse.java
Complete rewrite of exception handler to support BusinessException, MethodArgumentNotValidException, and general exceptions; introduces logging via ErrorCode log levels; removes legacy ErrorResponse record.
Track Domain - Legacy Cleanup
track/Track.java, track/TrackApi.java, track/TrackController.java, track/TrackService.java, track/TrackRepository.java, track/TrackNotFoundException.java
Removed old implementations replaced by refactored versions in controller and model subdirectories with new common infrastructure.
Track Domain - Model & Exception
track/model/Track.java, track/exception/TrackNotFoundException.java
Track now extends BaseEntity (removes explicit id and deletedAt fields); TrackNotFoundException now extends BusinessException with ErrorCode.TRACK_NOT_FOUND.
Track Domain - API Layer
track/controller/TrackApi.java, track/controller/TrackController.java
All endpoint return types wrapped in ApiResponse; delete endpoint returns ApiResponse with NO_CONTENT; added ApiResponse imports.
Track Domain - DTO & Service
track/dto/TrackRequest.java, track/service/TrackService.java
Added custom validation messages to TrackRequest (NotBlank, Size, NotNull with messages); delete operation switched from hard delete to track.softDelete() with repository.save().
Database & Tests
db/migration/V2__add_audit_columns_to_track.sql, track/service/TrackServiceTest.java
Flyway migration adds created_at and updated_at audit columns to track table; test updated to verify save() invocation and isDeleted() assertion for soft delete behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A rabbit hops through the code with glee,
Building foundations strong and free,
ApiResponse wraps responses with care,
ErrorCode's symphony floats through the air,
BaseEntity grows roots down deep,
While soft-deleted dreams the data shall keep! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

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.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: 공통 인프라 구축' directly reflects the main change—introducing common infrastructure across the codebase—and is concise and clear.
Description check ✅ Passed The PR description fully addresses the template structure with 관련 이슈, 작업 내용, 변경 사항 상세, and 참고 사항 sections, detailing all new files, modifications, and important implementation notes.
Linked Issues check ✅ Passed All objectives from issue #5 are met: common infrastructure classes created, BaseEntity with auditing implemented, GlobalExceptionHandler rewritten, Track domain updated, and tests adjusted for softDelete behavior.
Out of Scope Changes check ✅ Passed All changes are scoped to the common infrastructure objectives. The removal of old Track-related files and additions of common classes, auditing support, and Track refactoring directly support the issue requirements.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/5-add-common-infrastructure

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/bcsd/internal/common/ApiResponse.java`:
- Around line 8-18: The canonical constructor for ApiResponse must validate that
result is not null; update the ApiResponse canonical constructor (the public
ApiResponse(...) compact constructor) to first check if result == null and throw
an IllegalArgumentException (or Objects.requireNonNull) with a clear message,
then keep the existing checks for ResultType.SUCCESS/ERROR against error and
data; reference the ResultType enum and the constructor parameters result, data,
and error when adding the null guard so new ApiResponse<>(null, null, null) is
rejected.

In `@src/main/java/com/bcsd/internal/common/GlobalExceptionHandler.java`:
- Around line 26-37: Split the long
handleValidation(MethodArgumentNotValidException) into a short orchestrator and
small private helpers: keep handleValidation to call three new helpers —
extractFieldErrors(MethodArgumentNotValidException) to return List<FieldError>
(perform the stream map there), buildErrorMessage(List<FieldError>) to construct
and return the ErrorMessage using ErrorCode.INVALID_INPUT_VALUE, and
buildErrorResponse(ErrorMessage) to assemble and return the
ResponseEntity<ApiResponse<Void>> with the proper HTTP status; make all helpers
private and keep each helper under 10 lines so handleValidation only delegates
to them.
- Around line 46-51: The log(ErrorCode errorCode, Exception e) method currently
uses a switch on errorCode.getLogLevel(); replace that switch with early-return
if branches: first check if errorCode.getLogLevel() == ERROR then call
log.error("BusinessException: {}", e.getMessage(), e) and return; next check for
WARN then call log.warn("BusinessException: {}", e.getMessage()) and return;
otherwise call log.info("BusinessException: {}", e.getMessage()). Keep the same
logging calls and message text (log.error, log.warn, log.info) and ensure the
method returns immediately after each matched branch.

In `@src/main/java/com/bcsd/internal/track/controller/TrackController.java`:
- Around line 43-46: The delete method in TrackController currently returns
ApiResponse.success() with HttpStatus.NO_CONTENT which violates 204 semantics;
change the response to return a 200 OK with the unified wrapper instead—update
TrackController.delete to use ResponseEntity.ok(ApiResponse.success()) (or
ResponseEntity.status(HttpStatus.OK).body(ApiResponse.success())), keeping the
call to trackService.delete(id) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d0be88dc-b561-4bf1-be29-742920676320

📥 Commits

Reviewing files that changed from the base of the PR and between 75049cd and 0392f35.

📒 Files selected for processing (24)
  • src/main/java/com/bcsd/internal/BcsdInternalApiApplication.java
  • src/main/java/com/bcsd/internal/common/ApiResponse.java
  • src/main/java/com/bcsd/internal/common/BaseEntity.java
  • src/main/java/com/bcsd/internal/common/BusinessException.java
  • src/main/java/com/bcsd/internal/common/ErrorCode.java
  • src/main/java/com/bcsd/internal/common/ErrorMessage.java
  • src/main/java/com/bcsd/internal/common/ErrorResponse.java
  • src/main/java/com/bcsd/internal/common/FieldError.java
  • src/main/java/com/bcsd/internal/common/GlobalExceptionHandler.java
  • src/main/java/com/bcsd/internal/common/ResultType.java
  • src/main/java/com/bcsd/internal/track/Track.java
  • src/main/java/com/bcsd/internal/track/TrackApi.java
  • src/main/java/com/bcsd/internal/track/TrackController.java
  • src/main/java/com/bcsd/internal/track/TrackNotFoundException.java
  • src/main/java/com/bcsd/internal/track/TrackRepository.java
  • src/main/java/com/bcsd/internal/track/TrackService.java
  • src/main/java/com/bcsd/internal/track/controller/TrackApi.java
  • src/main/java/com/bcsd/internal/track/controller/TrackController.java
  • src/main/java/com/bcsd/internal/track/dto/TrackRequest.java
  • src/main/java/com/bcsd/internal/track/exception/TrackNotFoundException.java
  • src/main/java/com/bcsd/internal/track/model/Track.java
  • src/main/java/com/bcsd/internal/track/service/TrackService.java
  • src/main/resources/db/migration/V2__add_audit_columns_to_track.sql
  • src/test/java/com/bcsd/internal/track/service/TrackServiceTest.java
💤 Files with no reviewable changes (7)
  • src/main/java/com/bcsd/internal/track/TrackController.java
  • src/main/java/com/bcsd/internal/track/TrackService.java
  • src/main/java/com/bcsd/internal/track/TrackNotFoundException.java
  • src/main/java/com/bcsd/internal/track/TrackRepository.java
  • src/main/java/com/bcsd/internal/track/TrackApi.java
  • src/main/java/com/bcsd/internal/track/Track.java
  • src/main/java/com/bcsd/internal/common/ErrorResponse.java

Comment on lines +8 to +18
public ApiResponse {
if (result == ResultType.SUCCESS && error != null) {
throw new IllegalArgumentException("SUCCESS response must not have error");
}
if (result == ResultType.ERROR && data != null) {
throw new IllegalArgumentException("ERROR response must not have data");
}
if (result == ResultType.ERROR && error == null) {
throw new IllegalArgumentException("ERROR response must have error");
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against null for result in the canonical constructor.

new ApiResponse<>(null, null, null) currently passes and creates an invalid response shape.

💡 Proposed fix
 public ApiResponse {
+    if (result == null) {
+        throw new IllegalArgumentException("result must not be null");
+    }
     if (result == ResultType.SUCCESS && error != null) {
         throw new IllegalArgumentException("SUCCESS response must not have error");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/bcsd/internal/common/ApiResponse.java` around lines 8 - 18,
The canonical constructor for ApiResponse must validate that result is not null;
update the ApiResponse canonical constructor (the public ApiResponse(...)
compact constructor) to first check if result == null and throw an
IllegalArgumentException (or Objects.requireNonNull) with a clear message, then
keep the existing checks for ResultType.SUCCESS/ERROR against error and data;
reference the ResultType enum and the constructor parameters result, data, and
error when adding the null guard so new ApiResponse<>(null, null, null) is
rejected.

Comment on lines +26 to +37
public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors().stream()
.map(fe -> new FieldError(fe.getField(), fe.getDefaultMessage()))
.toList();
ErrorMessage errorMessage = ErrorMessage.of(
ErrorCode.INVALID_INPUT_VALUE.name(),
ErrorCode.INVALID_INPUT_VALUE.getMessage(),
fieldErrors
);
return ResponseEntity.status(ErrorCode.INVALID_INPUT_VALUE.getHttpStatus())
.body(ApiResponse.error(errorMessage));
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if file exists and extract the method at lines 26-37
if [ -f "src/main/java/com/bcsd/internal/common/GlobalExceptionHandler.java" ]; then
  echo "=== Lines 26-37 with line numbers ==="
  sed -n '26,37p' src/main/java/com/bcsd/internal/common/GlobalExceptionHandler.java | cat -n
  echo ""
  echo "=== Line count for lines 26-37 ==="
  sed -n '26,37p' src/main/java/com/bcsd/internal/common/GlobalExceptionHandler.java | wc -l
  echo ""
  echo "=== Context: lines 20-40 ==="
  sed -n '20,40p' src/main/java/com/bcsd/internal/common/GlobalExceptionHandler.java | cat -n
else
  echo "File not found"
fi

Repository: BCSDLab/BCSD_INTERNAL_API_V2

Length of output: 2052


Extract helper methods to comply with the 10-line method-length rule.

The handleValidation() method spans 12 lines and performs multiple tasks (field error extraction, error message creation, response assembly). Refactor into separate helper methods to keep each under 10 lines and satisfy the single-responsibility principle outlined in coding guidelines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/bcsd/internal/common/GlobalExceptionHandler.java` around
lines 26 - 37, Split the long handleValidation(MethodArgumentNotValidException)
into a short orchestrator and small private helpers: keep handleValidation to
call three new helpers — extractFieldErrors(MethodArgumentNotValidException) to
return List<FieldError> (perform the stream map there),
buildErrorMessage(List<FieldError>) to construct and return the ErrorMessage
using ErrorCode.INVALID_INPUT_VALUE, and buildErrorResponse(ErrorMessage) to
assemble and return the ResponseEntity<ApiResponse<Void>> with the proper HTTP
status; make all helpers private and keep each helper under 10 lines so
handleValidation only delegates to them.

Comment on lines +46 to +51
private void log(ErrorCode errorCode, Exception e) {
switch (errorCode.getLogLevel()) {
case ERROR -> log.error("BusinessException: {}", e.getMessage(), e);
case WARN -> log.warn("BusinessException: {}", e.getMessage());
default -> log.info("BusinessException: {}", e.getMessage());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "GlobalExceptionHandler.java" -type f

Repository: BCSDLab/BCSD_INTERNAL_API_V2

Length of output: 140


🏁 Script executed:

cat -n ./src/main/java/com/bcsd/internal/common/GlobalExceptionHandler.java | sed -n '40,60p'

Repository: BCSDLab/BCSD_INTERNAL_API_V2

Length of output: 857


🏁 Script executed:

find . -name "ErrorCode.java" -type f

Repository: BCSDLab/BCSD_INTERNAL_API_V2

Length of output: 127


🏁 Script executed:

cat -n ./src/main/java/com/bcsd/internal/common/ErrorCode.java

Repository: BCSDLab/BCSD_INTERNAL_API_V2

Length of output: 1179


Replace switch/case in log(...) with early-return if branches.

Lines 46-51 violate the Java guideline: "Do not use else or switch/case statements. Use early return (if + return) instead."

Proposed refactor
-    private void log(ErrorCode errorCode, Exception e) {
-        switch (errorCode.getLogLevel()) {
-            case ERROR -> log.error("BusinessException: {}", e.getMessage(), e);
-            case WARN -> log.warn("BusinessException: {}", e.getMessage());
-            default -> log.info("BusinessException: {}", e.getMessage());
-        }
-    }
+    private void log(ErrorCode errorCode, Exception exception) {
+        if (errorCode.getLogLevel() == org.slf4j.event.Level.ERROR) {
+            log.error("BusinessException: {}", exception.getMessage(), exception);
+            return;
+        }
+        if (errorCode.getLogLevel() == org.slf4j.event.Level.WARN) {
+            log.warn("BusinessException: {}", exception.getMessage());
+            return;
+        }
+        log.info("BusinessException: {}", exception.getMessage());
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/bcsd/internal/common/GlobalExceptionHandler.java` around
lines 46 - 51, The log(ErrorCode errorCode, Exception e) method currently uses a
switch on errorCode.getLogLevel(); replace that switch with early-return if
branches: first check if errorCode.getLogLevel() == ERROR then call
log.error("BusinessException: {}", e.getMessage(), e) and return; next check for
WARN then call log.warn("BusinessException: {}", e.getMessage()) and return;
otherwise call log.info("BusinessException: {}", e.getMessage()). Keep the same
logging calls and message text (log.error, log.warn, log.info) and ensure the
method returns immediately after each matched branch.

Comment on lines +43 to 46
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
trackService.delete(id);
return ResponseEntity.noContent().build();
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(ApiResponse.success());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Do not return a body with HTTP 204.

Line 45 sends ApiResponse.success() with NO_CONTENT; 204 responses should have no body. Prefer 200 OK with wrapper to keep the unified response contract.

💡 Proposed fix
-        return ResponseEntity.status(HttpStatus.NO_CONTENT).body(ApiResponse.success());
+        return ResponseEntity.ok(ApiResponse.success());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ResponseEntity<ApiResponse<Void>> delete(@PathVariable Long id) {
trackService.delete(id);
return ResponseEntity.noContent().build();
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(ApiResponse.success());
}
public ResponseEntity<ApiResponse<Void>> delete(`@PathVariable` Long id) {
trackService.delete(id);
return ResponseEntity.ok(ApiResponse.success());
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/bcsd/internal/track/controller/TrackController.java` around
lines 43 - 46, The delete method in TrackController currently returns
ApiResponse.success() with HttpStatus.NO_CONTENT which violates 204 semantics;
change the response to return a 200 OK with the unified wrapper instead—update
TrackController.delete to use ResponseEntity.ok(ApiResponse.success()) (or
ResponseEntity.status(HttpStatus.OK).body(ApiResponse.success())), keeping the
call to trackService.delete(id) unchanged.

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.

[task] 공통 인프라 구축

1 participant