Skip to content

[현승원] sprint12#125

Open
seungwon00 wants to merge 16 commits into
codeit-bootcamp-spring:현승원from
seungwon00:현승원-sprint12

Hidden character warning

The head ref may contain hidden characters: "\ud604\uc2b9\uc6d0-sprint12"
Open

[현승원] sprint12#125
seungwon00 wants to merge 16 commits into
codeit-bootcamp-spring:현승원from
seungwon00:현승원-sprint12

Conversation

@seungwon00
Copy link
Copy Markdown
Collaborator

@seungwon00 seungwon00 commented May 3, 2026

요구사항

기본

  • 웹소켓 구현하기
  • SSE 구현하기
  • 배포 아키텍처 구성하

심화

  • 웹소켓 인증/인가 처리하기
  • 분산 환경 배포 아키텍처 구성하기

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@seungwon00 seungwon00 requested a review from Highjune May 3, 2026 15:58
Copy link
Copy Markdown
Collaborator

@Highjune Highjune left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승원님 고생하셨습니다. 이번 구현 어렵지만 너무 잘 해주셨구요. 마지막 구현에 대한 아키텍쳐 가볍게 그려보는 것 추천드립니다.

질문 몇 가지만 남겨놓을게요~!


카프카가 현재 2가지 역할 하잖아요?
역할 A: 알림 저장 등 1번만 처리 (고정 groupId)
역할 B: SSE 이벤트를 모든 인스턴스에 브로드캐스트 (고유 groupId)

Q1) 위에서 redis pub/sub 으로 대체할 수 있는 영역이랑 없는 영역은? 바꿀 수 있다면 장/단점은?


현재 SseMessageRepository는 JVM 메모리(ConcurrentHashMap)에 이벤트를 저장하고 있잖아요.

Q2) 분산 환경(인스턴스 3개)에서 이 구현이 실패하는 시나리오를 설명하고, Redis로 어떻게 개선할 수 있는지 한번 고민해봐주세요~!

@@ -0,0 +1,7 @@
# IP Hash — 클라이언트 IP 기준으로 동일 백엔드에 고정 (세션/WS에 유리)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.conf 파일들을 다 삭제해주세요

public void releaseLock(String key) {
String lockKey = LOCK_KEY_PREFIX + key;
try {
redisTemplate.delete(lockKey);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 누구거든 그냥 삭제하고 있잖아요?

보통 분산 락 구현시에는 값이 내 것일때만 삭제하게 하는 로직이 있거든요.(app1, 2, 3 구분 필요)
lock:유저A 일 때 무조건 샂게하는데, 값이 app1 이든 app2 이든 상관없이 삭제해요.

케이스로 보면,

  • App2 가 정당하게 가지고 있는 락을 app1 이 지워버리거든요.
  • app2 는 락을 가지고 있다고 생각하지만 실제로는 없고
  • app3 이 락을 새로 획득 가능해요
  • app2 와 app3 이 동시에 같은 유저의 jwt 를 조작하게 됩니다.
  • 즉 maxActiveJwtCount 제한이 깨져버려요

'내 것만 삭제' 하게 로직 넣는 것이 좋을 것 같아요.

그리고 이미 락 만들때 고유한 키 값 만들고 계시네요.
그것으로 relsaeLock 떄도 사용하시면 됩니다. lockvalue

if (lastEventId != null) {
log.debug("SSE 연결, receiverId={}, lastEventId={}", receiverId, lastEventId);
}
SseEmitter emitter = new SseEmitter(EMITTER_TIMEOUT_MS);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타임아웃이 0으로, 즉 없는 것이 맞을까요? 클라이언트가 브라우저를 닫지 않고 방치하면 Emitter가 계~속 메모리에 남을 수 있어요

@Repository
public class SseMessageRepository {

private static final int MAX_MESSAGES = 2000;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max message 설정 잘하셨습니다. oom 방지를 할 수 있거든요!

private static final String USER_JWT_KEY_PREFIX = "jwt:user:";
private static final String ACCESS_TOKEN_INDEX_KEY = "jwt:access_tokens";
private static final String REFRESH_TOKEN_INDEX_KEY = "jwt:refresh_tokens";
private static final Duration DEFAULT_TTL = Duration.ofMinutes(30);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모든 유저의 토큰이 하나의 set 인데 30분 ttl -> 30분간 로그인 없으면 전체 인증이 깨지는 것 아닌가요?

return;
}

for (String userKey : Optional.ofNullable(redisTemplate.keys(USER_JWT_KEY_PREFIX + "*")).orElse(Set.of())) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KEYS * 사용하게 되면 redis 를 블로킹하거든요. 만약 유저수가 많아서 조회가 많아지게 되면, 즉 유저 수가 많으면 성능 문제가 있을 수도 있어요. SCAN 명령을 사용하거나, refreshToken → userId 역인덱스 추가를 추천드려요!

@Bean
public ConsumerFactory<String, String> realtimePushConsumerFactory(
KafkaProperties kafkaProperties,
@Value("${discodeit.kafka.realtime-push-group-id}") String groupId
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

카프카에서 groupId 가 동일하거나 같으면 어떤 특징들이 각각 있는건가요?

설정 파일에서 groupId를 주입받는데, 모든 인스턴스가 같은 설정 파일을 쓰면 같은 groupId → 1개 인스턴스만 수신 → 분산 환경에서 실패하게 됩니다.

위에 주석에 추가하신 것처럼, '푸시는 인스턴스마다 수신해야 하므로' -> docker-compose 에서 인스턴스별로 다른 값을 주입받거나, uuid 기반 자동 생성이 필요해요.

uuid 는 예시입니다.

event.messageId());
} catch (JsonProcessingException e) {
log.error("웹소켓 Kafka 페이로드 직렬화 실패, messageId={}", event.messageId(), e);
messagingTemplate.convertAndSend(destination, dto);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kafka 발행이 실패했을 때, 로컬 전송(호출) 하잖아요?

그러면 현재 인스턴스에 연결된 클라이언트만 받고 다른 인스턴스들의 클라이어늩들은 못 받게 되는데요. 에러 처리가 부분 성공 되어서 디버깅도 어려워질 수 잇어요.

부분 성공보다는 아예 깨끗한 실패가 나을 수도 있어요. 로컬 전송 말고 그냥 에러 로그만 남기는 것이 더 좋아보입니다.
메시지는 이미 디비에 저장되어 있어서 유저에게 다시 보여줄 수 있잖아요.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로, JsonProcessingException은 사실 개발 버그이잖아요. 네트워크 문제도 아니고, 코드에 버그가 있는 건데요. 재시도해도 계속 실패하는 것이라서, 진짜 kafka 장애(네트워크 끊긴다거나) 는 다른 예외가 나요.

KafkaException, TimeoutException 등등 이건 retry가 의미가 잇어요

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.

2 participants