[박하민] sprint12#124
Hidden character warning
Conversation
- WebSocketConfig 구현 - MessageWebSocketController 구현 - WebSocketRequiredEventListener 구현
- SseMessage - SseEmitterRepository - SseMessageRepository
- 알림 이벤트 전송 - 파일 업로드 상태 변경 이벤트 전송 - 채널 갱신 이벤트 전송 - 사용자 갱신 이벤트 전송
- Dockerfile 버전 변경 - docker-compose 배포 아키텍쳐 통합
- 트랜잭션 제거 - 관리자 알림 이벤트 발행 - 예외 제거하여 무한 루프 방지
Highjune
left a comment
There was a problem hiding this comment.
하민님 수고하셨어요! 이번 구현 레벨 어려웠는데 잘 이해하신 것 같고, 머리에 한 번에 안 들어오면 코드 보시면서 복습 많이 하세요. 그리고 이번 구현까지가 마지막인 것으로 알고 있는데, 마지막 구현을 보니 이 부분들에 대해서는 가볍게 아키텍쳐 그려보는 것 추천드려요. 그러면 문제점들도 더 보일 수 있습니다.
질문 몇 가지 해볼게요. 남겨주세요. 이번 구현은 되게 중요한 부분이었어요.
카프카가 현재 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); |
There was a problem hiding this comment.
현재 누구거든 그냥 삭제하고 있잖아요?
보통 분산 락 구현시에는 값이 내 것일때만 삭제하게 하는 로직이 있거든요.(app1, 2, 3 구분 필요)
lock:유저A 일 때 무조건 샂게하는데, 값이 app1 이든 app2 이든 상관없이 삭제해요.
케이스로 보면,
- App2 가 정당하게 가지고 있는 락을 app1 이 지워버리거든요.
- app2 는 락을 가지고 있다고 생각하지만 실제로는 없고
- app3 이 락을 새로 획득 가능해요
- app2 와 app3 이 동시에 같은 유저의 jwt 를 조작하게 됩니다.
- 즉 maxActiveJwtCount 제한이 깨져버려요
'내 것만 삭제' 하게 로직 넣는 것이 좋을 것 같아요.
There was a problem hiding this comment.
그리고 이미 락 만들때 고유한 키 값 만들고 계시네요.
그것으로 relsaeLock 떄도 사용하시면 됩니다. lockvalue
| public class SseMessageRepository { | ||
|
|
||
| // 메시지의 순서를 담고 있는 큐 | ||
| private final ConcurrentLinkedDeque<UUID> eventIdQueue = new ConcurrentLinkedDeque<>(); |
There was a problem hiding this comment.
지금 여기에 삭제 로직이 없어요. 계속 쌓이게 되면 OOM 터지게 되겠죠?
최대 크기 제한이 필요해 보여요
| @RequiredArgsConstructor | ||
| public class WebSocketRequiredEventListener { | ||
|
|
||
| private final SimpMessagingTemplate simpMessagingTemplate; |
There was a problem hiding this comment.
이것도 다중서버에서는 처리 같이 못하는 것 아닌가요?
로컬 이벤트만 처리하기 때문에, 메시지가 만약 app2 에서 생성되면 app1, app3 에 연결된 웹소켓 클라이언트는 못 받는 것 같은데요. 이것도 sse 처럼 kafka 경유해야 할 것 같아요
|
|
||
| @GetMapping(value = "/api/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) | ||
| public SseEmitter connect( | ||
| @AuthenticationPrincipal DiscodeitUserDetails userDetails, |
There was a problem hiding this comment.
이거 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); |
There was a problem hiding this comment.
현재 모든 유저의 토큰이 하나의 set 안에 있는거죠? 30분간 새 로그인이 없으면 set 전체가 만료되서 모든 기존 유저 인증이 깨지게 될 것 가 ㅌ은데요. 개별 토큰별 만료를 관리하거나 set ttl 을 훨씬 길게 잡아야 할 것 같아요
| } | ||
| } catch (Exception e) { | ||
| throw new DiscodeitException(ErrorCode.INVALID_TOKEN); | ||
| //throw new DiscodeitException(ErrorCode.INVALID_TOKEN); |
There was a problem hiding this comment.
이거 주석 풀어야 하지 않을까요?
그냥 예외 삼켜버리면 디버깅이 안되서 log.warn 정도는 남겨주세요
|
|
||
| public List<SseEmitter> findAllByReceiverId(UUID receiverId) { | ||
| // null 방지를 위해 default 명시 | ||
| return data.getOrDefault(receiverId, new ArrayList<>()); |
There was a problem hiding this comment.
위에서 저장은 new CopyOnWriteArrayList로 하는데, 여기에서는 arrayList 를 반환하네요.
실제로 저장된 리스트와 다른 인스턴스이므로 정리가 안될 것 같아요
| event.binaryContentDto(), null); | ||
| break; | ||
|
|
||
| case MESSAGE_FILE: |
| public void onUserLogInOut(String kafkaEvent) throws JsonProcessingException { | ||
| UserLogInOutEvent event = objectMapper.readValue(kafkaEvent, UserLogInOutEvent.class); | ||
|
|
||
| User user = userRepository.findById(event.userId()) |
There was a problem hiding this comment.
여기에서 에러나면 메시지 재처리 반복 계속 되지 않나요? try catch 로 감싸고 로그만 남기는 것이 어떨까요
지난번 리뷰에도 비슷한 것 있었던 것 같은데, 메시지 처리할떄(재처리 기능도 있을떄) 는 에러 던지는 것 유의하셔야 해요. 좀비처럼 계속 돌고든여
요구사항
기본
심화
주요 변경사항
스크린샷
멘토에게