Conversation
Summary by CodeRabbit
Walkthrough게스트(비회원) 인터뷰를 IP 기반으로 시작·진행·조회할 수 있도록 클라이언트 IP 파라미터와 게스트 전용 흐름(락, 검증, 저장, 결과 조회)을 서비스·컨트롤러·도메인·DB 마이그레이션에 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as 클라이언트
participant Controller as InterviewController
participant FacadeStart as InterviewStartFacadeService
participant Redis as Redis
participant RootService as RootQuestionService
participant Repo as InterviewRepository
participant DB as Database
Client->>Controller: POST /api/v1/interviews/guest (ClientIp)
Controller->>FacadeStart: startGuestInterview(ClientIp)
FacadeStart->>Redis: SET lock:interview:started:guest:<ip> (NX, TTL)
alt lock 획득
Redis-->>FacadeStart: OK
FacadeStart->>RootService: readRandomActiveRootQuestion()
RootService->>Repo: findAllByState(ACTIVE)
Repo->>DB: SELECT ...
DB-->>Repo: [rows]
Repo-->>RootService: questions
RootService-->>FacadeStart: chosen question
FacadeStart->>Repo: save(Interview member=null, guestIp)
Repo->>DB: INSERT ...
DB-->>Repo: interview_id
FacadeStart-->>Controller: InterviewStartResponse
Controller-->>Client: 200 OK
else lock 실패
Redis-->>FacadeStart: exists
FacadeStart-->>Controller: BadRequestException
Controller-->>Client: 400 Bad Request
end
sequenceDiagram
participant Client as 클라이언트
participant Controller as InterviewController
participant FacadeProceed as InterviewProceedFacadeService
participant Service as InterviewService
participant Redis as Redis
participant Async as InterviewProceedBedrockFlowAsyncService
participant DB as Database
Client->>Controller: POST /api/v1/interviews/{id}/proceed (ClientIp, optional auth)
Controller->>FacadeProceed: proceedInterviewBlockAsync(..., memberAuth?, ClientIp)
FacadeProceed->>Service: (if unauthenticated) validateGuestInterviewee(interviewId, ClientIp)
Service->>DB: SELECT interview WHERE id=?
DB-->>Service: interview (guestIp)
alt guestIp 일치
Service-->>FacadeProceed: 검증 통과
FacadeProceed->>Redis: SET lock:interview:proceed:guest:<ip> (NX)
Redis-->>FacadeProceed: lockKey
FacadeProceed->>Async: proceedInterviewByBedrockFlowAsync(memberId?, ..., interviewId, lockKey, lockValue)
Async-->>FacadeProceed: ack
FacadeProceed-->>Controller: 202 Accepted
Controller-->>Client: 202 Accepted
else 불일치
Service-->>FacadeProceed: ForbiddenException
FacadeProceed-->>Controller: 403 Forbidden
Controller-->>Client: 403 Forbidden
end
sequenceDiagram
participant Client as 클라이언트
participant Controller as InterviewController
participant QueryService as InterviewQueryService
participant Service as InterviewService
participant DB as Database
Client->>Controller: GET /api/v1/interviews/{id}/my-result (ClientIp, optional auth)
Controller->>QueryService: findMyInterviewResult(interviewId, memberAuth?, ClientIp)
QueryService->>Service: findMyInterviewResult(interviewId, memberAuth?, ClientIp)
Service->>DB: SELECT interview WHERE id=?
DB-->>Service: interview (maybe guest)
alt unauthenticated
Service->>Service: validateGuestInterviewee(interviewId, ClientIp)
alt guestIp 일치
Service->>DB: fetch answers/feedback
DB-->>Service: data
Service-->>QueryService: InterviewResultResponse.createMineForGuest(...)
else 불일치
Service-->>QueryService: ForbiddenException
end
else authenticated
Service-->>QueryService: member result flow
end
QueryService-->>Controller: InterviewResultResponse
Controller-->>Client: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이번 변경 사항은 비회원 사용자가 인터뷰 기능을 체험할 수 있도록 시스템을 확장하는 것입니다. IP 주소를 활용하여 비회원을 식별하고, 인터뷰 생성 및 결과 조회 로직을 비회원도 사용할 수 있도록 범용화했습니다. 또한, 비회원 인터뷰는 1회로 제한하며, 기존 회원 인터뷰 로직과의 호환성을 유지하면서도 보안을 위해 비회원 인터뷰 결과는 공개되지 않도록 설정했습니다. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. 비회원도 면접을 보네, 질문은 쏟아지고 땀은 흐르네. IP로 식별하고 락을 걸어, 한 번의 기회 소중히 여기네. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request implements a guest interview feature, allowing unauthenticated users to participate in a limited text-mode interview identified by their IP address. Key changes include modifying the Interview entity to support nullable members, implementing IP-based rate limiting via Redis, and updating controllers and services to handle unauthenticated sessions. Feedback highlights a potential NullPointerException in InterviewService when memberAuth is null, a performance concern regarding in-memory random question selection in RootQuestionService, and a recommendation to shorten the 365-day Redis TTL for guest IP locks to better accommodate dynamic IP environments.
| @Transactional(readOnly = true) | ||
| public InterviewResultResponse findMyInterviewResult(Long interviewId, MemberAuth memberAuth) { | ||
| public InterviewResultResponse findMyInterviewResult(Long interviewId, MemberAuth memberAuth, ClientIp clientIp) { | ||
| if (!memberAuth.isAuthenticated()) { |
| private final QuestionVoicePathResolver questionVoicePathResolver; | ||
|
|
||
| public RootQuestion findRandomActiveRootQuestion() { | ||
| List<RootQuestion> rootQuestions = rootQuestionRepository.findAllByState(RootQuestionState.ACTIVE); |
| public static final String GUEST_INTERVIEW_STARTED_LOCK_KEY_PREFIX = "guest:interview:started:"; | ||
| public static final int GUEST_INTERVIEW_MAX_QUESTION_COUNT = 3; | ||
| public static final InterviewMode GUEST_INTERVIEW_MODE = InterviewMode.TEXT; | ||
| public static final Duration GUEST_INTERVIEW_LOCK_TTL = Duration.ofDays(365); |
Test Results 51 files 51 suites 4m 24s ⏱️ Results for commit 887e2b6. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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/samhap/kokomen/interview/domain/Interview.java`:
- Around line 87-88: The guestIp field stores raw IPs which is a
security/privacy risk; change storage and comparison to a server-secret HMAC
instead: replace plain String guestIp persistence with a hashed field (e.g.,
guestIpHmac) and compute HMAC using a server-side secret when persisting and
when comparing in domain methods, update affected methods that read/compare
guestIp (search for guestIp uses in Interview and related comparison logic) and
add a migration to transform existing plain IPs to HMACs (or nullify), plus
ensure the secret is injected via configuration and never logged; apply the same
conversion for other places in this class that currently persist raw IPs.
In
`@src/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.java`:
- Around line 74-90: The guest-start lock currently uses acquireLock(lockKey) +
releaseLock(lockKey) which unconditionally deletes the key on error; change to
ownership-based locking: have startGuestInterview call the lock that returns a
lockValue (use createGuestInterviewStartedLockKey and the acquire variant that
returns a token), store that lockValue, and call releaseLock(lockKey, lockValue)
so the lock is only removed if the token matches (mirror the proceed-lock
pattern); ensure the catch/finally uses that lockValue when calling releaseLock
and that acquire failure still throws the BadRequestException.
In
`@src/main/java/com/samhap/kokomen/interview/service/question/RootQuestionService.java`:
- Line 30: The method name RootQuestionService.findRandomActiveRootQuestion
violates the team prefix rule because it throws when no value exists; rename it
to readRandomActiveRootQuestion and update all references (interfaces,
implementations, callers, and tests) accordingly so the signature and semantics
remain identical but the prefix reflects that the method guarantees a value;
ensure any overridden/implemented methods and Javadoc/comments are updated to
match the new name.
In `@src/main/resources/db/migration/V43__allow_guest_interview.sql`:
- Around line 5-7: 현재 plain-text guest_ip 컬럼과 인덱스(idx_interview_guest_ip, table
interview)는 개인정보 리스크가 있으니 평문 저장을 제거하거나 대체해야 합니다: 대신 guest_ip_hash 같은 컬럼을 추가하고(예:
VARCHAR(64)) IP를 서버 시크릿(salt)과 함께 안전한 해시(예: SHA-256)로 저장하도록 애플리케이션/마이그레이션을 변경하고,
기존 guest_ip 데이터를 변환해 해시로 마이그레이션한 뒤 원본 guest_ip 컬럼과 인덱스(idx_interview_guest_ip)를
제거하거나 NULL로 비우고 필요 시 해시 컬럼에 인덱스 생성하십시오; 또한 보존기간 기반 삭제 정책(예: interview 테이블에
guest_ip_hash 만 남기고 일정 기간 후 삭제)을 함께 설계해 구현하세요.
In
`@src/test/java/com/samhap/kokomen/interview/controller/InterviewControllerTest.java`:
- Around line 1388-1392: The test is sharing Redis state with other tests by
using a fixed IP and lock key; update the test that sets guestIp and calls
redisService.acquireLock (using
InterviewStartFacadeService.GUEST_INTERVIEW_STARTED_LOCK_KEY_PREFIX and
GUEST_INTERVIEW_LOCK_TTL) so it does not conflict: either generate a unique
guestIp per test (e.g., append a random/suffix) or explicitly clean the Redis
key after the test by removing
InterviewStartFacadeService.GUEST_INTERVIEW_STARTED_LOCK_KEY_PREFIX + guestIp
(or call the corresponding redisService.release/delete method) to ensure test
isolation and deterministic runs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0c140ef2-8da3-4bcb-a22f-d68c77e5a48e
📒 Files selected for processing (23)
src/docs/asciidoc/index.adocsrc/main/java/com/samhap/kokomen/interview/controller/InterviewController.javasrc/main/java/com/samhap/kokomen/interview/controller/InterviewControllerV2.javasrc/main/java/com/samhap/kokomen/interview/domain/Interview.javasrc/main/java/com/samhap/kokomen/interview/repository/RootQuestionRepository.javasrc/main/java/com/samhap/kokomen/interview/service/InterviewProceedFacadeService.javasrc/main/java/com/samhap/kokomen/interview/service/InterviewQueryService.javasrc/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.javasrc/main/java/com/samhap/kokomen/interview/service/core/InterviewProceedService.javasrc/main/java/com/samhap/kokomen/interview/service/core/InterviewService.javasrc/main/java/com/samhap/kokomen/interview/service/dto/InterviewResultResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/check/InterviewCheckResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/check/InterviewCheckTextModeResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/check/InterviewCheckVoiceModeResponse.javasrc/main/java/com/samhap/kokomen/interview/service/dto/check/InterviewFinishedCheckResponse.javasrc/main/java/com/samhap/kokomen/interview/service/infra/InterviewProceedBedrockFlowAsyncService.javasrc/main/java/com/samhap/kokomen/interview/service/question/RootQuestionService.javasrc/main/resources/db/migration/V43__allow_guest_interview.sqlsrc/test/java/com/samhap/kokomen/global/fixture/interview/InterviewFixtureBuilder.javasrc/test/java/com/samhap/kokomen/interview/controller/InterviewControllerTest.javasrc/test/java/com/samhap/kokomen/interview/service/GuestInterviewServiceTest.javasrc/test/java/com/samhap/kokomen/interview/service/InterviewProceedFacadeServiceTest.javasrc/test/java/com/samhap/kokomen/interview/service/core/InterviewServiceTest.java
| @Column(name = "guest_ip", length = 45) | ||
| private String guestIp; |
There was a problem hiding this comment.
게스트 IP 평문 저장은 개인정보/보안 리스크가 큽니다.
guest_ip를 평문으로 영속화하고 비교키로 직접 사용하는 구조는 유출 시 영향이 큽니다. IP는 원문 대신 서버 비밀키 기반 해시(HMAC 등)로 저장/비교하는 방향이 안전합니다(도메인, 마이그레이션, 비교 로직 동반 수정 필요).
Also applies to: 133-137, 151-153
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/java/com/samhap/kokomen/interview/domain/Interview.java` around
lines 87 - 88, The guestIp field stores raw IPs which is a security/privacy
risk; change storage and comparison to a server-secret HMAC instead: replace
plain String guestIp persistence with a hashed field (e.g., guestIpHmac) and
compute HMAC using a server-side secret when persisting and when comparing in
domain methods, update affected methods that read/compare guestIp (search for
guestIp uses in Interview and related comparison logic) and add a migration to
transform existing plain IPs to HMACs (or nullify), plus ensure the secret is
injected via configuration and never logged; apply the same conversion for other
places in this class that currently persist raw IPs.
| ADD COLUMN guest_ip VARCHAR(45) NULL; | ||
|
|
||
| CREATE INDEX idx_interview_guest_ip ON interview (guest_ip); |
There was a problem hiding this comment.
guest_ip 원문 저장은 개인정보 보관 리스크가 큽니다.
IP를 평문으로 누적 저장/인덱싱하면 추적 가능성이 높아져 컴플라이언스 부담이 커집니다. 최소한 해시 저장(서버 시크릿 포함) 또는 보존기간 기반 삭제 정책을 같이 넣는 게 안전합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/resources/db/migration/V43__allow_guest_interview.sql` around lines
5 - 7, 현재 plain-text guest_ip 컬럼과 인덱스(idx_interview_guest_ip, table interview)는
개인정보 리스크가 있으니 평문 저장을 제거하거나 대체해야 합니다: 대신 guest_ip_hash 같은 컬럼을 추가하고(예:
VARCHAR(64)) IP를 서버 시크릿(salt)과 함께 안전한 해시(예: SHA-256)로 저장하도록 애플리케이션/마이그레이션을 변경하고,
기존 guest_ip 데이터를 변환해 해시로 마이그레이션한 뒤 원본 guest_ip 컬럼과 인덱스(idx_interview_guest_ip)를
제거하거나 NULL로 비우고 필요 시 해시 컬럼에 인덱스 생성하십시오; 또한 보존기간 기반 삭제 정책(예: interview 테이블에
guest_ip_hash 만 남기고 일정 기간 후 삭제)을 함께 설계해 구현하세요.
There was a problem hiding this comment.
Actionable comments posted: 1
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/samhap/kokomen/interview/service/core/InterviewService.java (1)
198-221:⚠️ Potential issue | 🟠 Major참고답안 조회 쿼리에서 게스트 인터뷰(member == null)를 명시적으로 제외해야 합니다.
AnswerRepository.findTopAnswersByRootQuestionAndRank()쿼리가 게스트 인터뷰를 필터링하지 않으므로, A/B 등급의 게스트 답변이 참고답안 후보에 포함될 수 있습니다.findRootQuestionReferenceAnswers()메서드의 247번 라인에서answer.getQuestion().getInterview().getMember().getNickname()을 바로 호출하는데, 게스트 인터뷰는 member가 null이므로 NPE가 발생합니다.SQL 쿼리에서
WHERE i.member IS NOT NULL조건을 추가하여 게스트 인터뷰를 제외해주세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/samhap/kokomen/interview/service/core/InterviewService.java` around lines 198 - 221, The query for reference answers currently includes guest interviews (member == null) which leads to NPE when code in getReferenceAnswers/findRootQuestionReferenceAnswers calls answer.getQuestion().getInterview().getMember().getNickname(); update the AnswerRepository query method findTopAnswersByRootQuestionAndRank (and any JPQL/SQL it uses) to add a WHERE i.member IS NOT NULL clause so guest interviews are excluded, then re-run tests; also review getReferenceAnswers to ensure it expects only non-null members after this change.
🤖 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/samhap/kokomen/interview/service/InterviewStartFacadeService.java`:
- Around line 75-92: The guest Redis lock is only released in the catch block so
if the transaction fails at commit the lock remains; after acquiring the lock in
startGuestInterview (createGuestInterviewStartedLockKey / acquireLockWithValue),
register a TransactionSynchronization (via
TransactionSynchronizationManager.registerSynchronization or equivalent) that in
afterCompletion checks the transaction status and calls
redisService.releaseLockSafely(lockKey, lockValue) when the transaction did NOT
commit (i.e., rollback or unknown), and keep the existing catch to handle
immediate runtime exceptions before the synchronization runs. Ensure the
synchronization is registered immediately after successful acquireLockWithValue
so the lock is always cleaned on non-commit outcomes.
---
Outside diff comments:
In
`@src/main/java/com/samhap/kokomen/interview/service/core/InterviewService.java`:
- Around line 198-221: The query for reference answers currently includes guest
interviews (member == null) which leads to NPE when code in
getReferenceAnswers/findRootQuestionReferenceAnswers calls
answer.getQuestion().getInterview().getMember().getNickname(); update the
AnswerRepository query method findTopAnswersByRootQuestionAndRank (and any
JPQL/SQL it uses) to add a WHERE i.member IS NOT NULL clause so guest interviews
are excluded, then re-run tests; also review getReferenceAnswers to ensure it
expects only non-null members after this change.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f1eff466-2890-43a3-9b36-2365e5ce1566
📒 Files selected for processing (6)
src/main/java/com/samhap/kokomen/interview/controller/InterviewController.javasrc/main/java/com/samhap/kokomen/interview/service/InterviewQueryService.javasrc/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.javasrc/main/java/com/samhap/kokomen/interview/service/core/InterviewService.javasrc/main/java/com/samhap/kokomen/interview/service/question/RootQuestionService.javasrc/test/java/com/samhap/kokomen/interview/controller/InterviewControllerTest.java
| @Transactional | ||
| public InterviewStartResponse startGuestInterview(ClientIp clientIp) { | ||
| String lockKey = createGuestInterviewStartedLockKey(clientIp); | ||
| String lockValue = UUID.randomUUID().toString(); | ||
| if (!redisService.acquireLockWithValue(lockKey, lockValue, GUEST_INTERVIEW_LOCK_TTL)) { | ||
| throw new BadRequestException("비회원 면접은 1회만 가능합니다."); | ||
| } | ||
| try { | ||
| RootQuestion rootQuestion = rootQuestionService.readRandomActiveRootQuestion(); | ||
| Interview interview = interviewService.saveInterview(Interview.forGuest(rootQuestion, | ||
| GUEST_INTERVIEW_MAX_QUESTION_COUNT, GUEST_INTERVIEW_MODE, clientIp)); | ||
| Question question = questionService.saveQuestion(new Question(interview, rootQuestion.getContent())); | ||
| return new InterviewStartTextModeResponse(interview, question); | ||
| } catch (RuntimeException e) { | ||
| redisService.releaseLockSafely(lockKey, lockValue); | ||
| throw e; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "InterviewStartFacadeService.java"Repository: samhap-soft/kokomen-backend
Length of output: 157
🏁 Script executed:
wc -l ./src/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.javaRepository: samhap-soft/kokomen-backend
Length of output: 161
🏁 Script executed:
cat -n ./src/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.java | sed -n '70,100p'Repository: samhap-soft/kokomen-backend
Length of output: 1879
🏁 Script executed:
rg -t java "releaseLockSafely|acquireLockWithValue" --max-count 5Repository: samhap-soft/kokomen-backend
Length of output: 1712
🏁 Script executed:
cat ./src/main/java/com/samhap/kokomen/global/service/RedisService.javaRepository: samhap-soft/kokomen-backend
Length of output: 3176
🏁 Script executed:
grep -n "GUEST_INTERVIEW_LOCK_TTL" ./src/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.javaRepository: samhap-soft/kokomen-backend
Length of output: 255
트랜잭션 커밋 실패 시 게스트 락이 영구히 남습니다.
현재 해제 로직이 catch (RuntimeException) 블록에만 있어서, 메서드 본문은 정상 종료됐지만 트랜잭션 커밋 단계에서 예외가 발생하는 경우를 놓칩니다. 그 경우 인터뷰 저장은 롤백되는데 Redis 락은 365일 유지되어 해당 IP가 다시 시작하지 못합니다. 실패 시 해제는 catch가 아니라 트랜잭션 완료 콜백에서 rollback/non-commit 케이스로 처리해 주세요.
예시 수정안
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
...
public InterviewStartResponse startGuestInterview(ClientIp clientIp) {
String lockKey = createGuestInterviewStartedLockKey(clientIp);
String lockValue = UUID.randomUUID().toString();
if (!redisService.acquireLockWithValue(lockKey, lockValue, GUEST_INTERVIEW_LOCK_TTL)) {
throw new BadRequestException("비회원 면접은 1회만 가능합니다.");
}
- try {
- RootQuestion rootQuestion = rootQuestionService.readRandomActiveRootQuestion();
- Interview interview = interviewService.saveInterview(Interview.forGuest(rootQuestion,
- GUEST_INTERVIEW_MAX_QUESTION_COUNT, GUEST_INTERVIEW_MODE, clientIp));
- Question question = questionService.saveQuestion(new Question(interview, rootQuestion.getContent()));
- return new InterviewStartTextModeResponse(interview, question);
- } catch (RuntimeException e) {
- redisService.releaseLockSafely(lockKey, lockValue);
- throw e;
- }
+ TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+ `@Override`
+ public void afterCompletion(int status) {
+ if (status != TransactionSynchronization.STATUS_COMMITTED) {
+ redisService.releaseLockSafely(lockKey, lockValue);
+ }
+ }
+ });
+
+ RootQuestion rootQuestion = rootQuestionService.readRandomActiveRootQuestion();
+ Interview interview = interviewService.saveInterview(Interview.forGuest(rootQuestion,
+ GUEST_INTERVIEW_MAX_QUESTION_COUNT, GUEST_INTERVIEW_MODE, clientIp));
+ Question question = questionService.saveQuestion(new Question(interview, rootQuestion.getContent()));
+ return new InterviewStartTextModeResponse(interview, question);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@src/main/java/com/samhap/kokomen/interview/service/InterviewStartFacadeService.java`
around lines 75 - 92, The guest Redis lock is only released in the catch block
so if the transaction fails at commit the lock remains; after acquiring the lock
in startGuestInterview (createGuestInterviewStartedLockKey /
acquireLockWithValue), register a TransactionSynchronization (via
TransactionSynchronizationManager.registerSynchronization or equivalent) that in
afterCompletion checks the transaction status and calls
redisService.releaseLockSafely(lockKey, lockValue) when the transaction did NOT
commit (i.e., rollback or unknown), and keep the existing catch to handle
immediate runtime exceptions before the synchronization runs. Ensure the
synchronization is registered immediately after successful acquireLockWithValue
so the lock is always cleaned on non-commit outcomes.
closed #
작업 내용
스크린샷
참고 사항