Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies {
// Etc
implementation 'org.hibernate.validator:hibernate-validator'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

tasks.named('test', Test) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.example.solidconnection.chat.config;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

@Component
public class StompEventListener {

private final Set<String> sessions = ConcurrentHashMap.newKeySet();

@EventListener
public void connectHandle(SessionConnectEvent event) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
sessions.add(accessor.getSessionId());
}

@EventListener
public void disconnectHandle(SessionDisconnectEvent event) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
sessions.remove(accessor.getSessionId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.example.solidconnection.chat.config;

import static com.example.solidconnection.common.exception.ErrorCode.AUTHENTICATION_FAILED;

import com.example.solidconnection.auth.token.JwtTokenProvider;
import com.example.solidconnection.common.exception.CustomException;
import com.example.solidconnection.common.exception.ErrorCode;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class StompHandler implements ChannelInterceptor {

private final JwtTokenProvider jwtTokenProvider;

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Claims claims = validateAndExtractClaims(accessor, AUTHENTICATION_FAILED);
}

if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
Claims claims = validateAndExtractClaims(accessor, AUTHENTICATION_FAILED);

String email = claims.getSubject();
String destination = accessor.getDestination();

String roomId = extractRoomId(destination);

// todo: roomId 기반 실제 구독 권한 검사 로직 추가
}

return message;
}

private Claims validateAndExtractClaims(StompHeaderAccessor accessor, ErrorCode errorCode) {
String bearerToken = accessor.getFirstNativeHeader("Authorization");
if (bearerToken == null || !bearerToken.startsWith("Bearer ")) {
throw new CustomException(errorCode);
}
String token = bearerToken.substring(7);
return jwtTokenProvider.parseClaims(token);
}

private String extractRoomId(String destination) {
if (destination == null) {
throw new CustomException(ErrorCode.INVALID_ROOM_ID);
}
String[] parts = destination.split("/");
if (parts.length < 3 || !parts[1].equals("topic")) {
throw new CustomException(ErrorCode.INVALID_ROOM_ID);
}
return parts[2];
}
}
Copy link
Collaborator

@nayonsoso nayonsoso Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 파일 전체에도 포매팅이 필요해보입니다 🏃‍♀️
만약 다른 개발자가 main.java 패키지 전체를 포매팅했을 때, 이 파일까지 함께 변경된다면
그 변경이 본인의 관심사와는 무관한 부분임에도 불구하고 PR에 포함될 것입니다.
이런 경우엔 불필요한 diff가 발생해서 리뷰나 히스토리 추적에 혼란을 줄 수 있다고 생각합니다~
그래서 미리 포매팅을 맞춰두는 게 좋을 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

분명 모든 파일에 포매팅을 했는데........ 빼먹은 것 같습니다 담부터 잘 확인 할게용 ㅜㅜ

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.solidconnection.chat.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "websocket")
public record StompProperties(ThreadPool threadPool, HeartbeatProperties heartbeat) {

Copy link
Collaborator

@nayonsoso nayonsoso Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파일 끝 개행이 두줄 되어있는 것 같습니다 😲

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정 완료했습니다!

public record ThreadPool(InboundProperties inbound, OutboundProperties outbound) {

}

public record InboundProperties(int corePoolSize, int maxPoolSize, int queueCapacity) {

}

public record OutboundProperties(int corePoolSize, int maxPoolSize) {

}

public record HeartbeatProperties(long serverInterval, long clientInterval) {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.example.solidconnection.chat.config;

import com.example.solidconnection.chat.config.StompProperties.HeartbeatProperties;
import com.example.solidconnection.chat.config.StompProperties.InboundProperties;
import com.example.solidconnection.chat.config.StompProperties.OutboundProperties;
import com.example.solidconnection.security.config.CorsProperties;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {

private final StompHandler stompHandler;
private final StompProperties stompProperties;
private final CorsProperties corsProperties;

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
List<String> strings = corsProperties.allowedOrigins();
String[] allowedOrigins = strings.toArray(String[]::new);
registry.addEndpoint("/connect").setAllowedOrigins(allowedOrigins).withSockJS();
}

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
InboundProperties inboundProperties = stompProperties.threadPool().inbound();
registration.interceptors(stompHandler).taskExecutor().corePoolSize(inboundProperties.corePoolSize()).maxPoolSize(inboundProperties.maxPoolSize()).queueCapacity(inboundProperties.queueCapacity());
}

@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
OutboundProperties outboundProperties = stompProperties.threadPool().outbound();
registration.taskExecutor().corePoolSize(outboundProperties.corePoolSize()).maxPoolSize(outboundProperties.maxPoolSize());
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(1);
scheduler.setThreadNamePrefix("wss-heartbeat-");
scheduler.initialize();
HeartbeatProperties heartbeatProperties = stompProperties.heartbeat();
registry.setApplicationDestinationPrefixes("/publish");
registry.enableSimpleBroker("/topic").setHeartbeatValue(new long[]{heartbeatProperties.serverInterval(), heartbeatProperties.clientInterval()}).setTaskScheduler(scheduler);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ public enum ErrorCode {
UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."),
MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."),

// socket
UNAUTHORIZED_SUBSCRIBE(HttpStatus.FORBIDDEN.value(), "구독 권한이 없습니다."),
INVALID_ROOM_ID(HttpStatus.BAD_REQUEST.value(), "경로의 roomId가 잘못되었습니다."),

// report
ALREADY_REPORTED_BY_CURRENT_USER(HttpStatus.BAD_REQUEST.value(), "이미 신고한 상태입니다."),

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/secret
12 changes: 12 additions & 0 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ view:
count:
scheduling:
delay: 3000
websocket:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 WebSocket 관련 설정값이 test/resources/application.yml 에 작성되어 있는데, main/resources/application.yml 에 적어주어야 할 듯 합니다 ..!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니 분명 테스트랑 main 둘 다 올렸는데 merge 과정에서 날아간 것 같네요............. ㅜㅜㅜ 다시 올릴게용

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정 완료했습니다!

thread-pool:
inbound:
core-pool-size: 8
max-pool-size: 16
queue-capacity: 1000
outbound:
core-pool-size: 8
max-pool-size: 16
heartbeat:
server-interval: 15000
client-interval: 15000
oauth:
apple:
token-url: "https://appleid.apple.com/auth/token"
Expand Down
Loading