Skip to content

Conversation

@NCookies
Copy link
Collaborator

@NCookies NCookies commented Dec 29, 2025

작업 내용

  • 사용자가 문제 풀이 중 작성한 코드를 임시로 저장하는 자동 저장(Draft) 기능을 구현했습니다.
  • 채점 이력(Submission)과 분리된 별도의 도메인(Draft)을 설계하여 데이터 성격을 명확히 했습니다.
  • 다중 탭 환경에서의 덮어쓰기 방지를 위한 낙관적 락(Optimistic Lock) 기반 동시성 제어 로직을 적용했습니다.

변경 사항

  • Domain Layer

    • Draft 엔티티 생성
      • User + Problem + Language 3가지 조합의 Composite Unique Constraint 적용 (언어별 저장 지원)
      • @Version 필드 추가 (JPA 낙관적 락 적용)
    • DraftRepository 추가: findByUserAndProblemAndLanguage 조회 메서드 구현
  • Service Layer

    • DraftService, DraftDomainService 구현
    • Fail-Fast 전략 적용: DB 저장 전 요청 버전과 현재 버전을 비교하여 불필요한 쿼리 방지
    • saveAndFlush를 사용하여 갱신된 버전 정보를 즉시 반환하도록 구현
    • 조회 시 데이터가 없으면 404 에러 대신 빈 문자열("")version: null을 반환하여 프론트엔드 처리 간소화
  • Controller Layer

    • POST /api/v1/drafts: 자동 저장 API (Upsert)
    • ObjectOptimisticLockingFailureException 발생 시 409 Conflict 예외 코드로 매핑하여 반환
  • Configuration

    • SecurityConfig: 로컬 테스트 환경을 위한 CORS 설정 추가 (AllowedOriginPatterns 적용)

트러블 슈팅

  • 문제 1: 동시 수정 시 데이터 덮어쓰기 문제

    • 상황: 여러 탭을 띄워두고 번갈아 수정할 경우, 이전 탭의 내용으로 최신 코드가 덮어씌워지는 현상 발생
    • 해결: JPA의 @Version을 이용한 **낙관적 락(Optimistic Lock)**을 도입. 버전 불일치 시 409 Conflict를 반환하고, 클라이언트에서 "불러오기 vs 덮어쓰기"를 선택하도록 유도함.
  • 문제 2: 초기 진입 시 404 예외 처리의 모호함

    • 상황: 사용자가 문제를 처음 풀러 들어왔을 때 저장된 Draft가 없어 EntityNotFoundException이 발생, 프론트엔드에서 이를 에러로 처리해야 하는 불편함 존재
    • 해결: 예외를 던지는 대신 **빈 객체(Empty Object)**를 200 OK로 반환하도록 변경하여 "정상적인 초기 상태"임을 명확히 함.

참고 사항

  • 기술적 의사결정 (HTTP vs WebSocket)
    • 이미 프로젝트에 WebSocket(ActiveMQ)이 존재하나, 코드 텍스트 데이터의 페이로드 크기와 t3.small의 메모리 제약을 고려하여 HTTP Polling + Debouncing 방식을 채택함.
    • 데이터 정합성이 실시간성보다 중요하다고 판단하여 트랜잭션 관리가 용이한 REST API 방식을 선택함.

코드 리뷰 전 확인 체크리스트

  • 불필요한 콘솔 로그, 주석 제거
  • 커밋 메시지 컨벤션 준수 (type : )
  • 기능 정상 동작 확인

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 코드 임시 저장 기능 추가
    • 저장된 코드 조회 API 추가
    • 여러 탭에서의 동시 수정 시 버전 충돌 감지 및 안내
    • 문제별 프로그래밍 언어별 임시 저장 지원

✏️ Tip: You can customize this high-level summary in your review settings.

- 도메인 분리: Submission과 별도로 Draft 도메인 패키지 신설
- DB 설계: User+Problem+Language 복합 유니크 키 및 Version 컬럼 추가
- 동시성 제어:
  - JPA Optimistic Locking(@Version) 적용
  - 요청 버전과 DB 버전을 비교하는 명시적 검증 로직 구현
  - 충돌 시 409 Conflict 예외 처리
