묻고 답해요
161만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
StompHandler 관한 질문
안녕하세요 강의를 토대로 프로젝트에 채팅기능을 구현중입니다.강의에서는 StompHandler 에서 CONNECT, SUBSCRIBE 인 경우만 다루는데 저는 SEND 인 경우의 코드도 작성중입니다.SEND 시 사용자 정보를SecurityContextHolder.getContext().setAuthentication(auth);이런식으로 담아서 StompController 에서 SecurityContextHolder.getContext().getAuthentication().getPrincipal()이런식으로 꺼내쓰려고 했습니다.그런데 담을 때는 잘 담았는데, 막상 StompController에서 꺼낼 때는 비어져있다고(null) 합니다. 왜 비어져있는 지 알 수 있을까요? 그러면 사용자 정보를 가져올 수 있는 방법은 없는 건가요?
-
미해결프로덕션 레벨 실시간 채팅 서버 구축: 분산 처리부터 성능 최적화까지 (Kotlin & Spring)
빌드 파일
빌드 파일을 파일 마다 각각 만드는데 그렇게 하는 이유는 무엇인가요???
-
해결됨프로덕션 레벨 실시간 채팅 서버 구축: 분산 처리부터 성능 최적화까지 (Kotlin & Spring)
웹소켓을 이용한 채팅시스템에서 부하테스트를 어떻게 진행해야할까요?
안녕하세요. 취준하고있는 예비 개발자입니다. 개인프로젝트를 진행하다가 여쭤보고 싶은게 있어 강의까지 구매하게 되었습니다. 가장 궁금한 질문은 '웹소켓을 이용한 채팅시스템에서 부하테스트를 어떻게 진행해야하는가?'입니다. 추가질문 및 부연설명을 위해 조금만 더 읽어주시면 감사하겠습니다. 현재 진행하고 있는 앱 개발 프로젝트 진행 중입니다.기술스택은 서버는 코틀린,스프링이고, 클라이언트는 iOS(swift)와 안드로이드(Kotlin)로 구성했습니다. http요청을 처리하는 서버(스프링 서버)는 단일 서버와 단일 데이터베이스로만 구성한 상황이고, 단일 인스턴스는 aws의 t2.micro를 이용하고 있습니다. 서버에는 nginx / next 서버(홍보용 홈페이지) / 스프링 서버 / github-runner 등의 프로세스가 실행 중에 있습니다. 클라이언트의 무한 재연결 로직의 문제로 인해 인스턴스 내부에서 'ss -s' 명령어를 이용해 소켓상태를 조회해본 결과, 소켓 tcp연결이 폭발적으로 증가하여 400개까지 증가한 상황이 있었습니다. 이 상황에서 소켓을 이용한 채팅뿐만 아니라 사용자 조회와 같은 http요청 모두 느려지는 것을 확인되었습니다. 하지만 재연결 로직을 수정하고 이후 tcp연결이 400(= 클라이언트 - nginx 200개 / nginx - 서버 200개)까지 증가하는 상황을 만들어봐야 또 문제가 발생하는지 확인할 수 있다고 생각했지만 200명의 테스터를 모을 수 없다고 생각했습니다. 또한, 채팅의 API를 하나 파고, ngrinder를 이용해 부하테스트를 요청하는 상황이 적합할까 생각했을 떄, 웹소켓 연결이 되지 않는 상황이라고 생각되어 적합하지 않다고 생각했습니다. 이런 상황에서 어떻게 테스트해볼 수 있을지 고민됩니다. 추가적으로 궁금한 점은앱 개발에서 채팅시스템을 구축하는 상황이고, 대략 500~1000명을 수용해야한다면 어떤 기술을 적용해 채팅시스템을 구축하셨을 것 같나요? 실시간 통신하면 웹소켓정도 밖에 모르는 상황이었기에 웹소켓을 적용했지만서도 타당했는가? 적합했는가에 대한 의문이 여전히 남아있는 상황이라고 생각하기 때문에 질문드렸습니다. t2.micro 서버는 얼만큼의 소켓연결까지 버틸 수 있는지 알고 싶습니다. 현재 서비스의 사전예약자가 80명 정도 되는 상황이라 t2.micro를 이용했을 때 서버가 터질까봐 우려스럽습니다. 그래서 서버 스펙을 확장을 고려하고 있는데, 취준생이기에 비용적인 측면에서 고려하지 않을 수가 없는 상황이라 '정말 확장하는게 맞을까?', '내가 능력이 부족한 게 아닐까?' 라는 생각이 들어 갈피를 못잡고 있는 상황이라 질문드렸습니다.제 질문들이 대부분 인프라 확장의 타당성을 갖추기 위한 질문이라고 생각합니다. 혹시 인프라 확장을 위한 근거로써 어떤 지표가 타당성을 확보할 수 있다고 생각하시는지 궁금합니다. 긴 질문 읽어주셔서 감사합니다.행복한 하루 되세요~
-
미해결웹에서 다루는 미디어 - 화상 대화를 만들면서 배우는 MediaStream API
오디오 Input -> Speaker 출력 Noise
안녕하세요.이번에 프로젝트를 진행 하고 있는데 해결되지 않는 부분이 있어서 질문을 드립니다.상황을 간단히 말씀드리면, 미팅룸 개설을 하고 참여한 인원중에 말을 하면 해당 음성을 다른 참여자의 스피커로 출력하는 방식입니다. (발화자 제외) 이때 Input Audio format은 16Khz, MONO, 32Float, 16,000 sample 로 지정되어 있습니다.(음성 출력 뿐만 아니라, STT 서버에 보내서 텍스트를 반환하는데 이때 STT 서버의 오디오 요청스펙 입니다.) 그리고 Gemini의 도움을 받아 아래와 같이 옵션을 설정하였지만, 실제로 스피커 출력시 매우심한 Noise가 발생합니다. (STT 서버의 응답 텍스트는 정상 동작) 저는 백엔드 개발자인데, 프론트단에서 해결 방법을 잘 모르겠어서, 강의를 결제하게 되었습니다. 혹시 조언을 해주실수 있을까요?아니면 강의에 몇강을 보면 관련 주제가 나오는지 알려주도 좋을거같습니다. 긴글 읽어주셔서 감사합니다. Input audio data 관련 코드audio: { echoCancellation: true, noiseSuppression: false, autoGainControl: false, } this.highPassFilter = this.audioContext.createBiquadFilter(); this.highPassFilter.type = 'highpass'; // [튜닝] 목소리 뭉개짐을 피하기 위해 60Hz로 설정 this.highPassFilter.frequency.value = 60; // 2. Compressor (안전장치/Limiter 역할 튜닝) this.compressor = this.audioContext.createDynamicsCompressor(); // [튜닝] -6dB를 넘어가는 "정말 큰 기계음"만 잡는 '안전장치'로 사용 this.compressor.threshold.value = -6; this.compressor.knee.value = 30; // [튜닝] 2:1로 최소한만 압축 this.compressor.ratio.value = 2; // [튜닝] 순간적인 피크를 빠르게(3ms) 잡음 this.compressor.attack.value = 0.003; this.compressor.release.value = 0.25; // 3. GainNode (전체 볼륨 증폭) this.gainNode = this.audioContext.createGain(); // [튜닝] 압축을 거의 안 하므로 1.1배로 소폭만 증폭 this.gainNode.gain.value = 1.1; // --- 7. 노드 체인 연결 --- this.audioSource.connect(this.highPassFilter); // 1. (마이크) -> 저주파 험 제거 this.highPassFilter.connect(this.compressor); // 2. -> "정말 큰 소리"만 방지 this.compressor.connect(this.gainNode); // 3. -> 전체 볼륨 소폭 증폭 this.gainNode.connect(this.resamplerNode); // 4. -> VAD 및 리샘플링 this.resamplerNode.connect(this.audioContext.destination); // (워크렛 실행용)스피커 출력 관련 코드// --- [수정] 오디오 출력(Playback) 로직 (심리스 스케줄링) --- private handleIncomingAudio(audioData: ArrayBuffer): void { if (audioData.byteLength === 0 || !this.playbackAudioContext) return; if (this.playbackAudioContext.state === 'suspended') { this.playbackAudioContext.resume().catch((err) => { console.error('Playback AudioContext 재개 실패:', err); }); } this.audioQueue.push(audioData); // [수정] 재생 루프가 멈춰있을 때(!this.isPlaying)만 새로 시작 if (!this.isPlaying) { this.isPlaying = true; // 현재 시간을 기준으로 스케줄링을 다시 시작합니다. this.nextChunkTime = this.playbackAudioContext.currentTime; this.playNextChunk(); } } private playNextChunk(): void { if (this.audioQueue.length === 0) { this.isPlaying = false; // 큐가 비면 재생 중지 return; } if (!this.playbackAudioContext || this.playbackAudioContext.state === 'closed') { this.isPlaying = false; this.audioQueue = []; return; } const audioData = this.audioQueue.shift()!; try { const float32Data = new Float32Array(audioData); const audioBuffer = this.playbackAudioContext.createBuffer( PLAYBACK_CHANNELS, float32Data.length, this.playbackAudioContext.sampleRate ); audioBuffer.copyToChannel(float32Data, 0); const source = this.playbackAudioContext.createBufferSource(); source.buffer = audioBuffer; source.connect(this.playbackAudioContext.destination); // --- [수정] 심리스 스케줄링 로직 --- // 1. 랙(Lag)으로 인해 예약 시간이 이미 지났는지 확인 const currentTime = this.playbackAudioContext.currentTime; if (this.nextChunkTime < currentTime) { // 지연이 발생했으면, 갭(Gap)이 생기지 않도록 현재 시간으로 리셋 this.nextChunkTime = currentTime; } // 2. 계산된 nextChunkTime에 재생을 '예약'합니다. (갭 제거) source.start(this.nextChunkTime); // 3. 다음 청크가 시작될 시간을 미리 계산합니다. this.nextChunkTime += audioBuffer.duration; // 4. [수정] onended에서 다음 청크를 비동기적으로 호출합니다. (버그 수정) source.onended = () => { // 큐에 다음 데이터가 있으면, 딜레이 없이 바로 다음 청크를 스케줄링합니다. if (this.audioQueue.length > 0) { this.playNextChunk(); } else { this.isPlaying = false; // 큐가 비었으면 재생 종료 } }; // 5. [삭제] 즉각적인 재귀 호출을 삭제합니다. (이것이 버그였습니다) // if (this.audioQueue.length > 0) { // this.playNextChunk(); // } } catch (e) { console.error('오디오 청크 재생 중 오류:', e); this.isPlaying = false; // 오류 발생 시 재생 루프 중지 } }
-
해결됨RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기
동적 큐 이름 설정 방법 및 SimpleRabbitListenerContainerFactory의 재정의에 따른 Retry 설정 미적용 관련 질문입니다.
안녕하십니까 코드빌런님.이번 추석 연휴동안 레빗 엠큐 강의 잘 들었습니다. 다름이 아니라 강의에서 알려주신 여러 내용을 바탕으로 기존에 구현하였던 redis pub/sub 기반의 알림 기능에 레빗 엠큐를 적용해보고 있습니다. 그리고 구현 중 두가지 질문이 있어 질문을 작성하게 되었습니다. 1. 동적 큐 이름 설정 방식우선 알림을 전송하는 과정에 대해 말씀드리면 다음과 같습니다.알림 객체 저장성공 시 알림 발송sse 연결들을 ConcurrentMap으로 관리하여 대상 userId의 sse연결을 찾아 해당 연결로 알림 객체 전송 현재 메시지 큐 구조는 다음과 같습니다. 알림 저장 메시지 생성 (direct exchange, saveNotificationQueue) -> 메시지 저장 성공 시 알림 전달 메시지 생성 (fanout exchange, publishNotificationQueue), 메시지 저장 실패 시 데드레터 큐로 전달 현재 서비스는 3개의 인스턴스로 동작하고 있습니다. 이때 알림 저장 큐는 1개라서 복수 저장될 일이 없지만, 그 후에 진행되는 알림 전달의 경우 단일 큐로 작동하면 대상 sse 연결이 없는 인스턴스에서 해당 메시지를 소비하게 되면 전송이 실패합니다. 그래서 각 인스턴스마다 큐를 만들어주고 fanout exchange에 모두 바인딩하여 사용하는 방식으로 만들어야 할 것 같다고 생각하였습니다. 그래서 찾아보니 SpEL 기반 동적 큐 이름 지정 방식이 있다고 하여 해당 방식으로 구현해보았습니다.// RabbitMQConfig.java // 알림 발송 큐 @Bean public String dynamicPublishNotificationQueueName() { String randomString = UUID.randomUUID().toString(); return PUBLISH_NOTIFICATION_QUEUE + " : " + randomString; } @Bean public Queue publishNotificationQueue() { return new Queue(dynamicPublishNotificationQueueName(), false); } @Bean public FanoutExchange publishNotificationExchange() { return new FanoutExchange(PUBLISH_NOTIFICATION_EXCHANGE); } @Bean public Binding publishNotificationBinding() { return BindingBuilder.bind(publishNotificationQueue()).to(publishNotificationExchange()); } // NotificationSubscriber.java @RabbitListener(queues = "#{@dynamicPublishNotificationQueueName}") public void consumePublishNotificationMessage(Notification notification) { notificationService.publishNotification(notification); }해당 방식으로 정상 작동은 확인하였는데, 혹시 해당 방식 외에 더 나은 방식이 있는지 궁금합니다.2. SimpleRabbitListenerContainerFactory의 재정의에 따른 Retry 설정 미적용강의 18강에서 application.yml에 retry 관련 프로퍼티를 설정하는 것만으로 자동으로 retry가 적용된다고 하여 해당 방식을 프로젝트에 적용해보았습니다. spring.rabbitmq.listener.simple.retry.enabled=true spring.rabbitmq.listener.simple.retry.initial-interval=1000 spring.rabbitmq.listener.simple.retry.max-attempts=3 spring.rabbitmq.listener.simple.retry.max-interval=1000 spring.rabbitmq.listener.simple.default-requeue-rejected=false하지만 어떤 이유인지는 몰라도 retry가 작동하지 않았습니다. 실제로 실행되는 코드에 로그를 찍어봐도 한번만 시도하고 설정한 예외가 발생 후 바로 DLQ로 이동하였습니다. 그래서 원인을 찾던 도중https://inf.run/bsxxr에서@RabbitListener를 사용하면 내부적으로 SimpleMessageListenerContainer가자동으로 생성되기 때문에 retry 설정을 읽어서 exception 이 발생할 경우 RetryTemplate을 사용해서 자동으로 설정된 속성에 해당하는 작업을 수행하게 됩니다.라고 코드빌런님이 말씀하신 것을 보았습니다.확인해보니 메시지큐에서 객체 자동 역직렬화를 위해@Bean public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(messageConverter()); return factory; }이렇게 SimpleRabbitListenerContainerFactory를 정의하여 빈으로 등록해놓았는데, SimpleRabbitListenerContainerFactory를 살펴보니public class SimpleRabbitListenerContainerFactory extends AbstractRabbitListenerContainerFactory<SimpleMessageListenerContainer> { ...말씀하신 SimpleMessageListenerContainer를 타입파라미터로 받아 상속받고 있는 형태였습니다. 이에 말씀하신 SimpleMessageListenerContainer가 자동으로 생성되어 retry 설정이 적용안되는것인가? 라고 예상하여 application.properties에 정의하는 대신 @Bean public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ConnectionFactory connectionFactory) { SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); factory.setConnectionFactory(connectionFactory); factory.setMessageConverter(messageConverter()); factory.setDefaultRequeueRejected(false); factory.setAdviceChain(RetryInterceptorBuilder.stateless() .maxAttempts(3) .backOffOptions(1000, 2.0, 10000) .build()); return factory; }이렇게 직접 retry 설정을 넣어주니 그제야 재시도가 정상적으로 작동하였습니다. 해당 원인이 제가 생각한 직접 팩토리를 Bean으로 등록하면 application.properties의 retry 설정이 무시되는 것이 맞는지 궁금합니다. 코드는 아래 url에서 보실 수 있습니다.https://github.com/Dockerel/4th-SC-TEAM1-BE/pull/15/files 강의 정말 잘 들었습니다! 이렇게 프로젝트에 바로 적용해볼 수 있어서 기분이 좋네요.나중에 코드빌런님의 다른 기술 스택 강의도 들어보고 싶습니다.감사합니다.
-
해결됨Spring Boot를 활용하여 채팅 플랫폼 만들어보기
비전공자인데 AI가 발전한 요즘 백엔드로 진로를 하고 싶으면 어떤식으로 공부를 해야 하는지 알 수 있을까요???
비전공자이고 백엔드로 하고 싶은데,AI가 발전한 현재 시점에서 어떻게 공부를 해야 할지 조언을 구하고 싶습니다.
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
리프레시 토큰은 알아서 구현하면 되는건가요??
리프레시 토큰은 알아서 구현해야 하면 되는건가요?? 따로 없는데..
-
해결됨프로덕션 레벨 실시간 채팅 서버 구축: 분산 처리부터 성능 최적화까지 (Kotlin & Spring)
엔티티는 Data Class로 작성하면 안되나요?
아직 코틀린에 익숙치 않은데 어떤 경우 Data class를 선언하고 어떤 경우 일반 class를 선언하는지 감이 안잡히네요.추가로 object나 compainon object는 어떤 경우 사용하게 되나요?
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
강의 잘듣고 있습니다. 혹시 다음 강의 계획은 없으신가요?
강의 잘듣고 있습니다. 혹시 다음 강의 계획은 없으신가요?
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
STOMP 동작 과정 질문
강의에서 STOMP 동작 간 /app, /topic 요청을 동시에 보낸다고 하셨는데 그럼 app 경로 발행된 메세지는 broker를 통해서 topic 경로로 전달이 되고 맨 처음에 동시에 보낸 topic 요청도 broker를 통해서 전달이 되어서 broker에서 구독하고 있는 사용자들에게 메세지를 보내준다고 이해하면 되는 것일까요? /topic으로 2번 가는 것 처럼 느껴져서 약간의 혼동이 있었습니다!
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
chain.doFilter()
JwtAuthFilter에서 토큰이 없을 때 예외를 던지지 않고 그냥 chain.doFilter()를 호출하는 이유는?
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
자바 21로 소스 작성해도 되나요?
자바 17로 되어 있던데 자바 21로 소스 작성해도 되나요?
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
메시지 브로커 선택에 관한 질문
안녕하세요, 강의에서는 메시지 브로커용으로 redis를 사용하셨는데, redis 외에도 rabbitmq나 카프카 같은 것들도 사용되는 것으로 알고있습니다. 그 중에서 특별히 redis를 사용한 이유가 있는지 궁금합니다.그리고 무중단 배포 시 스프링 내장 브로커를 사용하면 서버 재실행 시 구독 정보가 초기화되기에 메시지 브로커를 도입하려고 하는데 이때는 셋 중에 어떤 것을 사용하면 좋을지 궁금합니다.
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
WebSocket과 Spring Security 질문
WebSocket 연결이 처음 http요청으로 시작하기 때문에 필터 체인이 요청을 가로챈다.따라서 /connect를 permitAll()로 풀어줘야 400에러가 안난다. 로 이해했는데 맞을까요?
-
미해결RabbitMQ를 이용한 비동기 아키텍처 한방에 해결하기
강의 자료 관련
강의자료가 PDF로 변환하다보니, 문자 길이 때문인지 끊어지는 경우가 있는거 같은데 혹시 Notion 페이지로 제공해주실 수 잇나요?
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
추가 커스텀 구현 질문 있습니다.
로그인을 했을 때 소속된 채팅방에 안 읽은 메시지가 있다면, 알림(보통 종모양)에 +안읽은메시지숫자를 구현 하려고할 땐 sse 통신을 이용하는게 좋을까요?
-
해결됨프로덕션 레벨 실시간 채팅 서버 구축: 분산 처리부터 성능 최적화까지 (Kotlin & Spring)
stomp websocket
stomp 웹소캣을 사용할때도 ChatWebSocketHandler를 구현해야 할까요??아니면 raw websocket만 구현하면 되는 건가요??
-
해결됨프로덕션 레벨 실시간 채팅 서버 구축: 분산 처리부터 성능 최적화까지 (Kotlin & Spring)
웹소캣 stomp
raw level 말고 stomp로 개발하고 싶으면 섹션7 부분만 조금 다른게 코딩하면 될까요??
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
connect와 동시에 구독하는데 구독 검증이 꼭 필요한가요?
우선 최초 웹 소켓 연결에 토큰 검증을 진행하고, 연결에 성공하면 특정 토픽에 바로 구독하는 걸로 알 고 있습니다.이 두 작업이 거의 동시에 이루어진다고 보는데, 구독 검증은 왜 필요한가요 선생님?
-
미해결웹소켓/STOMP 채팅서비스(spring, vue, redis)
저도 동일한 질문인데
왜 도커 Redis cli 터미널 내에서 강사님이 말씀해주신 거처럼 해도 메시지가 안 오고 PUBSUB CHANNELS Empty List라고 뜰까요 근데 강사님처럼 8080 8081 서버간에 통신은 돼서 문제가 무엇인지 모르겠습니다.