2024-04-29T12:10:45.012Z INFO 11975 --- [nawabali] [ionShutdownHook] o.s.c.support.DefaultLifecycleProcessor : Shutdown phase 2147481599 ends with 1 bean still running after timeout of 30000ms: [webServerStartStop]
에러 단계가 INFO여서 문제가 되지는 않았지만 예상치 못한 작업이나 리소스 누출로 인해 발생할 수도 있는 에러를 예방하는 차원에서 추가 수정을 했다.
이 로그는 스프링 애플리케이션이 종료될 때 남아 있는 빈이 있는 경우에 발생한다. 보통은 애플리케이션이 종료되는 동안에도 여전히 실행 중인 작업이나 백그라운드 스레드 등이 있는 경우에 이러한 경고가 표시됩니다.
종료가 된 후에도 hearbeat로 보내는 시간(30000ms)이 남아있어서 경고가 표시됐던것 같다.
연결이 끊어진 클라이언트의 SseEmitter를 emitters 맵에서 제거하는 부분에서 문제가 발생할 수 있어서 수정을 했다.
일반적으로 Java에서는 컬렉션을 반복하면서 수정하는 것은 안전하지 않고 클라이언트의 연결이 끊어졌을 때 이를 안전하게 제거해야 한다.그래서 Iterator를 이용하기로 했다.
Iterator를 사용하면 컬렉션을 반복하면서 안전하게 제거할 수 있고 ConcurrentModificationException을 방지하는 데 도움이 된다.
@Scheduled(fixedRate = 30000)
public void sendHeartbeat() {
Iterator<Map.Entry<Long, SseEmitter>> iterator = emitters.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, SseEmitter> entry = iterator.next();
Long userId = entry.getKey();
SseEmitter emitter = entry.getValue();
try {
emitter.send(SseEmitter.event().comment("heartbeat"));
log.info("하트비트 보내기");
} catch (IOException e) {
emitter.completeWithError(e);
iterator.remove(); // 안전하게 제거
log.info("연결이 끊어져 삭제됨: userId = {}", userId);
}
}
}