기술면접 (5) - 코드기반

ssongyi·2025년 7월 12일
0

Java/Spring TIL

목록 보기
27/27

전체 흐름 → 구체 처리 → 이유/근거 형식

✅ 기능 흐름 관련 질문

1. 채팅방에 유저가 입장할 때 어떤 흐름으로 동작하나요?

유저가 채팅방에 입장하면, 서버는 WebSocket 핸드셰이크 단계에서 JWT를 검증하고 사용자 정보를 WebSocketPrincipal로 세션에 매핑합니다. 이후 REST API를 통해 채팅방 멤버로 등록되며, SimpMessagingTemplate을 통해 /sub/chat/rooms/{roomId}/members 채널로 실시간 멤버 목록이 브로드캐스트됩니다.

2. 채팅 메시지를 보낼 때 서버에서 어떤 처리를 하나요?

클라이언트가 /pub/chat/message로 메시지를 보내면, 서버는 사용자 인증 및 채팅방 참여 여부를 검증한 후 메시지를 DB에 저장합니다. 저장된 메시지는 클라이언트에 WebSocket을 통해 /sub/chat/rooms/{roomId}/messages 채널로 전달됩니다.

3. 채팅방에서 나갈 때 서버는 어떤 작업을 수행하나요?

사용자의 ChatParticipant를 비활성화 처리하고, 마지막 참여자라면 채팅방과 메시지를 삭제합니다. 이후 트랜잭션 커밋 후 chatMemberEventService를 통해 남은 멤버 목록을 브로드캐스트합니다.

4. 누군가 초대되거나 강퇴될 때 클라이언트가 어떻게 실시간으로 알 수 있나요?

초대 또는 강퇴 시 서버는 broadcastMembers()를 호출하여 /sub/chat/rooms/{roomId}/members채널로 실시간으로 멤버 목록을 전송합니다. 클라이언트는 이 채널을 구독하고 있어 자동으로 최신 멤버 정보를 받게 됩니다.

5. 채팅 메시지를 읽었다는 상태는 어떻게 동기화되나요?

사용자가 메시지를 읽으면 lastReadMessageId가 업데이트되고, ChatReadService에서 메시지 ID 기준으로만 상향 갱신되도록 처리합니다. 이를 통해 읽음 동기화 및 서버 부하 방지를 동시에 고려했습니다.

6. 채팅방이 비어 있으면 자동으로 삭제된다던데, 그 흐름을 설명해주세요.

마지막 사용자가 leaveChatRoom()을 호출하면 ChatParticipant가 비활성화되고, 남은 인원이 0명이면 채팅방과 메시지를 삭제합니다. 이 삭제는 트랜잭션 내에서 일어나며, 이후 브로드캐스트로 멤버 정보가 업데이트됩니다.

7. 웹소켓 연결과 메시지 전송 간 차이점이나 역할 분담은 어떻게 되나요?

WebSocket 연결은 세션 유지 및 인증된 사용자 식별을 담당하고, 메시지 전송은 STOMP 프레임을 통해 특정 채널로 전송되는 구조입니다. 연결은 HandshakeInterceptor와 CONNECT 프레임 검증으로 이루어지고, 메시지는 인증된 사용자의 요청만 허용됩니다.


✅ 기술 선택 이유

8. JWT 인증을 WebSocket에 어떻게 연동했나요?

WebSocket 연결 시 HandshakeInterceptorDefaultHandshakeHandler에서 JWT를 검증하여 사용자 정보를 WebSocketPrincipal로 설정합니다. 이후 STOMP CONNECT 프레임에서도 추가로 토큰을 검증해 보안성을 강화했습니다.

9. 왜 SimpMessagingTemplate을 사용했나요?

WebSocket을 통해 특정 채널로 메시지를 전송하거나 브로드캐스트하기 위해 사용했습니다. 특히 채팅방 멤버 변경이나 메시지 도착 시 클라이언트에 실시간 반영하기에 적합합니다.


✅ 보안 및 인증 관련 질문

10. WebSocket에서는 JWT를 어떻게 검증하나요?

1차는 HandshakeInterceptor에서, 2차는 StompCommand.CONNECT 프레임에서 토큰을 추출하여 JwtUtil로 검증합니다. 두 단계에서 실패하면 연결 자체를 차단합니다.

11. CONNECT 프레임에서 토큰을 검증하는 이유는 무엇인가요?

SockJS fallback의 경우 핸드셰이크 단계에서 JWT를 전달하지 못할 수 있기 때문에 CONNECT 프레임에서도 별도로 토큰을 검증해 보안 이슈를 방지합니다.

12. 악의적인 사용자가 임의로 메시지를 보내는 걸 막기 위한 조치는 무엇인가요?

메시지를 보내기 전에 서버는 JWT 검증은 물론 해당 사용자가 채팅방의 활성화된 참여자인지도 확인합니다. 조건을 만족하지 않으면 USER_NOT_IN_CHAT_ROOM 예외를 던집니다.


✅ 설계 및 구조 관련 질문

13. CQRS 구조를 왜 적용했나요? 어떤 장점이 있었나요?

채팅은 읽기와 쓰기가 빈번히 분리되는 구조이므로, 명확한 책임 분리와 성능 최적화를 위해 CQRS를 적용했습니다. 유지보수성과 테스트 용이성이 크게 향상되었습니다.

14. Command와 Query를 실제로 어떻게 나눴나요?

