if (chatRoom.getIsGroupChat().equals("N")) { throw new IllegalArgumentException("단체 채팅방이 아닙니다."); } leaveGroupChatRoom 메소드에서 이 부분이 맞나요? 개인채팅일 경우 여기에 걸려 예외가 납니다.
안녕하세요, Spring Security 필터 등록 방식에 대해 질문이 있습니다. 현재 이전까지 저는 Security 필터를 아래처럼 new 로 직접 생성해서 사용하고 있습니다. LoginFilter loginFilter = new LoginFilter(authenticationManager, loginSuccessHandler, loginFailureHandler); @Component 를 달아 Bean으로 등록하면 Spring이 Security 필터체인과 별개로 기본 서블릿 필터체인에도 자동 등록해버려서 필터가 두 번 실행되는 문제가 있기 때문입니다. 혹시 최신 Spring Boot 버전에서는 이 문제가 개선되었나요? 아니면 @Component + 생성자 주입 방식을 안전하게 사용할 수 있는 방법이 따로 있는지 궁금합니다.
retryCount가 전역에서 작동하는것이 좀 수상하네요. 각 메세지의 retryCount를 세어야 할 것 같은데 저렇게 두면 혹시 메세지들이 들어오면 숫자가 마구 이랬다 저랬다 할 가능성은 없나요? 이를테면 메세지 두세개가 들어왔는데 첫번째는 실패 두번째 메세지 실패 세번째 성공 이런 경우 첫번째 메세지가 성공할때까지 sync하게 작동하여 완전 처리될때까지 두번째 메세지는 아예 실행되지 않는것인가요? 아니면 평행하게 동시에 실행되는거라 저 코드가 좀 잘못된 건가요?
안녕하세요 강사님. 서비스 계층을 설계할 때 인터페이스를 두고 구현체를 만드는 패턴을 사용하는 이유가 궁금합니다. 현재 프로젝트에서는 구현체가 하나뿐인데, 이런 상황에서도 인터페이스를 분리하는 것이 가지는 실질적인 이점이 무엇인지 알고 싶습니다. 특히 구현이 하나로 고정되어 있다면, 바로 구현 클래스를 빈으로 등록해도 되지 않을지 고민이 되었는데요. 설계 관점에서 인터페이스를 미리 분리해두는 이유와, 실무에서 어떤 장점이 있는지 설명해주시면 감사하겠습니다
안녕하세요, step 4에 fanout 브로드캐스팅으로 구현한 이유가 궁금해서 질문드립니다. 다른 질문을 찾아보니 아래와 같이 답변을 주셨습니다. "정리하면 Fanout은 모든 큐에 메시지를 브로드캐스트하지만 WebSocket을 이용해 특정 뉴스 타입만 선택적으로 구독할 수 있습니다. (서로 독립적으로 동작합니다) 따라서 Pub/Sub의 성격을 이용해서 브로드 캐스트를 하는 것은 Direct나 Topic 보다 Fanout이 적합하고, 전체 브로드캐스트가 필요하지 않은 Pub/Sub의 일부 제한이 필요한 경우는 WebSocket과 STOMP를 통해서 필터링 .. 여기서 저는 왜 step4에서 Pub/Sub의 성격을 이용해서 브로드캐스팅하는지 이해가 안됩니다. 어차피 WebSocket에서 필터링할 것이라면 왜 비효율적으로 브로드캐스팅하는건가요? fanout보다 topic Exchange가 적합하지 않나요? 어차피 브로드캐스팅하고 모든 큐에 넣을 것이라면, javaQueue, vueQueue, springQueue로 구분할 필요가 있나요? pub/sub 모델은 아니지만, 하나의 newQueue로 관리하고 simpMessagingTemplate의 destination을 구분하는 것은 어떤가요?
안녕하세요. 설정 시 String 으로 작성 된게 눈에 걸려서 확인해봤는데, Spring AMQP 2.X 에서는 다른 방법이 있어서 내용 공유 드립니다... 다른 수강생분들도 참고용으로 q&a로 남겼습니다.. 감사합니다... @Bean public Queue orderQueue() { return QueueBuilder.durable(ORDER_COMPLETED_QUEUE) .deadLetterExchange(DLX) // Available since Spring AMQP 2.x .deadLetterRoutingKey(DLQ) // Available since Spring AMQP 2.x // .withArgument("x-dead-letter-exchange", DLX) // Spring AMQP 1.x compatible // .withArgument("x-dead-letter-routing-key", DLQ) // Spring AMQP 1.x compatible .ttl(5000) .build(); }
안녕하세요 ~ 강의 잘 수강하고 있습니다 ! ERD 부분에서 질문이 생겨 여쭤보게 되었습니다 !! chat_participant 부분에서 전용 pk를 하나 두고 chat_room_id와 member_id를 두셨는데, 혹시 이런 경우에는 pk 자체를 chat_participant_id 라는 전용 pk 말고 chat_room_id, member_id 복합 pk로 사용해도 괜찮을까요 ? message_read_status 테이블을 따로 정규화시킨 이유가 있을까요 ? chat_message와 거의 비슷한 외래키를 쓰고 있는데 chat_message 테이블 안에 isRead 컬럼 자체를 두는 것이 더 적절하지 않은지 강의를 수강하다 궁금해서 여쭤보게 되었습니다 !! 또, 혹시 강의 내용을 공부한 부분에 대해서 학습 목적으로 블로그에 공부한 것들을 정리해서 업로드하는것이 괜찮을까요 ? 좋은 강의 제공해주셔서 너무 감사드립니다 ~~~ 오늘 설 연휴인데 새해복 많이 받으셔요 ~~
RedisMessageBroker 가 @Service 로 관리 되기 때문에 싱글톤으로 관리 되는걸로 알고 있는데, 아래처럼 핸들러를 할당하는 경우 여러곳에서 setLocalMessageHandler 호출 시 문제가 발생할거 같은데 맞는건지 궁금합니다. fun setLocalMessageHandler(handler: (Long, ChatMessage) -> Unit) { this.localMessageHandler = handler }
안녕하세요 강의를 토대로 프로젝트에 채팅기능을 구현중입니다. 강의에서는 StompHandler 에서 CONNECT, SUBSCRIBE 인 경우만 다루는데 저는 SEND 인 경우의 코드도 작성중입니다. SEND 시 사용자 정보를 SecurityContextHolder.getContext().setAuthentication(auth); 이런식으로 담아서 StompController 에서 SecurityContextHolder.getContext().getAuthentication().getPrincipal() 이런식으로 꺼내쓰려고 했습니다. 그런데 담을 때는 잘 담았는데, 막상 StompController에서 꺼낼 때는 비어져있다고(null) 합니다. 왜 비어져있는 지 알 수 있을까요? 그러면 사용자 정보를 가져올 수 있는 방법은 없는 건가요?
안녕하세요. 취준하고있는 예비 개발자입니다. 개인프로젝트를 진행하다가 여쭤보고 싶은게 있어 강의까지 구매하게 되었습니다. 가장 궁금한 질문은 '웹소켓을 이용한 채팅시스템에서 부하테스트를 어떻게 진행해야하는가?'입니다. 추가질문 및 부연설명을 위해 조금만 더 읽어주시면 감사하겠습니다. 현재 진행하고 있는 앱 개발 프로젝트 진행 중입니다. 기술스택은 서버는 코틀린,스프링이고, 클라이언트는 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를 이용했을 때 서버가 터질까봐 우려스럽습니다. 그래서 서버 스펙을 확장을 고려하고 있는데, 취준생이기에 비용적인 측면에서 고려하지 않을 수가 없는 상황이라 '정말 확장하는게 맞을까?', '내가 능력이 부족한 게 아닐까?' 라는 생각이 들어 갈피를 못잡고 있는 상황이라 질문드렸습니다. 제 질문들이 대부분 인프라 확장의 타당성을 갖추기 위한 질문이라고 생각합니다. 혹시 인프라 확장을 위한 근거로써 어떤 지표가 타당성을 확보할 수 있다고 생각하시는지 궁금합니다. 긴 질문 읽어주셔서 감사합니다. 행복한 하루 되세요~
안녕하십니까 코드빌런님. 이번 추석 연휴동안 레빗 엠큐 강의 잘 들었습니다. 다름이 아니라 강의에서 알려주신 여러 내용을 바탕으로 기존에 구현하였던 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 강의 정말 잘 들었습니다! 이렇게 프로젝트에 바로 적용해볼 수 있어서 기분이 좋네요. 나중에 코드빌런님의 다른 기술 스택 강의도 들어보고 싶습니다. 감사합니다.
강의에서 STOMP 동작 간 /app, /topic 요청을 동시에 보낸다고 하셨는데 그럼 app 경로 발행된 메세지는 broker를 통해서 topic 경로로 전달이 되고 맨 처음에 동시에 보낸 topic 요청도 broker를 통해서 전달이 되어서 broker에서 구독하고 있는 사용자들에게 메세지를 보내준다고 이해하면 되는 것일까요? /topic으로 2번 가는 것 처럼 느껴져서 약간의 혼동이 있었습니다!