- API 구현:
  - POST /api/drafts (자동 저장/업데이트)
  - GET /api/drafts (임시 저장 내역 조회)
- 기존에 저장된 값이 없다면 null 값을 리턴하도록 함
@NCookies NCookies self-assigned this Dec 29, 2025
@NCookies NCookies added the enhancement New feature or request label Dec 29, 2025
@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

Walkthrough

새로운 Draft(임시 저장) 기능을 구현합니다. 사용자는 코드를 임시로 저장하고 조회할 수 있으며, 낙관적 잠금을 통한 동시성 제어와 접근 권한 검증을 포함합니다.

Changes

Cohort / File(s) 변경 사항
요청/응답 DTO
src/main/java/org/ezcode/codetest/application/draft/dto/request/DraftSaveRequest.java, src/main/java/org/ezcode/codetest/application/draft/dto/response/DraftResponse.java
새로운 record 기반 DTO 추가: DraftSaveRequest는 problemId, languageId, code(최대 100KB), version 필드를 포함하며 유효성 검사 어노테이션 적용. DraftResponse는 Draft 엔티티를 응답으로 변환하는 매퍼 메서드 포함
예외 처리
src/main/java/org/ezcode/codetest/domain/draft/exception/DraftException.java, src/main/java/org/ezcode/codetest/domain/draft/exception/DraftExceptionCode.java
새로운 DraftException 클래스와 DraftExceptionCode enum 추가. 세 가지 에러 코드 정의: DRAFT_NOT_FOUND(404), DRAFT_VERSION_CONFLICT(409), DRAFT_ACCESS_DENIED(403)
도메인 엔티티 및 리포지토리
src/main/java/org/ezcode/codetest/domain/draft/model/entity/Draft.java, src/main/java/org/ezcode/codetest/domain/draft/repository/DraftRepository.java
JPA 엔티티 Draft 추가(사용자-문제-언어 복합 유니크 제약, 낙관적 잠금 버전 필드). 도메인 계층 DraftRepository 인터페이스 정의
도메인 서비스
src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java
autoSave() 메서드로 버전 충돌 감지 및 자동 업데이트 로직 구현. getDraft() 메서드로 Draft 조회
인프라 리포지토리
src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/draft/DraftJpaRepository.java, src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/draft/DraftRepositoryImpl.java
Spring Data JPA 기반 DraftJpaRepository 추가 및 DraftRepositoryImpl로 도메인 인터페이스 구현
애플리케이션 서비스 및 컨트롤러
src/main/java/org/ezcode/codetest/application/draft/service/DraftService.java, src/main/java/org/ezcode/codetest/presentation/draft/DraftController.java
DraftService는 @Transactional 경계 관리 및 ObjectOptimisticLockingFailureException 변환. DraftController는 POST /api/drafts(저장), GET /api/drafts(조회) 엔드포인트 제공

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Controller as DraftController
    participant Service as DraftService
    participant DomainService as DraftDomainService
    participant Repository as DraftRepository
    participant Database as Database

    rect rgba(100, 200, 100, 0.2)
    Note over Client,Database: 임시 저장 (autoSave) 흐름
    Client->>Controller: POST /api/drafts<br/>(problemId, languageId, code, version)
    Controller->>Service: autoSave(userId, request)
    Service->>Service: 사용자, 문제, 언어 엔티티 조회
    Service->>DomainService: autoSave(user, problem, language,<br/>code, version)
    DomainService->>Repository: findByUserAndProblemAndLanguage()
    Repository->>Database: 기존 Draft 조회
    alt Draft 존재
        Database-->>Repository: Draft 반환
        Repository-->>DomainService: Draft
        DomainService->>DomainService: 버전 확인<br/>(제공된 version == stored version?)
        alt 버전 일치
            DomainService->>DomainService: updateCode(newCode)
            DomainService->>Repository: saveAndFlush(draft)
            Repository->>Database: UPDATE draft
        else 버전 불일치
            DomainService-->>Service: DraftException<br/>(DRAFT_VERSION_CONFLICT)
            Service-->>Controller: 409 Conflict
        end
    else Draft 미존재
        DomainService->>Repository: save(new Draft)
        Repository->>Database: INSERT draft
    end
    Database-->>Repository: Draft (저장됨)
    Repository-->>DomainService: Draft
    DomainService-->>Service: Draft
    Service->>Service: DraftResponse.from(draft)
    Service-->>Controller: DraftResponse
    Controller-->>Client: 200 OK + DraftResponse
    end

    rect rgba(100, 150, 200, 0.2)
    Note over Client,Database: 임시 저장 조회 (getDraft) 흐름
    Client->>Controller: GET /api/drafts<br/>(problemId, languageId)
    Controller->>Service: getDraft(userId, problemId, languageId)
    Service->>Service: 사용자, 문제, 언어 엔티티 조회
    Service->>DomainService: getDraft(user, problem, language)
    DomainService->>Repository: findByUserAndProblemAndLanguage()
    Repository->>Database: 조회
    Database-->>Repository: Optional<Draft>
    Repository-->>DomainService: Optional<Draft>
    DomainService-->>Service: Optional<Draft>
    Service->>Service: 존재 여부 확인
    alt Draft 존재
        Service->>Service: DraftResponse.from(draft)
        Service-->>Controller: Optional(DraftResponse)
    else Draft 미존재
        Service-->>Controller: Optional.empty()
    end
    Controller-->>Client: 200 OK<br/>(DraftResponse 또는 null)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • thezz9
  • Kimminu7
  • chat26666
  • minjee2758
  • pokerbearkr

