Feat: 비활성 채팅방 자동 종료 스케쥴러 구현#114
Conversation
ChatInactivityScheduler 추가 - 1분 주기로 비활성 채팅방 감지 및 자동 종료 처리 - 기준 시간(inactivity-timeout-minutes) application.yml로 외부화 - 종료 대상: lastMessageAt 기준 설정 시간 경과 + WAITING/IN_PROGRESS 상태 - 개별 try-catch 적용으로 단일 방 처리 실패 시 나머지 방 계속 처리 - TODO: 분산락 적용 예정 (현재 비관적 락 + 상태 재확인으로 1차 방어) autoCloseRoom() 추가 - 스케쥴러 전용 종료 메서드 분리 (closeChatRoom과 역할 분리) - findByIdForUpdate() 비관적 락으로 동시 상태 변경 방어 - 이미 COMPLETED 상태면 스킵 (분산 환경 중복 실행 방어) - 시스템 메시지 DB 저장 후 Redis Pub/Sub으로 클라이언트에 실시간 알림 findInactiveRooms() QueryDSL로 구현 - JPQL 전체 패키지명 방식 제거 → ChatRoomRepositoryCustomImpl에 QueryDSL로 통합 - chatRoom.chatRoomStatus.in(WAITING, IN_PROGRESS) + lastMessageAt.before(cutoff) - 기존 ChatRoomRepositoryCustom 구조에 맞춰 일관성 유지
🤖 AI 코드리뷰
🟡 개선 제안1. 컴파일 에러 및 로깅 포맷 오류 (치명적) log.info("[자동종료] 처리 대상 {}건");인자를 1개 요구하고 있는데 아무것도 안 넘겼습니다. 빌드가 안 되거나 런타임에 2. 매직 스트링 하드코딩 3. 로깅 순서로 인한 데이터 정합성 오류 (상태 변경 후 로깅) chatRoom.changeStatus(ChatRoomStatus.COMPLETED);
// ...
log.info("[자동종료] 완료 roomId: {}, 이전 상태: {}", roomId, chatRoom.getChatRoomStatus());이전 상태를 로깅하고 싶다면, 상태를 변경하기 전에 로컬 변수에 담아두고 로깅해야 합니다. 현재 코드는 상태를 이미 🔴 보안 / 성능 주의1. 무페이징 대량 조회 (Full Table Scan 위험) List<ChatRoom> findInactiveRooms(LocalDateTime cutoff)
💡 학습 포인트분산 환경에서의 스케쥴러 동시성 제어 🤔 생각해보기이 스케쥴러가 1분 주기로 돌고 있고,
|
ChatInactivityScheduler 추가 - 1분 주기로 비활성 채팅방 감지 및 자동 종료 처리 - 기준 시간(inactivity-timeout-minutes) application.yml로 외부화 - 종료 대상: lastMessageAt 기준 설정 시간 경과 + WAITING/IN_PROGRESS 상태 - 개별 try-catch 적용으로 단일 방 처리 실패 시 나머지 방 계속 처리 - TODO: 분산락 적용 예정 (현재 비관적 락 + 상태 재확인으로 1차 방어) autoCloseRoom() 추가 - 스케쥴러 전용 종료 메서드 분리 (closeChatRoom과 역할 분리) - findByIdForUpdate() 비관적 락으로 동시 상태 변경 방어 - 이미 COMPLETED 상태면 스킵 (분산 환경 중복 실행 방어) - 시스템 메시지 DB 저장 후 Redis Pub/Sub으로 클라이언트에 실시간 알림 findInactiveRooms() QueryDSL로 구현 - JPQL 전체 패키지명 방식 제거 → ChatRoomRepositoryCustomImpl에 QueryDSL로 통합 - chatRoom.chatRoomStatus.in(WAITING, IN_PROGRESS) + lastMessageAt.before(cutoff) - 기존 ChatRoomRepositoryCustom 구조에 맞춰 일관성 유지
🤖 AI 코드리뷰
📋 과제 요구사항 준수 여부해당 PR은 도전 기능 중 실시간 채팅(비활성 채팅방 자동 종료) 확장 기능에 해당합니다.
🟢 잘된 점1. 이전 피드백(치명적 오류) 완벽하게 반영 2. 심도 있는 동시성 제어 설계 3. 장애 격리(Fault Isolation) 처리 🔴 필수 수정특이사항 없음. 이전에 지적된 컴파일 에러 및 로직 오류가 모두 해결되었습니다. 🟡 권장 개선1. 여전히 잠재적인 OOM(Out Of Memory) 위험 존재 해결 방법 (ID 페이징 / No-Offset 방식 도입) // ChatRoomRepositoryCustom.java
public interface ChatRoomRepositoryCustom {
- List<ChatRoom> findInactiveRooms(LocalDateTime cutoff);
+ List<ChatRoom> findInactiveRooms(LocalDateTime cutoff, Long lastId, int batchSize);
}
// ChatRoomRepositoryCustomImpl.java
+import com.querydsl.core.types.dsl.BooleanExpression;
public class ChatRoomRepositoryCustomImpl implements ChatRoomRepositoryCustom {
@Override
- public List<ChatRoom> findInactiveRooms(LocalDateTime cutoff) {
+ public List<ChatRoom> findInactiveRooms(LocalDateTime cutoff, Long lastId, int batchSize) {
return queryFactory
.selectFrom(chatRoom)
.where(
+ gtRoomId(lastId),
chatRoom.chatRoomStatus.in(
ChatRoomStatus.WAITING,
ChatRoomStatus.IN_PROGRESS
),
chatRoom.lastMessageAt.before(cutoff)
- ).fetch();
+ )
+ .orderBy(chatRoom.id.asc())
+ .limit(batchSize)
+ .fetch();
}
+ private BooleanExpression gtRoomId(Long lastId) {
+ return lastId != null ? chatRoom.id.gt(lastId) : null;
+ }
}
// ChatInactivityScheduler.java
-List<ChatRoom> targets = chatRoomRepository.findInactiveRooms(cutOff);
-// ... for 루프 ...💡 학습 포인트스케쥴러의 대용량 데이터 처리 패턴 (ID 페이징) 🤔 생각해보기현재 분산락(Lettuce)을 적용할 때
🔄 이전 피드백 반영 여부
|
🤖 AI 코드리뷰
🔄 이전 피드백 반영 여부
🟡 개선 제안1. 매직 리터럴 상수화 미비 2. 대량 트랜잭션 병목 (N+1 쓰기 문제) 🔴 보안 / 성능 주의1. 락(Lock) 획득 실패 시 무한 대기 가능성 (장애 전파 위험) if (!redisLockRepository.tryLock(WRITE_BACK_LOCK_KEY, lockValue, WRITE_BACK_LOCK_TTL)) {
// ...
}
🤔 생각해보기현재 비활성 채팅방을 종료할 때,
|
🤖 AI 코드리뷰
📋 과제 요구사항 준수 여부
🟢 잘된 점1. 이전 피드백 완벽 반영 및 아키텍처 대폭 개선 2. 완벽한 장애 격리(Fault Isolation)와 graceful degradation 3. KeywordScheduler의 분산락 도입 🔴 필수 수정특이사항 없음. 이전에 지적된 필수 수정 사항 및 런타임 오류 위험이 모두 해결되었습니다. 🟡 권장 개선1. 2. Bulk Update 시 영속성 컨텍스트(Persistence Context) 불일치 주의 💡 학습 포인트
🤔 생각해보기현재
🔄 이전 피드백 반영 여부
|
🤖 AI 코드리뷰
🔄 이전 피드백 반영 여부
🟡 개선 제안1. 2. Bulk Update와 동시성 제어의 모순 💡 학습 포인트
🤔 생각해보기현재 구조는 비활성 채팅방을 배치 사이즈(100개) 단위로 묶어 처리합니다. 만약 이 배치 단위의 처리(조회 -> 상태 변경 -> 메시지 저장)를 하나의 트랜잭션으로 묶는다면, 10만 개의 방을 처리할 때 DB에는 어떤 형태의 락(Lock)이 어떻게 걸리게 될까요? 이 구조가 대규모 트래픽 환경에서 다른 비즈니스 로직에 어떤 연쇄적인 장애를 유발할 수 있을지 고민해 보시기 바랍니다.
|
💡 개요
🛠️ 작업 내용
ChatInactivityScheduler 추가
autoCloseRoom() 추가
findInactiveRooms() QueryDSL로 구현
JPQL 전체 패키지명 방식 제거 → ChatRoomRepositoryCustomImpl에 QueryDSL로 통합
chatRoom.chatRoomStatus.in(WAITING, IN_PROGRESS) + lastMessageAt.before(cutoff)
기존 ChatRoomRepositoryCustom 구조에 맞춰 일관성 유지
Closes: [Feat] 실시간 채팅 자동 종료 스케쥴러 #113