요즘은 헬스도 꾸준히 다니고 있고, 탁구 동호회에도 들어가서 몸도 마음도 리프레시 중이다.
그런데도 개발에 대한 갈증은 쉽게 사라지지 않아서, 최근 진행 중인 개인프로젝트에 '실시간 채팅' 기능을 붙여보기로 했다.
얼마 전엔 스타트업 기술 면접도 봤는데, 무려 6시간 동안 진행돼서 진이 쏙 빠졌지만, 돌아보면 정말 좋은 경험이었다.
준비가 부족했던 부분도 보였고, 놓쳤던 것도 많았지만 덕분에 지금 내가 부족한 게 뭔지 명확히 보였달까.
그래서 이번엔 진짜 하나라도 더 구현해보자!는 마음으로, React + Spring Boot 기반의 게시판 + 결제 프로젝트에 WebSocket 채팅 기능을 직접 구현해봤다.
React + Spring Boot로 게시판 + 결제 기능을 구현하고 있었는데, 문득 "게시글 작성자와 실시간으로 대화할 수 있으면 좋겠다"는 생각이 들어 WebSocket 채팅 기능을 붙이게 되었다.
처음엔 막막했지만, 해보니까 생각보다 재미있고, 성능 개선까지 이어지면서 전체적으로 만족스러운 작업이 되었다.
WebSocket은 일반 HTTP와 달리 Header에 토큰을 넣을 수 없기 때문에, SockJS 연결 시 쿼리파라미터로 토큰을 전달하고, 백엔드에서는 WebSocket handshake 과정에서 이걸 검증했다.
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String token = accessor.getFirstNativeHeader("token");
String email = jwtTokenProvider.getUserEmail(token);
accessor.getSessionAttributes().put("userEmail", email);
}
return message;
}
});
}
✅ WebSocket에서도 사용자를 식별 가능하게 됨
채팅 기능을 붙이다 보니, 성능이나 구조적으로 아쉬운 부분들이 보여서 리팩토링도 병행했다. 특히 다음 3가지는 눈에 띄게 체감될 정도로 개선이 되었다.
처음에는 메시지를 수신할 때마다 동기적으로 DB에 저장하고 있었는데, 이러면 실시간성이 떨어졌다.
// 기존: 동기 저장 → 메시지 전송이 느림
chatMessageRepository.save(message);
// 개선: 비동기 저장 처리
@Async
public void saveMessageAsync(ChatMessage message) {
chatMessageRepository.save(message);
}
✅ WebSocket 응답 속도 약 35% 향상 (체감 확실)
ChatRoom 조회 후 sender/receiver의 nickname을 각각 호출하면서 N+1 쿼리 폭탄 발생...
// 개선 전
room.getSender().getNickname(); // 쿼리 발생
// 개선 후: fetch join
@Query("SELECT r FROM ChatRoom r JOIN FETCH r.sender JOIN FETCH r.receiver WHERE r.sender = :user OR r.receiver = :user")
List<ChatRoom> findAllWithUsersByUser(User user);
✅ 평균 조회 시간 450ms → 260ms로 감소
읽음 처리도 하나하나 save() 하다 보니, 메시지 개수가 많아질수록 성능이 확 떨어졌다.
// 기존
messages.forEach(m -> m.markAsRead());
// 개선
@Modifying
@Query("UPDATE ChatMessage m SET m.isRead = true WHERE m.chatRoom = :room AND m.sender <> :user AND m.isRead = false")
void markMessagesAsReadBulk(ChatRoom room, User user);
✅ 수백 건도 1~2ms 내 처리 가능
이제 실시간 채팅도 잘 붙었고, 성능 개선까지 했으니 다음은 사용자 경험을 좀 더 다듬는 일만 남았다.
오늘도 개발하면서 또 한 걸음 나아간 느낌이다 😊