Skip to content

[박하민] sprint12#124

Open
parkhamin wants to merge 16 commits into
codeit-bootcamp-spring:박하민from
parkhamin:박하민-sprint12

Hidden character warning

The head ref may contain hidden characters: "\ubc15\ud558\ubbfc-sprint12"
Open

[박하민] sprint12#124
parkhamin wants to merge 16 commits into
codeit-bootcamp-spring:박하민from
parkhamin:박하민-sprint12

Conversation

@parkhamin
Copy link
Copy Markdown
Collaborator

@parkhamin parkhamin commented May 3, 2026

요구사항

기본

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

심화

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

주요 변경사항

InMemoryJwtRegistry의 한계점: 분산 환경에서 사용자가 Backend-1에서 로그인하여 JWT를 발급받아도,
다음 요청이 Nginx 로드밸런서에 의해 Backend-2로 전달되면 해당 서버에는 인증 정보가 없어 "로그아웃"된 것으로 처리될 수 있다.
또한, 서버가 업데이트되거나 장애로 인해 재시작될 경우, 메모리에 저장된 모든 토큰 정보가 삭제되어 모든 사용자가 강제로 로그아웃 처리된다.

분산환경에 따른 웹소켓과 SSE의 한계점: 사용자가 Backend-1에 연결돼 있다면 Backend-2에서 발생한 이벤트를 Backend-1로 전달 불가능하다.
서버별로 메시지 큐가 따로 동작해서, 서버에 따라 유저별 메시지 "누락" 혹은 "중복 전송" 문제 야기할 수 있다.

스크린샷

스크린샷 2026-04-29 111533 - `localhost:3000` 접속 스크린샷 2026-05-03 191855 - 앱 인스턴스 3개 확인

멘토에게

  • 분산 환경에 따라 redis, kafka를 활용해 리팩토링하는 과정이 좀 어려웠고 잘 적용되었는지 모르겠습니다....
  • 심화 요구사항 반영 후에 권한 문제가 뜨고 있는데 계속해서 디버깅하여 수정해보겠습니다!!

@parkhamin parkhamin requested a review from Highjune May 3, 2026 15:23
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.

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로 어떻게 개선할 수 있는지 한번 고민해봐주세요~!

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 제한이 깨져버려요

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

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.

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

public class SseMessageRepository {

// 메시지의 순서를 담고 있는 큐
private final ConcurrentLinkedDeque<UUID> eventIdQueue = new ConcurrentLinkedDeque<>();
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.

지금 여기에 삭제 로직이 없어요. 계속 쌓이게 되면 OOM 터지게 되겠죠?
최대 크기 제한이 필요해 보여요

@RequiredArgsConstructor
public class WebSocketRequiredEventListener {

private final SimpMessagingTemplate simpMessagingTemplate;
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.

이것도 다중서버에서는 처리 같이 못하는 것 아닌가요?

로컬 이벤트만 처리하기 때문에, 메시지가 만약 app2 에서 생성되면 app1, app3 에 연결된 웹소켓 클라이언트는 못 받는 것 같은데요. 이것도 sse 처럼 kafka 경유해야 할 것 같아요


@GetMapping(value = "/api/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(
@AuthenticationPrincipal DiscodeitUserDetails userDetails,
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.

이거 npe 가능할 것 같아요. 왜냐하면
SecurityConfig클래스에 여기에 대한 권한 permitALl 이잖아요?

그래서 userDetails 가 null 이면 밑에 UUID receiverId = userDetails.getUserDto().id(); 에서 npe 가능할 것 같은데요?!

redisTemplate.opsForSet().add(REFRESH_TOKEN_INDEX_KEY, refreshToken);

// 인덱스 키에도 만료 시간 설정 (메모리 누수 방지)
redisTemplate.expire(ACCESS_TOKEN_INDEX_KEY, DEFAULT_TTL);
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분간 새 로그인이 없으면 set 전체가 만료되서 모든 기존 유저 인증이 깨지게 될 것 가 ㅌ은데요. 개별 토큰별 만료를 관리하거나 set ttl 을 훨씬 길게 잡아야 할 것 같아요

}
} catch (Exception e) {
throw new DiscodeitException(ErrorCode.INVALID_TOKEN);
//throw new DiscodeitException(ErrorCode.INVALID_TOKEN);
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.

이거 주석 풀어야 하지 않을까요?
그냥 예외 삼켜버리면 디버깅이 안되서 log.warn 정도는 남겨주세요


public List<SseEmitter> findAllByReceiverId(UUID receiverId) {
// null 방지를 위해 default 명시
return data.getOrDefault(receiverId, new ArrayList<>());
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.

위에서 저장은 new CopyOnWriteArrayList로 하는데, 여기에서는 arrayList 를 반환하네요.
실제로 저장된 리스트와 다른 인스턴스이므로 정리가 안될 것 같아요

event.binaryContentDto(), null);
break;

case MESSAGE_FILE:
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.

스위치 문에서 default 는 없을까요?

public void onUserLogInOut(String kafkaEvent) throws JsonProcessingException {
UserLogInOutEvent event = objectMapper.readValue(kafkaEvent, UserLogInOutEvent.class);

User user = userRepository.findById(event.userId())
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.

여기에서 에러나면 메시지 재처리 반복 계속 되지 않나요? try catch 로 감싸고 로그만 남기는 것이 어떨까요

지난번 리뷰에도 비슷한 것 있었던 것 같은데, 메시지 처리할떄(재처리 기능도 있을떄) 는 에러 던지는 것 유의하셔야 해요. 좀비처럼 계속 돌고든여

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