[fix] MOA-218 동아리 정보나 지원서 수정 시에 동시성 문제를 해결한다#724
Conversation
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
Global exception handling for concurrencybackend/src/main/java/moadong/global/exception/GlobalExceptionHandler.java |
@Slf4j 추가, OptimisticLockingFailureException 및 UncategorizedMongoDbException 처리 핸들러 추가(409 CONFLICT, ErrorCode.CONCURRENCY_CONFLICT 응답) |
Service formatting onlybackend/src/main/java/moadong/club/service/ClubApplyService.java |
메서드 사이 공백 라인 추가 등 포맷팅 변경만 수행, 로직/시그니처 변경 없음 |
Concurrency test revisionsbackend/src/test/java/moadong/club/service/ClubApplyServiceTest.java, backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java |
멀티스레드 수 확대 및 CyclicBarrier 동기화 도입, 저장소 직접 업데이트 방식으로 전환(일부), 성공 1/나머지 충돌 검증, MockMvc 자동 구성과 PasswordEncoder 주입, 테스트 메서드명 변경 |
Sequence Diagram(s)
sequenceDiagram
autonumber
participant C as Client
participant API as Controller
participant S as Service
participant R as Repository/DB
participant GX as GlobalExceptionHandler
C->>API: PUT /clubs/... (update)
API->>S: edit(...)
S->>R: save(entity)
R-->>S: OptimisticLockingFailureException
S-->>API: throw
API-->>GX: propagate exception
GX-->>C: HTTP 409 CONFLICT (ErrorCode.CONCURRENCY_CONFLICT)
note over GX: 중앙집중 예외 처리로 동시성 충돌 표준화 응답
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
Assessment against linked issues
| Objective | Addressed | Explanation |
|---|---|---|
| 동아리 정보/지원서 수정 시 낙관적 락 충돌 발생 시 Retry로 최신 데이터 재로딩 후 수정 구현 (MOA-218) | ❌ | 재시도/재로딩 로직이 서비스 계층에 구현되지 않음. 전역 409 처리 및 테스트 개편만 확인됨. |
Assessment against linked issues: Out-of-scope changes
(해당 없음)
Possibly related PRs
- [feature] 액세스 토큰 및 리프레시 토큰의 만료 시간을 수정하고, 로그인, 관리자 계정 관련의 동시성 문제를 해결한다 #713 — 전역 동시성 충돌 처리와 관련된 변경(에러 코드/핸들러) 및 낙관적 락 테스트 보강으로 본 PR과 동일 주제의 연장선 가능성
Suggested labels
🐞 Bug, 🛠Fix, 💾 BE, ✅ Test
Suggested reviewers
- lepitaaar
✨ Finishing Touches
- 📝 Generate Docstrings
🧪 Generate unit tests
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
fix/#723-concurrency-MOA-218
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.
🪧 Tips
Chat
There are 3 ways to chat with CodeRabbit:
- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
I pushed a fix in commit <commit_id>, please review it.Open a follow-up GitHub issue for this discussion.
- Files and specific lines of code (under the "Files changed" tab): Tag
@coderabbitaiin a new review comment at the desired location with your query. - PR comments: Tag
@coderabbitaiin a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
Support
Need help? Create a ticket on our support page for assistance with any issues or questions.
CodeRabbit Commands (Invoked using PR/Issue comments)
Type @coderabbitai help to get the list of available commands.
Other keywords and placeholders
- Add
@coderabbitai ignoreor@coderabbit ignoreanywhere in the PR description to prevent this PR from being reviewed. - Add
@coderabbitai summaryto generate the high-level summary at a specific location in the PR description. - Add
@coderabbitaianywhere in the PR title to generate the title automatically.
Status, Documentation and Community
- Visit our Status Page to check the current availability of CodeRabbit.
- Visit our Documentation for detailed information on how to use CodeRabbit.
- Join our Discord Community to get help, request features, and share feedback.
- Follow us on X/Twitter for updates and announcements.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Test Results80 tests 77 ✅ 6s ⏱️ Results for commit 1eb97b4. |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/src/main/java/moadong/club/service/ClubApplyService.java (1)
51-59: 개별 질문 수정에도 동일한 재시도 패턴을 적용하세요.단발 저장으로는 충돌 시 항상 실패합니다. 아래와 같이 동일 패턴으로 보완을 제안합니다.
- @Transactional - public void editClubApplicationQuestion(String questionId, CustomUserDetails user, ClubApplicationEditRequest request) { - ClubQuestion clubQuestion = clubQuestionRepository.findById(questionId) - .orElseThrow(() -> new RestApiException(ErrorCode.QUESTION_NOT_FOUND)); - - updateQuestions(clubQuestion, request); - clubQuestion.updateEditedAt(); - - clubQuestionRepository.save(clubQuestion); - } + @Transactional + public void editClubApplicationQuestion(String questionId, CustomUserDetails user, ClubApplicationEditRequest request) { + final int maxRetries = 3; + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + ClubQuestion clubQuestion = clubQuestionRepository.findById(questionId) + .orElseThrow(() -> new RestApiException(ErrorCode.QUESTION_NOT_FOUND)); + updateQuestions(clubQuestion, request); + clubQuestion.updateEditedAt(); + clubQuestionRepository.save(clubQuestion); + return; + } catch (org.springframework.dao.OptimisticLockingFailureException | org.springframework.dao.DataAccessException e) { + if (!isWriteConflict(e) || attempt == maxRetries) { + throw e; + } + } + } + }
🧹 Nitpick comments (9)
backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java (3)
3-3: 사용되지 않는 MockMvc/ObjectMapper 제거 또는 실제 사용으로 전환하세요.현재 MockMvc/ObjectMapper가 주입되지만 사용되지 않습니다. 불필요한 의존성은 테스트 안정성을 해칩니다.
-import com.fasterxml.jackson.databind.ObjectMapper; @@ -@AutoConfigureMockMvc @@ - @Autowired - private PasswordEncoder passwordEncoder; - @Autowired - private MockMvc mockMvc; + @Autowired + private PasswordEncoder passwordEncoder;Also applies to: 15-20
89-91: 스레드 풀 종료 안전성 보강 제안.플래키 상황에서 풀 미종료를 막기 위해 awaitTermination + shutdownNow를 보강하세요.
- latch.await(); // 모든 스레드가 작업을 마칠 때까지 대기 - executorService.shutdown(); + latch.await(); // 모든 스레드가 작업을 마칠 때까지 대기 + executorService.shutdown(); + if (!executorService.awaitTermination(1, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + }
69-77: 메시지 기반 WriteConflict 판정은 취약 — 원인 체인 검사로 완화하세요.문구 변경 시 깨질 수 있습니다. helper로 cause 체인을 확인하세요.
private static boolean containsWriteConflict(Throwable t) { while (t != null) { String m = t.getMessage(); if (m != null && m.contains("WriteConflict")) return true; t = t.getCause(); } return false; }- } catch (DataAccessException e) { - if (e.getMessage() != null && e.getMessage().contains("WriteConflict")) { + } catch (DataAccessException e) { + if (containsWriteConflict(e)) { conflictCount.incrementAndGet(); } else { e.printStackTrace(); }backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (6)
4-4: 미사용 import 정리.-import moadong.club.entity.ClubApplicationQuestion;
15-15: 미사용 import 정리.-import org.bson.types.ObjectId;
59-64: CyclicBarrier 동기화 적절 — 플래키 완화 위해 반복 실행 도입 고려.ClubProfileServiceTest처럼 @RepeatedIfExceptionsTest를 적용하면 간헐 실패를 줄일 수 있습니다.
70-70: 미사용 지역변수 제거.- final int threadNum = i; executorService.submit(() -> {
74-74: Optional#get 직접 호출 지양 — 존재하지 않을 때 명시 메시지로 실패하세요.- ClubQuestion questionToUpdate = clubQuestionRepository.findById(this.clubQuestionId).get(); + ClubQuestion questionToUpdate = clubQuestionRepository.findById(this.clubQuestionId) + .orElseThrow(() -> new NoSuchElementException("ClubQuestion not found: " + this.clubQuestionId));
87-95: 모든 DataAccessException을 충돌로 집계하면 과대포착 위험.낙관적 락(OptimisticLockingFailureException)과 WriteConflict만 충돌로 집계하세요.
- } catch (DataAccessException e) { - // DataAccessException은 WriteConflict 등을 포함 - conflictCount.incrementAndGet(); + } catch (OptimisticLockingFailureException e) { + conflictCount.incrementAndGet(); + } catch (DataAccessException e) { + if (e.getMessage() != null && e.getMessage().contains("WriteConflict")) { + conflictCount.incrementAndGet(); + } else { + e.printStackTrace(); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (4)
backend/src/main/java/moadong/club/service/ClubApplyService.java(1 hunks)backend/src/main/java/moadong/global/exception/GlobalExceptionHandler.java(2 hunks)backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java(5 hunks)backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java(2 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-05-19T05:45:52.957Z
Learnt from: lepitaaar
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/service/ClubApplyService.java:34-38
Timestamp: 2025-05-19T05:45:52.957Z
Learning: The code duplication between createClubApplication and editClubApplication methods in ClubApplyService.java is acknowledged but will be addressed in a future refactoring, as per the developer's plan.
Applied to files:
backend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
📚 Learning: 2025-08-25T14:43:52.320Z
Learnt from: lepitaaar
PR: Moadong/moadong#703
File: backend/src/main/java/moadong/club/controller/ClubApplyController.java:84-84
Timestamp: 2025-08-25T14:43:52.320Z
Learning: In the Moadong codebase, questionId and clubId are equivalent identifiers that represent the same entity. The ClubApplicationRepository.findAllByIdInAndQuestionId method correctly uses clubId as the questionId parameter for filtering club applications.
Applied to files:
backend/src/main/java/moadong/club/service/ClubApplyService.javabackend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
📚 Learning: 2025-05-15T12:03:57.356Z
Learnt from: Zepelown
PR: Moadong/moadong#406
File: backend/src/main/java/moadong/club/entity/ClubApplicationQuestion.java:32-33
Timestamp: 2025-05-15T12:03:57.356Z
Learning: 엔티티 클래스는 요청/응답 객체(DTO)에 의존해서는 안 됩니다. 계층 간 의존성 문제를 방지하기 위해 엔티티와 DTO는 분리되어야 합니다. 예를 들어, `ClubApplicationQuestion` 엔티티가 `ClubApplicationRequest.Options`와 같은 요청 객체를 직접 참조하는 대신, 엔티티 패키지 내에 `QuestionOptions`와 같은 별도의 클래스를 정의하고 사용해야 합니다. 이렇게 하면 요청 객체 변경이 데이터베이스 스키마나 엔티티 계층에 영향을 미치지 않습니다.
Applied to files:
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java
🧬 Code graph analysis (2)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (2)
backend/src/test/java/moadong/fixture/UserFixture.java (1)
UserFixture(9-33)backend/src/test/java/moadong/fixture/ClubApplicationEditFixture.java (1)
ClubApplicationEditFixture(10-26)
backend/src/test/java/moadong/club/service/ClubProfileServiceTest.java (2)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (1)
IntegrationTest(29-107)backend/src/test/java/moadong/fixture/UserFixture.java (1)
UserFixture(9-33)
🔇 Additional comments (3)
backend/src/test/java/moadong/club/service/ClubApplyServiceTest.java (3)
41-41: 엔티티 대신 ID만 보관하는 변경 좋습니다.동시 조회-수정 테스트에서 스냅샷 오염을 줄입니다.
49-53: setUp에서 ID만 선조회 — 동시성 테스트에 적합합니다.
104-105: 검증 로직 적절합니다.1 성공, 나머지 충돌이라는 기대치가 명확합니다.
| @Transactional | ||
| public void editClubApplication(String clubId, CustomUserDetails user, ClubApplicationEditRequest request) { | ||
| ClubQuestion clubQuestion = getClubQuestion(clubId, user); | ||
|
|
||
| clubQuestion.updateEditedAt(); | ||
| clubQuestionRepository.save(updateQuestions(clubQuestion, request)); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
낙관적 락 충돌 시 재시도 로직이 없습니다 (MOA-218 요구사항 불충족).
이슈 체크리스트에 명시된 “충돌 시 최신 데이터 재조회 후 재적용(retry)”가 서비스 레이어에 구현되어 있지 않습니다. 409만 반환하면 사용자 경험상 손실이 큽니다. 아래처럼 최대 3회 재시도를 권장합니다.
- @Transactional
- public void editClubApplication(String clubId, CustomUserDetails user, ClubApplicationEditRequest request) {
- ClubQuestion clubQuestion = getClubQuestion(clubId, user);
-
- clubQuestion.updateEditedAt();
- clubQuestionRepository.save(updateQuestions(clubQuestion, request));
- }
+ @Transactional
+ public void editClubApplication(String clubId, CustomUserDetails user, ClubApplicationEditRequest request) {
+ final int maxRetries = 3;
+ for (int attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ ClubQuestion clubQuestion = getClubQuestion(clubId, user);
+ clubQuestion.updateEditedAt();
+ clubQuestionRepository.save(updateQuestions(clubQuestion, request));
+ return;
+ } catch (org.springframework.dao.OptimisticLockingFailureException | org.springframework.dao.DataAccessException e) {
+ if (!isWriteConflict(e) || attempt == maxRetries) {
+ throw e;
+ }
+ // 재시도: 다음 루프에서 최신 버전 재조회
+ }
+ }
+ }도움 메서드(파일 하단 등 임의 위치에 추가):
// 동시성 충돌 여부 판별(테스트와 동일한 WriteConflict 문자열도 허용)
private boolean isWriteConflict(Throwable t) {
while (t != null) {
String msg = t.getMessage();
if (msg != null && msg.contains("WriteConflict")) return true;
t = t.getCause();
}
return false;
}검증 스크립트(ClubQuestion에 @Version 존재 여부 확인):
🏁 Script executed:
#!/bin/bash
rg -n --type=java -C2 '@Version' | rg -n 'class\s+ClubQuestion|@Version|-C'Length of output: 223
낙관적 락 충돌 시 3회 재시도 로직 구현 필요
ClubApplyService.editClubApplication 메서드가 OptimisticLockingFailureException 발생 시 최신 엔티티를 재조회해 최대 3회 재시도하도록 수정해야 합니다(ClubQuestion 엔티티에 @Version이 적용되어 있음 확인됨).
backend/src/main/java/moadong/club/service/ClubApplyService.java:42-48
- @Transactional
- public void editClubApplication(String clubId, CustomUserDetails user, ClubApplicationEditRequest request) {
- ClubQuestion clubQuestion = getClubQuestion(clubId, user);
-
- clubQuestion.updateEditedAt();
- clubQuestionRepository.save(updateQuestions(clubQuestion, request));
- }
+ @Transactional
+ public void editClubApplication(String clubId, CustomUserDetails user, ClubApplicationEditRequest request) {
+ final int maxRetries = 3;
+ for (int attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ ClubQuestion clubQuestion = getClubQuestion(clubId, user);
+ clubQuestion.updateEditedAt();
+ clubQuestionRepository.save(updateQuestions(clubQuestion, request));
+ return;
+ } catch (org.springframework.dao.OptimisticLockingFailureException | org.springframework.dao.DataAccessException e) {
+ if (!isWriteConflict(e) || attempt == maxRetries) {
+ throw e;
+ }
+ // 재시도: 다음 루프에서 최신 버전 재조회
+ }
+ }
+ }
+
+// ClubQuestion에 @Version 충돌 여부 판단용 헬퍼
+private boolean isWriteConflict(Throwable t) {
+ while (t != null) {
+ String msg = t.getMessage();
+ if (msg != null && msg.contains("WriteConflict")) return true;
+ t = t.getCause();
+ }
+ return false;
+}📝 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.
| @Transactional | |
| public void editClubApplication(String clubId, CustomUserDetails user, ClubApplicationEditRequest request) { | |
| ClubQuestion clubQuestion = getClubQuestion(clubId, user); | |
| clubQuestion.updateEditedAt(); | |
| clubQuestionRepository.save(updateQuestions(clubQuestion, request)); | |
| } | |
| @Transactional | |
| public void editClubApplication(String clubId, CustomUserDetails user, ClubApplicationEditRequest request) { | |
| final int maxRetries = 3; | |
| for (int attempt = 1; attempt <= maxRetries; attempt++) { | |
| try { | |
| ClubQuestion clubQuestion = getClubQuestion(clubId, user); | |
| clubQuestion.updateEditedAt(); | |
| clubQuestionRepository.save(updateQuestions(clubQuestion, request)); | |
| return; | |
| } catch (org.springframework.dao.OptimisticLockingFailureException | org.springframework.dao.DataAccessException e) { | |
| if (!isWriteConflict(e) || attempt == maxRetries) { | |
| throw e; | |
| } | |
| // 재시도: 다음 루프에서 최신 버전 재조회 | |
| } | |
| } | |
| } | |
| // ClubQuestion에 @Version 충돌 여부 판단용 헬퍼 | |
| private boolean isWriteConflict(Throwable t) { | |
| while (t != null) { | |
| String msg = t.getMessage(); | |
| if (msg != null && msg.contains("WriteConflict")) return true; | |
| t = t.getCause(); | |
| } | |
| return false; | |
| } |
| @ExceptionHandler({OptimisticLockingFailureException.class, UncategorizedMongoDbException.class}) | ||
| public ResponseEntity<Response> handleConcurrencyConflict(Exception ex) { | ||
| log.warn("데이터베이스 충돌 발생: {}", ex.getMessage()); | ||
| return ResponseEntity | ||
| .status(HttpStatus.CONFLICT) | ||
| .body(new Response(ErrorCode.CONCURRENCY_CONFLICT.getCode(), ErrorCode.CONCURRENCY_CONFLICT.getMessage(), null)); | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
❓ Verification inconclusive
UncategorizedMongoDbException을 409로 포괄 처리하면 과대포착 위험이 큽니다 — DataAccessException 포함 + WriteConflict 필터링 권장.
동시성 외의 DB 오류도 409로 변환될 수 있습니다. DataAccessException을 함께 받되, WriteConflict(또는 낙관적 락) 케이스만 409로 매핑하세요. 나머지는 500 등으로 위임하는 것이 안전합니다.
+import org.springframework.dao.DataAccessException;
@@
- @ExceptionHandler({OptimisticLockingFailureException.class, UncategorizedMongoDbException.class})
- public ResponseEntity<Response> handleConcurrencyConflict(Exception ex) {
- log.warn("데이터베이스 충돌 발생: {}", ex.getMessage());
- return ResponseEntity
- .status(HttpStatus.CONFLICT)
- .body(new Response(ErrorCode.CONCURRENCY_CONFLICT.getCode(), ErrorCode.CONCURRENCY_CONFLICT.getMessage(), null));
- }
+ @ExceptionHandler({OptimisticLockingFailureException.class, UncategorizedMongoDbException.class, DataAccessException.class})
+ public ResponseEntity<Response> handleConcurrencyConflict(Exception ex) {
+ if (ex instanceof DataAccessException dae && !isWriteConflict(dae)) {
+ log.error("데이터베이스 예외(동시성 아님): {}", dae.getMessage(), dae);
+ return ResponseEntity
+ .status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(new Response("INTERNAL_SERVER_ERROR", "서버 내부 오류가 발생했습니다.", null));
+ }
+ log.warn("데이터베이스 충돌 발생: {}", ex.getMessage());
+ return ResponseEntity
+ .status(HttpStatus.CONFLICT)
+ .body(new Response(ErrorCode.CONCURRENCY_CONFLICT.getCode(), ErrorCode.CONCURRENCY_CONFLICT.getMessage(), null));
+ }도움 메서드(파일 하단 등 임의 위치 추가):
private boolean isWriteConflict(Throwable t) {
while (t != null) {
String msg = t.getMessage();
if (msg != null && msg.contains("WriteConflict")) return true;
t = t.getCause();
}
return false;
}UncategorizedMongoDbException 409 처리 범위 제한 및 WriteConflict 필터링 추가
UncategorizedMongoDbException을 그대로 409로 응답하면 낙관적 락 외의 모든 DB 오류까지 과대포착됩니다. DataAccessException을 함께 받되, WriteConflict(또는 OptimisticLockingFailureException) 케이스만 409로 매핑하고 나머지는 500으로 위임하세요.
+import org.springframework.dao.DataAccessException;
@@
- @ExceptionHandler({OptimisticLockingFailureException.class, UncategorizedMongoDbException.class})
- public ResponseEntity<Response> handleConcurrencyConflict(Exception ex) {
- log.warn("데이터베이스 충돌 발생: {}", ex.getMessage());
- return ResponseEntity
- .status(HttpStatus.CONFLICT)
- .body(new Response(ErrorCode.CONCURRENCY_CONFLICT.getCode(), ErrorCode.CONCURRENCY_CONFLICT.getMessage(), null));
- }
+ @ExceptionHandler({OptimisticLockingFailureException.class, UncategorizedMongoDbException.class, DataAccessException.class})
+ public ResponseEntity<Response> handleConcurrencyConflict(Exception ex) {
+ if (ex instanceof DataAccessException dae && !isWriteConflict(dae)) {
+ log.error("데이터베이스 예외(동시성 아님): {}", dae.getMessage(), dae);
+ return ResponseEntity
+ .status(HttpStatus.INTERNAL_SERVER_ERROR)
+ .body(new Response("INTERNAL_SERVER_ERROR", "서버 내부 오류가 발생했습니다.", null));
+ }
+ log.warn("데이터베이스 충돌 발생: {}", ex.getMessage());
+ return ResponseEntity
+ .status(HttpStatus.CONFLICT)
+ .body(new Response(ErrorCode.CONCURRENCY_CONFLICT.getCode(), ErrorCode.CONCURRENCY_CONFLICT.getMessage(), null));
+ }
도움 메서드 위치(클래스 하단 등):
```java
private boolean isWriteConflict(Throwable t) {
while (t != null) {
String msg = t.getMessage();
if (msg != null && msg.contains("WriteConflict")) return true;
t = t.getCause();
}
return false;
}🤖 Prompt for AI Agents
In backend/src/main/java/moadong/global/exception/GlobalExceptionHandler.java
around lines 28-34, narrow the 409 mapping so only
optimistic-lock/write-conflict cases return CONFLICT: change the handler to
accept DataAccessException (or Exception) and implement a small
isWriteConflict(Throwable) helper that walks the cause chain checking for
"WriteConflict" in messages; if ex is an instance of
OptimisticLockingFailureException or isWriteConflict(ex) then keep returning
409, otherwise log appropriately and delegate to/return a 500
INTERNAL_SERVER_ERROR response (or rethrow to let the global fallback handle
it).
테스트용으로 필요합니다 |
#️⃣연관된 이슈
#723
📝작업 내용
동시성 문제 발생 시에 에러 핸들링 추가
-> 커스텀 에러 반환

동아리 지원서 동시성 문제 테스트 코드 추가
application.properties에 spring.data.mongodb.write-concern.w=majority 추가
-> MongoDB에 데이터를 쓸 때 어느 수준까지 데이터의 안정성을 보장받을 것인지를 정의하는 옵션으로 MongoDB Altas 샤딩에서 쓰기 작업이 과반수 노드에 안전하게 저장되었음이 보장됨.
-> 낙관적 락의 Version 필드가 정상적으로 다 반영이 됐다고 가정할 수 있게 됨.
-> 낙관적 락이 정상적으로 동작함.
중점적으로 리뷰받고 싶은 부분(선택)
논의하고 싶은 부분(선택)
🫡 참고사항