Poem

🐰 깡충깡충 새로운 Draft 기능이 생겼어요!
코드를 임시로 저장하고, 버전 충돌도 척척 처리하고,
낙관적 잠금으로 안전하게 관리하는 마법! ✨
계층별로 깔끔하게 정렬된 구조가 정말 멋져요! 🎉

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 변경사항의 핵심을 명확하게 전달합니다. '코딩 테스트 코드 자동 저장(Draft) 기능 및 동시성 제어 구현'은 추가된 주요 기능(Draft 자동 저장과 낙관적 락 기반 동시성 제어)을 정확하게 요약합니다.
✨ 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 feature/draft

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

🧹 Nitpick comments (4)
src/main/java/org/ezcode/codetest/domain/draft/model/entity/Draft.java (2)

64-71: Builder에서 version 파라미터 노출을 제거하는 것을 권장합니다.

Builder를 통해 version을 수동으로 설정할 수 있는데, 이는 JPA의 @Version 자동 관리 메커니즘과 충돌할 수 있습니다. version은 JPA가 관리해야 하므로 생성자에서 제외하는 것이 안전합니다.

🔎 권장 수정안
 @Builder
-public Draft(User user, Problem problem, Language language, String code, Long version) {
+public Draft(User user, Problem problem, Language language, String code) {
     this.user = user;
     this.problem = problem;
     this.language = language;
     this.code = code;
-    this.version = version;
 }

60-62: 코드 검증 로직 추가를 고려하세요.

updateCode 메서드가 단순 세터로 구현되어 있습니다. 코드 길이 검증(예: 100,000자 제한)이나 null 체크를 추가하면 도메인 무결성이 향상됩니다.

🔎 제안 수정안
 public void updateCode(String newCode) {
+    if (newCode != null && newCode.length() > 100000) {
+        throw new IllegalArgumentException("코드는 최대 100,000자까지 입력 가능합니다.");
+    }
     this.code = newCode;
 }
src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java (1)

24-51: 수동 버전 검증과 JPA 낙관적 락의 중복 전략을 명확히 하세요.

Line 32의 수동 버전 체크는 "fail-fast" 접근법이지만, Line 26과 Line 37 사이에 발생하는 실제 race condition은 방지하지 못합니다. JPA의 @Version만이 flush 시점에 동시 수정을 감지할 수 있습니다.

현재 구현:

  • 수동 체크: 클라이언트가 오래된 버전으로 요청 시 즉시 실패 (fail-fast)
  • JPA 체크: 실제 동시 수정 시 ObjectOptimisticLockingFailureException 발생

이 이중 검증 전략이 의도된 것이라면 주석으로 명확히 문서화하거나, 불필요하다면 수동 체크를 제거하고 JPA 낙관적 락만 사용하는 것을 권장합니다.

🔎 제안 1: 전략 문서화
 if (existingDraftOpt.isPresent()) {
     Draft draft = existingDraftOpt.get();
 
+    // Fail-fast: 클라이언트가 명백히 오래된 버전을 보낸 경우 즉시 거부
+    // (실제 동시성 제어는 JPA @Version이 flush 시점에 처리)
     if (version != null && !Objects.equals(version, draft.getVersion())) {
         throw new DraftException(DraftExceptionCode.DRAFT_VERSION_CONFLICT);
     }
🔎 제안 2: 수동 체크 제거 (JPA만 사용)
 if (existingDraftOpt.isPresent()) {
     Draft draft = existingDraftOpt.get();
 
-    // 명시적 버전 검증: 클라이언트가 보낸 version과 DB의 version이 일치해야 함
-    if (version != null && !draft.getVersion().equals(version)) {
-        throw new DraftException(DraftExceptionCode.DRAFT_VERSION_CONFLICT);
-    }
-
     draft.updateCode(code);
     draftRepository.saveAndFlush(draft);
+    // JPA @Version이 동시 수정을 감지하여 ObjectOptimisticLockingFailureException 발생
src/main/java/org/ezcode/codetest/application/draft/servcie/DraftService.java (1)

43-53: 이중 버전 충돌 검증 메커니즘 명확히 하기

현재 버전 충돌은 두 계층에서 감지됩니다:

  1. DraftDomainService의 명시적 버전 체크 (라인 31-34): 요청 버전과 DB 버전 비교 후 즉시 DraftException 발생
  2. DraftServiceObjectOptimisticLockingFailureException 처리 (라인 51-52): JPA 낙관적 락 예외 catch

이 이중 방어 전략은 TOCTOU(Time-of-Check to Time-of-Use) 시나리오를 방어합니다. 명시적 체크(라인 32)와 실제 저장(라인 37) 사이에 다른 스레드가 Draft를 수정할 수 있기 때문에, JPA의 @Version 낙관적 락은 필수적인 안전망입니다.

이 의도를 팀과 공유하기 위해 catch 블록에 주석을 추가하는 것을 권장합니다:

 		} catch (ObjectOptimisticLockingFailureException e) {
+			// DraftDomainService의 명시적 체크 후에도 발생 가능한 TOCTOU 시나리오 방어
 			throw new DraftException(DraftExceptionCode.DRAFT_VERSION_CONFLICT);
 		}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d632c6 and fd565af.

📒 Files selected for processing (11)
  • src/main/java/org/ezcode/codetest/application/draft/dto/request/DraftSaveRequest.java
  • src/main/java/org/ezcode/codetest/application/draft/dto/response/DraftResponse.java
  • src/main/java/org/ezcode/codetest/application/draft/servcie/DraftService.java
  • src/main/java/org/ezcode/codetest/domain/draft/exception/DraftException.java
  • src/main/java/org/ezcode/codetest/domain/draft/exception/DraftExceptionCode.java
  • src/main/java/org/ezcode/codetest/domain/draft/model/entity/Draft.java
  • src/main/java/org/ezcode/codetest/domain/draft/repository/DraftRepository.java
  • src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/draft/DraftJpaRepository.java
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/draft/DraftRepositoryImpl.java
  • src/main/java/org/ezcode/codetest/presentation/draft/DraftController.java
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-15T04:37:29.231Z
Learnt from: chat26666
Repo: ezcode-my/backend PR: 64
File: src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/EncounterChoiceRepositoryImpl.java:0-0
Timestamp: 2025-06-15T04:37:29.231Z
Learning: EncounterChoiceRepositoryImpl in src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/game/EncounterChoiceRepositoryImpl.java is intentionally a skeleton implementation that is work-in-progress and will be completed later.

Applied to files:

  • src/main/java/org/ezcode/codetest/domain/draft/repository/DraftRepository.java
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/draft/DraftJpaRepository.java
  • src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/draft/DraftRepositoryImpl.java
📚 Learning: 2025-07-02T12:05:54.917Z
Learnt from: Kimminu7
Repo: ezcode-my/backend PR: 133
File: src/test/java/org/ezcode/codetest/domain/problem/service/ProblemDomainServiceTest.java:92-99
Timestamp: 2025-07-02T12:05:54.917Z
Learning: ProblemDomainService의 removeProblem 메서드는 DB에서 Problem을 삭제한 후 Elasticsearch에서도 해당 ProblemSearchDocument를 찾아서 삭제합니다. 만약 Elasticsearch에서 문서를 찾지 못하면 ProblemException(PROBLEM_NOT_FOUND)을 던지므로, 테스트에서는 problem.getId()와 searchRepository.findById() 모두 적절하게 mock해야 합니다.

Applied to files:

  • src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java
🧬 Code graph analysis (4)
src/main/java/org/ezcode/codetest/application/draft/servcie/DraftService.java (1)
src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java (1)
  • Slf4j (17-57)
src/main/java/org/ezcode/codetest/domain/draft/exception/DraftExceptionCode.java (1)
src/main/java/org/ezcode/codetest/domain/draft/exception/DraftException.java (1)
  • Getter (9-21)
src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java (1)
src/main/java/org/ezcode/codetest/application/draft/servcie/DraftService.java (1)
  • Slf4j (24-68)
src/main/java/org/ezcode/codetest/domain/draft/exception/DraftException.java (1)
src/main/java/org/ezcode/codetest/common/base/exception/BaseException.java (1)
  • BaseException (5-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (13)
src/main/java/org/ezcode/codetest/domain/draft/exception/DraftException.java (1)

10-21: LGTM! 표준 예외 패턴을 올바르게 구현했습니다.

BaseException을 확장하여 ResponseCode 기반의 일관된 예외 처리를 지원합니다. 구조가 명확하고 구현이 정확합니다.

src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/draft/DraftJpaRepository.java (1)

11-14: LGTM! Spring Data JPA 표준 패턴을 따릅니다.

메서드 명명 규칙에 따라 복합 키 조회를 위한 쿼리가 자동 생성됩니다. 구현이 올바릅니다.

src/main/java/org/ezcode/codetest/application/draft/dto/response/DraftResponse.java (1)

7-22: LGTM! 명확한 DTO 매핑 구조입니다.

from(Draft) 정적 팩토리 메서드가 연관 엔티티에서 ID를 추출하여 응답 DTO를 구성합니다. 구현이 정확합니다.

src/main/java/org/ezcode/codetest/domain/draft/repository/DraftRepository.java (1)

10-17: LGTM! 도메인 레이어 인터페이스가 명확합니다.

saveAndFlush 메서드가 노출되어 있어 낙관적 락의 버전을 즉시 반환할 수 있습니다(PR 목표에 부합).

src/main/java/org/ezcode/codetest/domain/draft/model/entity/Draft.java (2)

23-36: LGTM! 엔티티 구조가 올바릅니다.

복합 유니크 제약 조건이 적절히 설정되어 사용자-문제-언어별 단일 Draft를 보장합니다. LAZY 로딩 전략도 성능상 적절합니다.


57-58: LGTM! 낙관적 락을 위한 @Version 필드가 올바르게 설정되었습니다.

JPA가 자동으로 버전을 관리하여 동시성 제어를 제공합니다.

src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java (1)

53-56: LGTM! 조회 메서드가 명확합니다.

저장된 Draft를 조회하는 로직이 간결하고 정확합니다.

src/main/java/org/ezcode/codetest/presentation/draft/DraftController.java (1)

33-49: LGTM! POST 엔드포인트가 올바르게 구현되었습니다.

@Valid 검증과 인증 처리가 적절하며, Swagger 문서화도 명확합니다. 버전 충돌 시 409 응답이 반환됩니다(DraftService에서 DraftException 발생).

src/main/java/org/ezcode/codetest/application/draft/dto/request/DraftSaveRequest.java (1)

6-18: 필드 검증이 적절하게 설계되었습니다.

version 필드가 새 엔티티의 경우 null을 허용하도록 설정되어 있으며, 코드 크기 제한(100,000자)이 올바르게 적용되었습니다. Draft 엔티티의 code 필드에 @lob 어노테이션이 적용되어 있고, MySQL의 LONGTEXT 타입은 4GB 이상의 용량을 지원하므로 100,000자 제한을 충분히 수용할 수 있습니다.

src/main/java/org/ezcode/codetest/domain/draft/exception/DraftExceptionCode.java (2)

16-25: LGTM!

낙관적 락 충돌 처리를 위한 DRAFT_VERSION_CONFLICT(409)와 권한 검증을 위한 DRAFT_ACCESS_DENIED(403)가 적절하게 정의되어 있습니다. 사용자 친화적인 한국어 메시지도 잘 작성되었습니다.


13-13: DRAFT_NOT_FOUND 예외 코드는 사용되지 않고 있습니다.

DRAFT_NOT_FOUND는 정의되어 있지만 코드베이스에서 전혀 사용되지 않고 있습니다. DraftDomainService.getDraft()Optional<Draft>을 반환하고, DraftController.getDraft()에서는 데이터가 없을 경우 ResponseEntity.ok().body(null)로 200 OK 상태로 응답합니다. 이는 PR 목표와 일치합니다.

미사용 예외 코드를 제거하거나 향후 사용 계획이 있다면 명확히 문서화하는 것을 검토하세요.

src/main/java/org/ezcode/codetest/infrastructure/persistence/repository/draft/DraftRepositoryImpl.java (1)

14-37: LGTM!

리포지토리 구현이 깔끔하게 작성되었습니다. saveAndFlush 메서드를 사용하여 낙관적 락의 버전을 즉시 반환할 수 있도록 구현한 점이 PR 목표와 일치합니다.

src/main/java/org/ezcode/codetest/application/draft/servcie/DraftService.java (1)

58-67: LGTM!

조회 메서드가 적절하게 구현되었습니다. @Transactional(readOnly = true)를 사용하여 읽기 전용 트랜잭션으로 최적화했으며, Optional을 반환하여 데이터가 없는 경우를 우아하게 처리합니다. PR 목표에 명시된 "조회 시 데이터 없으면 빈 객체 반환" 전략과 일치합니다.

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

♻️ Duplicate comments (1)
src/main/java/org/ezcode/codetest/presentation/draft/DraftController.java (1)

33-49: DraftException의 HTTP 상태 코드가 올바르게 반환되는지 확인하세요.

이전 리뷰에서 지적된 대로, GlobalExceptionHandlerDraftException (또는 BaseException)을 처리하지 않아 DRAFT_VERSION_CONFLICT가 의도한 409 Conflict 대신 400 Bad Request로 반환될 수 있습니다. Line 39의 API 문서에서는 409를 명시하고 있으므로, GlobalExceptionHandlerBaseException을 처리하는 @ExceptionHandler 메서드를 추가하여 각 예외의 getHttpStatus()를 올바르게 반환하도록 해야 합니다.

다음 스크립트로 GlobalExceptionHandler에 BaseException 핸들러가 추가되었는지 확인하세요:

#!/bin/bash
# GlobalExceptionHandler에서 BaseException 처리 여부 확인
rg -A 10 "@ExceptionHandler.*BaseException" src/main/java/org/ezcode/codetest/common/base/exception/GlobalExceptionHandler.java
🧹 Nitpick comments (1)
src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java (1)

41-51: 새 Draft 생성 시 버전 파라미터 검증을 고려하세요.

클라이언트가 존재하지 않는 Draft에 대해 version != null을 전송하는 경우, 이는 클라이언트 상태가 불일치함을 나타낼 수 있습니다. 현재는 이 값이 조용히 무시되지만, 클라이언트의 잘못된 가정을 조기에 감지하기 위해 version != null인 경우 예외를 던지는 것을 고려해보세요.

🔎 검증 로직 추가 제안
 } else {
+	// 새 엔티티 생성 시 클라이언트가 버전을 보냈다면 상태 불일치
+	if (version != null) {
+		throw new DraftException(DraftExceptionCode.DRAFT_VERSION_CONFLICT);
+	}
+
 	// 새 엔티티인 경우
 	Draft draft = Draft.builder()
 		.user(user)
 		.problem(problem)
 		.language(language)
 		.code(code)
 		.build();
 
 	return draftRepository.saveAndFlush(draft);
 }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd565af and 2bd1ee7.

📒 Files selected for processing (3)
  • src/main/java/org/ezcode/codetest/application/draft/service/DraftService.java
  • src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java
  • src/main/java/org/ezcode/codetest/presentation/draft/DraftController.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-02T12:05:54.917Z
Learnt from: Kimminu7
Repo: ezcode-my/backend PR: 133
File: src/test/java/org/ezcode/codetest/domain/problem/service/ProblemDomainServiceTest.java:92-99
Timestamp: 2025-07-02T12:05:54.917Z
Learning: ProblemDomainService의 removeProblem 메서드는 DB에서 Problem을 삭제한 후 Elasticsearch에서도 해당 ProblemSearchDocument를 찾아서 삭제합니다. 만약 Elasticsearch에서 문서를 찾지 못하면 ProblemException(PROBLEM_NOT_FOUND)을 던지므로, 테스트에서는 problem.getId()와 searchRepository.findById() 모두 적절하게 mock해야 합니다.

Applied to files:

  • src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (3)
src/main/java/org/ezcode/codetest/domain/draft/service/DraftDomainService.java (1)

54-57: 구현이 명확하고 정확합니다.

단순 조회 로직이 올바르게 구현되었습니다.

src/main/java/org/ezcode/codetest/application/draft/service/DraftService.java (2)

35-56: 낙관적 락 예외 처리가 올바르게 구현되었습니다.

Line 51-52의 ObjectOptimisticLockingFailureException 처리는 DraftDomainService의 명시적 버전 체크(Line 33)를 통과한 후 발생할 수 있는 레이스 컨디션을 방어합니다. 두 단계의 예외 처리(도메인 레이어의 Fail-Fast + 애플리케이션 레이어의 JPA 예외)는 동시성 제어를 위한 올바른 방어적 프로그래밍 패턴입니다.


58-67: 읽기 전용 트랜잭션과 Optional 매핑이 적절합니다.

readOnly = true 트랜잭션 설정과 Optional.map을 통한 DTO 변환이 깔끔하게 구현되었습니다.

Comment on lines +73 to +75
return response
.map(ResponseEntity::ok)
.orElse(ResponseEntity.ok().body(null));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Draft 미존재 시 응답 형식이 PR 목표와 다릅니다.

PR 목표에서는 "조회 시 데이터 없으면 404 대신 빈 문자열("")과 version: null 반환"이라고 명시되어 있지만, 현재 구현은 body(null)을 반환하여 응답 본문이 완전히 비어있습니다.

클라이언트가 일관된 응답 구조를 기대한다면, Draft가 없을 때 DraftResponse 객체를 빈 값들(code="", version=null)로 반환하는 것이 더 나은 선택일 수 있습니다.

🔎 일관된 응답 구조 제안

DraftResponse에 정적 팩토리 메서드를 추가:

// DraftResponse.java에 추가
public static DraftResponse empty() {
    return DraftResponse.builder()
        .code("")
        .version(null)
        .build();
}

그런 다음 컨트롤러를 수정:

 return response
     .map(ResponseEntity::ok)
-    .orElse(ResponseEntity.ok().body(null));
+    .orElseGet(() -> ResponseEntity.ok(DraftResponse.empty()));

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/org/ezcode/codetest/presentation/draft/DraftController.java
around lines 73-75, the controller returns ResponseEntity.ok().body(null) when
the draft is missing; change this to return a DraftResponse instance with code =
"" and version = null so the response shape is consistent. Add a static factory
method DraftResponse.empty() in DraftResponse (or construct the object inline)
that returns code "" and version null, and use
response.map(ResponseEntity::ok).orElse(ResponseEntity.ok(DraftResponse.empty()))
so clients always receive the expected JSON structure.

@NCookies NCookies merged commit c81eb78 into dev Dec 29, 2025
2 checks passed
@NCookies NCookies deleted the feature/draft branch December 29, 2025 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants