Skip to content

Commit 49a4cea

Browse files
authored
Merge pull request #289 from prgrms-aibe-devcourse/fix/dm
Fix: 비동기 오류 해결
2 parents 7d96fdb + 6e78365 commit 49a4cea

1 file changed

Lines changed: 64 additions & 9 deletions

File tree

src/main/java/kr/co/amateurs/server/service/alarm/SseService.java

Lines changed: 64 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import lombok.extern.slf4j.Slf4j;
88
import org.springframework.scheduling.annotation.Scheduled;
99
import org.springframework.stereotype.Service;
10+
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
1011
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
1112

1213
import java.io.IOException;
@@ -22,40 +23,65 @@ public class SseService {
2223
private final ConcurrentHashMap<Long, SseEmitter> connections = new ConcurrentHashMap<>();
2324
private final UserService userService;
2425

25-
@Scheduled(fixedRate = DEFAULT_HEARTBEAT_INTERVAL) // 30초
26+
@Scheduled(fixedRate = DEFAULT_HEARTBEAT_INTERVAL)
2627
public void sendHeartbeat() {
2728
if (connections.isEmpty()) {
2829
return;
2930
}
3031

31-
connections.forEach((userId, emitter) -> {
32+
connections.entrySet().removeIf(entry -> {
33+
Long userId = entry.getKey();
34+
SseEmitter emitter = entry.getValue();
35+
3236
try {
3337
emitter.send(SseEmitter.event()
3438
.name("heartbeat")
3539
.data("ping"));
40+
return false; // 정상 전송, 유지
41+
} catch (IOException e) {
42+
// 클라이언트 연결 종료 - 정상적인 상황이므로 조용히 처리
43+
closeEmitterSafely(emitter);
44+
return true; // 제거
3645
} catch (Exception e) {
37-
connections.remove(userId);
46+
// 예상치 못한 오류만 로깅
47+
log.warn("SSE 하트비트 전송 실패: userId={}, error={}", userId, e.getMessage());
48+
closeEmitterSafely(emitter);
49+
return true; // 제거
3850
}
3951
});
4052
}
4153

4254
public SseEmitter connect() {
4355
long currentUserId = userService.getCurrentLoginUser().getId();
44-
connections.remove(currentUserId);
56+
57+
// 기존 연결이 있다면 안전하게 정리
58+
SseEmitter existingEmitter = connections.remove(currentUserId);
59+
if (existingEmitter != null) {
60+
closeEmitterSafely(existingEmitter);
61+
}
4562

4663
SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT);
4764
connections.put(currentUserId, emitter);
4865

66+
// 연결 수명주기 이벤트 핸들러 등록
4967
emitter.onCompletion(() -> connections.remove(currentUserId));
5068
emitter.onTimeout(() -> connections.remove(currentUserId));
51-
emitter.onError(e -> connections.remove(currentUserId));
69+
emitter.onError(throwable -> {
70+
connections.remove(currentUserId);
71+
// 예상치 못한 오류만 로깅
72+
if (!isExpectedConnectionError(throwable)) {
73+
log.warn("SSE 연결 예상치 못한 오류: userId={}, error={}",
74+
currentUserId, throwable.getMessage());
75+
}
76+
});
5277

5378
try {
5479
emitter.send(SseEmitter.event()
5580
.name("connect")
5681
.data("Connected successfully"));
5782
} catch (IOException e) {
5883
connections.remove(currentUserId);
84+
log.error("SSE 초기 연결 메시지 전송 실패: userId={}", currentUserId, e);
5985
}
6086

6187
return emitter;
@@ -65,24 +91,53 @@ public void disconnect() {
6591
long currentUserId = userService.getCurrentLoginUser().getId();
6692
SseEmitter emitter = connections.remove(currentUserId);
6793
if (emitter != null) {
68-
emitter.complete();
94+
closeEmitterSafely(emitter);
6995
}
7096
}
7197

7298
public void sendAlarmToUser(long userId, Alarm alarm) {
7399
SseEmitter emitter = connections.get(userId);
74100
if (emitter == null) {
75-
return;
101+
return; // 연결 없음 - 조용히 무시
76102
}
77103

78104
try {
79105
AlarmDTO alarmDTO = AlarmDTO.from(alarm);
80106
emitter.send(SseEmitter.event()
81-
.id(alarm.getId())
107+
.id(String.valueOf(alarm.getId()))
82108
.name("alarm")
83109
.data(alarmDTO));
84110
} catch (IOException e) {
111+
// 클라이언트 연결 종료 - 정상적인 상황이므로 조용히 처리
112+
connections.remove(userId);
113+
closeEmitterSafely(emitter);
114+
} catch (Exception e) {
115+
// 예상치 못한 오류만 로깅
116+
log.warn("알람 전송 예상치 못한 오류: userId={}, alarmId={}, error={}",
117+
userId, alarm.getId(), e.getMessage());
85118
connections.remove(userId);
119+
closeEmitterSafely(emitter);
86120
}
87121
}
88-
}
122+
123+
/**
124+
* SseEmitter를 안전하게 종료하는 유틸리티 메서드
125+
*/
126+
private void closeEmitterSafely(SseEmitter emitter) {
127+
try {
128+
emitter.complete();
129+
} catch (Exception ignored) {
130+
// emitter 종료 중 예외는 무시 (이미 종료된 상태일 수 있음)
131+
}
132+
}
133+
134+
/**
135+
* 예상되는 연결 종료 오류인지 판단
136+
*/
137+
private boolean isExpectedConnectionError(Throwable throwable) {
138+
return throwable instanceof IOException ||
139+
throwable instanceof AsyncRequestNotUsableException ||
140+
(throwable.getMessage() != null &&
141+
throwable.getMessage().contains("Broken pipe"));
142+
}
143+
}

0 commit comments

Comments
 (0)