ChatRoomCommandServiceChatRoomQueryService로 나누어, 쓰기 작업은 Command, 조회 작업은 Query에 명확히 분리했습니다. 퍼사드 레이어(ChatRoomServiceFacade)를 통해 외부에 일관된 API를 제공합니다.

15. 트랜잭션 후 브로드캐스트는 어떻게 구현했나요? 그 이유는?

TransactionSynchronizationManager.registerSynchronization()을 활용해 트랜잭션 커밋 이후에 broadcastMembers()가 실행되도록 했습니다. 트랜잭션이 롤백될 경우 잘못된 데이터가 브로드캐스트되는 것을 방지하기 위함입니다.

16. 서비스 클래스를 세분화한 기준은 무엇인가요?

도메인 책임 단위로 나누었습니다. 예: 메시지 처리, 읽음 처리, 멤버 관리 등을 각기 ChatMessageService, ChatReadService, ChatMemberEventService 등으로 나누어 SRP 원칙을 지켰습니다.

17. 퍼사드(ChatRoomServiceFacade)의 역할은 무엇인가요?

Command/Query로 나뉜 내부 구조를 외부에 하나의 API처럼 제공하고, 트랜잭션 경계를 명확히 설정하는 역할을 합니다. 이로 인해 컨트롤러에서는 구현 세부를 몰라도 됩니다.


✅ 예외 처리 / 경계 상황

18. 메시지 삭제 제한 시간(5분)은 어디에서 검증하나요?

ChatMessageService.deleteMessageByRoom()에서 삭제 요청 시 현재 시간과 sentAt을 비교하여 5분 이상 지난 메시지는 MESSAGE_DELETE_TIME_EXPIRED 예외를 던집니다.

19. 유저가 채팅방에 없는 경우 어떤 예외가 발생하나요?

ChatValidator.validateParticipant()를 통해 USER_NOT_IN_CHAT_ROOM 예외가 발생합니다. 이를 통해 무단 접근을 방지합니다.

20. DIRECT 채팅방은 어떻게 중복 생성을 막고 있나요?

DIRECT 타입은 참여자 조합을 기준으로 정렬 후, 동일한 참여자 수와 구성의 방이 이미 존재하면 DUPLICATE_DIRECT_ROOM 예외를 발생시켜 중복 생성을 방지합니다.


✅ 성능 및 확장성 관련

21. 읽지 않은 메시지 수는 어떻게 계산하고 있나요?

ChatMessageRepository.countByChatRoomAndIdGreaterThan()을 사용해, 참여자의 lastReadMessageId보다 이후에 저장된 메시지 수를 집계합니다.

22. 대용량 메시지나 방이 많을 경우 성능 문제는 어떻게 해결할 계획인가요?

현재는 RDB 기반 구조지만, 메시지 큐(Redis Pub/Sub, Kafka)나 분산 캐시 도입을 고려하고 있습니다. 또한 메시지 조회에는 페이지네이션을 적용해 서버 부담을 줄였습니다.

23. 현재는 SimpleBroker를 사용하는데, 향후 RabbitMQ나 Redis pub/sub으로 확장 가능성이 있나요?

네, enableSimpleBroker() 대신 enableStompBrokerRelay()로 설정을 변경하면 손쉽게 RabbitMQ나 Redis 기반 브로커로 확장할 수 있습니다. 확장성 고려해 설계해두었습니다.


✅ 웹소켓 통신 처리

24. /pub과 /sub 경로는 어떤 기준으로 나눴나요?

/pub은 클라이언트가 서버로 메시지를 보낼 때 사용하고, /sub은 서버에서 클라이언트에게 메시지를 브로드캐스트할 때 사용합니다. STOMP 표준 구조에 따랐습니다.

25. WebSocketPrincipal을 왜 사용하나요?

세션에 인증된 사용자 정보를 보존하고, 메시지 처리 시 simpUser로 식별하기 위함입니다. 이렇게 하면 WebSocket에서도 Principal 기반 권한 처리가 가능합니다.

26. 클라이언트에서 보낸 메시지는 어떤 경로로 서버에서 처리되나요?

클라이언트는 /pub/... 경로로 STOMP 메시지를 보내고, 이 메시지는 컨트롤러 or 서비스에서 처리되어 DB 저장 및 /sub/... 채널로 브로드캐스트됩니다.


✅ 기타 면접 상황에서 나올 수 있는 질문

27. 이 시스템에서 가장 어려웠던 점은 무엇이었고 어떻게 해결했나요?

트랜잭션 이후 실시간 브로드캐스트 시점 조절이 가장 어려웠습니다. DB가 커밋되기 전에 메시지를 보내면 클라이언트와 불일치가 생기므로 afterCommit()을 이용해 시점을 제어했습니다.

28. 이 구조의 장점과 단점은 무엇인가요?

  • 장점: 역할별 분리가 명확해 유지보수성이 높고, CQRS 및 퍼사드 패턴으로 확장과 테스트가 쉽습니다.
  • 단점: 클래스 수가 많아 초기 진입장벽이 높고, 퍼사드 도입 시 오히려 중복 코드가 늘 수 있습니다.

29. 이 프로젝트를 다른 사람과 협업한다면 어떤 부분을 문서화하거나 공유할 필요가 있다고 생각하나요?

WebSocket 인증 처리 흐름, CQRS 구조 설명, WebSocket 브로커 경로(/pub, /sub) 사용 방식, 메시지 흐름도(입장전송읽음~나감), 예외 응답 코드 구조 등을 문서화해야 협업 효율이 높아집니다.

0개의 댓글