Skip to content

fix(backend): 이어하기가 실제로는 아무것도 하지 않던 문제 - #181

Merged
i3months merged 1 commit into
devfrom
fix/resume-stale-entity
Aug 19, 2026
Merged

fix(backend): 이어하기가 실제로는 아무것도 하지 않던 문제#181
i3months merged 1 commit into
devfrom
fix/resume-stale-entity

Conversation

@i3months

Copy link
Copy Markdown
Member

변경 사항

직전 PR(#180)의 후속. 머지 직후 교차 검토하다 발견했다 — 이어하기 기능 전체가 동작하지 않는 상태였다.

무엇이 잘못됐나

재개 전이 후 엔티티를 다시 읽었다:

sessionRepository.resumeIfInterrupted(sessionId, Instant.now());
sessionRepository.flush();
InterviewSession resumed = sessionRepository.findById(sessionId).orElseThrow();

JPQL 벌크 UPDATE 는 영속성 컨텍스트를 갱신하지 않는다. 세션은 바로 위에서 findByIdAndUser_IdAndDeletedFalse 로 이미 로드돼 1차 캐시에 있으므로, findById 는 DB 를 다시 읽지 않고 낡은 인스턴스(status=INTERRUPTED) 를 그대로 돌려준다. flush() 는 변경을 DB 로 밀 뿐 엔티티를 새로 읽지 않는다.

결과:

  1. advanceToNextGeneral 이 첫 줄에서 getStatus() != IN_PROGRESS조용히 되돌아간다 → 생성 중 placeholder 복구도, 다음 질문 발행도 일어나지 않는다. feat: 중단된 면접 이어하기 (B-5) #180 이 자랑한 "끊긴 턴 복구"가 통째로 죽어 있었다
  2. 응답 status 가 INTERRUPTED 로 나가 프론트가 라이브 화면으로 전환하지도 않는다

사용자 입장에선 '이어서 진행하기' 를 눌러도 아무 일도 일어나지 않는다.

수정

이 저장소에는 이미 정답이 있었다 — SessionService.startstartIfReady 뒤에 session.start() 로 인메모리를 맞춘다:

if (sessionRepository.startIfReady(sessionId, Instant.now()) == 0) { ... }
session.start();  // 응답·인메모리 동기화(DB는 위 조건부 UPDATE 로 이미 IN_PROGRESS).

같은 방식으로 InterviewSession.resume(now) 를 추가했다. UPDATE 와 같은 now 를 넘겨 DB 와 인메모리 값이 어긋나지 않게 했다(start() 는 각자 Instant.now() 를 써서 미세하게 다를 수 있는데, 여기선 피했다).

테스트가 왜 못 잡았나

sessionRepository.findById별도의 IN_PROGRESS 인스턴스를 돌려주도록 목을 걸어서, 운영에서 낡은 엔티티가 오는 상황 자체가 재현되지 않았다. 목이 실제 동작을 가린 전형적인 경우다.

픽스처를 인스턴스 하나만 쓰도록 바꾸고 resume_syncsInMemoryStateAfterBulkUpdate 를 추가했다 — 상태 · resumedAt · endedAt 소거 · durationAnchor() · 응답 status 를 한 번에 못 박는다.

수정 전 코드로 되돌려 이 테스트가 실패하는 것을 확인했다 (resume_leavesAnsweredQuestionAlone, resume_syncsInMemoryStateAfterBulkUpdate 2건 FAILED).

참고: 복구 분기 테스트들은 QuestionsCallbackService 를 목으로 두기 때문에 이 버그를 잡지 못한다(목에는 상태 검사가 없다). 그래서 인메모리 동기화 자체를 검증 대상으로 삼았다.

영향 범위

backend/CLAUDE.md 에 "조건부 UPDATE 뒤에는 인메모리 상태를 반드시 맞춘다" 를 규약으로 적었다 — 같은 실수가 다음 전이에서 반복되지 않게.

리뷰어 체크포인트

  • 다른 조건부 UPDATE 경로(end, interrupt, cancel)는 전이 후 엔티티 상태를 읽지 않거나 이미 동기화하고 있어 같은 문제가 없다. startsession.start() 로 맞추고, end/interrupt 는 이후 엔티티 상태에 의존하지 않는다

직전 PR(#180)에서 재개 후 엔티티를 `findById` 로 다시 읽었다. **벌크 UPDATE 는 영속성
컨텍스트를 갱신하지 않으므로** 같은 트랜잭션의 `findById` 는 1차 캐시에 있는 낡은
엔티티(status=INTERRUPTED)를 그대로 돌려준다. 결과적으로:

- `advanceToNextGeneral` 이 `getStatus() != IN_PROGRESS` 에서 조용히 되돌아가
  **턴 복구가 통째로 죽었다** — 생성 중 placeholder도, 다음 질문 발행도 일어나지 않는다
- 응답 `status` 가 INTERRUPTED 로 나가 프론트가 라이브 화면으로 전환하지도 않는다

즉 이어하기 기능 전체가 동작하지 않았다.

`SessionService.start` 가 `startIfReady` 뒤에 `session.start()` 로 인메모리를 맞추는 것과
같은 방식으로 `InterviewSession.resume(now)` 을 추가해 맞춘다. UPDATE 와 같은 `now` 를
넘겨 DB 와 값이 어긋나지 않게 했다.

## 테스트가 왜 못 잡았나

`sessionRepository.findById` 를 **별도의 IN_PROGRESS 인스턴스**를 돌려주도록 목을 걸어서,
운영에서 낡은 엔티티가 오는 상황이 재현되지 않았다. 픽스처를 인스턴스 하나만 쓰도록 바꾸고
`resume_syncsInMemoryStateAfterBulkUpdate` 를 추가했다 — 상태·resumedAt·endedAt·
durationAnchor·응답 status 를 한 번에 못 박는다. 수정 전 코드로 되돌리면 이 테스트가 실패하는
것을 확인했다.

`backend/CLAUDE.md` 에 "조건부 UPDATE 뒤 인메모리 동기화" 를 규약으로 적었다.
@i3months
i3months merged commit e689b21 into dev Aug 19, 2026
5 checks passed
@i3months
i3months deleted the fix/resume-stale-entity branch August 19, 2026 06:21
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