Skip to content

Commit 69a0bc9

Browse files
authored
feat: STOMP Config 추가 (#400)
* feat: Stomp Config 추가 - build.gradle 의존성 추가 - ErrorCode 추가 - config 작성 * feat: 수정사항 반영 - 불필요한 의존성 삭제 - 불필요한 파일 삭제 - 매직넘버 application-variable에서 관리 - 에러코드 추가 - 중복 코드 함수화 * fix: 테스트 코드 에러 해결을 위한 프로퍼티 기본값 추가 * refactor: 수정사항 반영 - 로그 삭제 - @ConfigurationProperties 사용을 위한 파일 추가 - application.yml에 websocket 값 추가 * refactor: 수정사항 반영 - application-variable.yml에 설정값 추가 - code formatting, 오타 수정 - 불필요한 @Sl4fj제거 - StompWebSocketConfig 변수 분리 * chore: secret 해시 수정
1 parent ae9babf commit 69a0bc9

8 files changed

Lines changed: 187 additions & 1 deletion

File tree

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ dependencies {
6666
// Etc
6767
implementation 'org.hibernate.validator:hibernate-validator'
6868
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782'
69+
implementation 'org.springframework.boot:spring-boot-starter-websocket'
6970
}
7071

7172
tasks.named('test', Test) {
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.example.solidconnection.chat.config;
2+
3+
import java.util.Set;
4+
import java.util.concurrent.ConcurrentHashMap;
5+
import org.springframework.context.event.EventListener;
6+
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
7+
import org.springframework.stereotype.Component;
8+
import org.springframework.web.socket.messaging.SessionConnectEvent;
9+
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
10+
11+
@Component
12+
public class StompEventListener {
13+
14+
private final Set<String> sessions = ConcurrentHashMap.newKeySet();
15+
16+
@EventListener
17+
public void connectHandle(SessionConnectEvent event) {
18+
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
19+
sessions.add(accessor.getSessionId());
20+
}
21+
22+
@EventListener
23+
public void disconnectHandle(SessionDisconnectEvent event) {
24+
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
25+
sessions.remove(accessor.getSessionId());
26+
}
27+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.example.solidconnection.chat.config;
2+
3+
import static com.example.solidconnection.common.exception.ErrorCode.AUTHENTICATION_FAILED;
4+
5+
import com.example.solidconnection.auth.token.JwtTokenProvider;
6+
import com.example.solidconnection.common.exception.CustomException;
7+
import com.example.solidconnection.common.exception.ErrorCode;
8+
import io.jsonwebtoken.Claims;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.messaging.Message;
11+
import org.springframework.messaging.MessageChannel;
12+
import org.springframework.messaging.simp.stomp.StompCommand;
13+
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
14+
import org.springframework.messaging.support.ChannelInterceptor;
15+
import org.springframework.stereotype.Component;
16+
17+
@Component
18+
@RequiredArgsConstructor
19+
public class StompHandler implements ChannelInterceptor {
20+
21+
private final JwtTokenProvider jwtTokenProvider;
22+
23+
@Override
24+
public Message<?> preSend(Message<?> message, MessageChannel channel) {
25+
final StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
26+
27+
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
28+
Claims claims = validateAndExtractClaims(accessor, AUTHENTICATION_FAILED);
29+
}
30+
31+
if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
32+
Claims claims = validateAndExtractClaims(accessor, AUTHENTICATION_FAILED);
33+
34+
String email = claims.getSubject();
35+
String destination = accessor.getDestination();
36+
37+
String roomId = extractRoomId(destination);
38+
39+
// todo: roomId 기반 실제 구독 권한 검사 로직 추가
40+
}
41+
42+
return message;
43+
}
44+
45+
private Claims validateAndExtractClaims(StompHeaderAccessor accessor, ErrorCode errorCode) {
46+
String bearerToken = accessor.getFirstNativeHeader("Authorization");
47+
if (bearerToken == null || !bearerToken.startsWith("Bearer ")) {
48+
throw new CustomException(errorCode);
49+
}
50+
String token = bearerToken.substring(7);
51+
return jwtTokenProvider.parseClaims(token);
52+
}
53+
54+
private String extractRoomId(String destination) {
55+
if (destination == null) {
56+
throw new CustomException(ErrorCode.INVALID_ROOM_ID);
57+
}
58+
String[] parts = destination.split("/");
59+
if (parts.length < 3 || !parts[1].equals("topic")) {
60+
throw new CustomException(ErrorCode.INVALID_ROOM_ID);
61+
}
62+
return parts[2];
63+
}
64+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.example.solidconnection.chat.config;
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties;
4+
5+
@ConfigurationProperties(prefix = "websocket")
6+
public record StompProperties(ThreadPool threadPool, HeartbeatProperties heartbeat) {
7+
8+
public record ThreadPool(InboundProperties inbound, OutboundProperties outbound) {
9+
10+
}
11+
12+
public record InboundProperties(int corePoolSize, int maxPoolSize, int queueCapacity) {
13+
14+
}
15+
16+
public record OutboundProperties(int corePoolSize, int maxPoolSize) {
17+
18+
}
19+
20+
public record HeartbeatProperties(long serverInterval, long clientInterval) {
21+
22+
}
23+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.example.solidconnection.chat.config;
2+
3+
import com.example.solidconnection.chat.config.StompProperties.HeartbeatProperties;
4+
import com.example.solidconnection.chat.config.StompProperties.InboundProperties;
5+
import com.example.solidconnection.chat.config.StompProperties.OutboundProperties;
6+
import com.example.solidconnection.security.config.CorsProperties;
7+
import java.util.List;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.context.annotation.Configuration;
10+
import org.springframework.messaging.simp.config.ChannelRegistration;
11+
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
12+
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
13+
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
14+
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
15+
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
16+
17+
@Configuration
18+
@EnableWebSocketMessageBroker
19+
@RequiredArgsConstructor
20+
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
21+
22+
private final StompHandler stompHandler;
23+
private final StompProperties stompProperties;
24+
private final CorsProperties corsProperties;
25+
26+
@Override
27+
public void registerStompEndpoints(StompEndpointRegistry registry) {
28+
List<String> strings = corsProperties.allowedOrigins();
29+
String[] allowedOrigins = strings.toArray(String[]::new);
30+
registry.addEndpoint("/connect").setAllowedOrigins(allowedOrigins).withSockJS();
31+
}
32+
33+
@Override
34+
public void configureClientInboundChannel(ChannelRegistration registration) {
35+
InboundProperties inboundProperties = stompProperties.threadPool().inbound();
36+
registration.interceptors(stompHandler).taskExecutor().corePoolSize(inboundProperties.corePoolSize()).maxPoolSize(inboundProperties.maxPoolSize()).queueCapacity(inboundProperties.queueCapacity());
37+
}
38+
39+
@Override
40+
public void configureClientOutboundChannel(ChannelRegistration registration) {
41+
OutboundProperties outboundProperties = stompProperties.threadPool().outbound();
42+
registration.taskExecutor().corePoolSize(outboundProperties.corePoolSize()).maxPoolSize(outboundProperties.maxPoolSize());
43+
}
44+
45+
@Override
46+
public void configureMessageBroker(MessageBrokerRegistry registry) {
47+
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
48+
scheduler.setPoolSize(1);
49+
scheduler.setThreadNamePrefix("wss-heartbeat-");
50+
scheduler.initialize();
51+
HeartbeatProperties heartbeatProperties = stompProperties.heartbeat();
52+
registry.setApplicationDestinationPrefixes("/publish");
53+
registry.enableSimpleBroker("/topic").setHeartbeatValue(new long[]{heartbeatProperties.serverInterval(), heartbeatProperties.clientInterval()}).setTaskScheduler(scheduler);
54+
}
55+
}

src/main/java/com/example/solidconnection/common/exception/ErrorCode.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ public enum ErrorCode {
112112
UNAUTHORIZED_MENTORING(HttpStatus.FORBIDDEN.value(), "멘토링 권한이 없습니다."),
113113
MENTORING_ALREADY_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "이미 승인 또는 거절된 멘토링입니다."),
114114

115+
// socket
116+
UNAUTHORIZED_SUBSCRIBE(HttpStatus.FORBIDDEN.value(), "구독 권한이 없습니다."),
117+
INVALID_ROOM_ID(HttpStatus.BAD_REQUEST.value(), "경로의 roomId가 잘못되었습니다."),
118+
115119
// report
116120
ALREADY_REPORTED_BY_CURRENT_USER(HttpStatus.BAD_REQUEST.value(), "이미 신고한 상태입니다."),
117121

src/main/resources/secret

src/test/resources/application.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ view:
4242
count:
4343
scheduling:
4444
delay: 3000
45+
websocket:
46+
thread-pool:
47+
inbound:
48+
core-pool-size: 8
49+
max-pool-size: 16
50+
queue-capacity: 1000
51+
outbound:
52+
core-pool-size: 8
53+
max-pool-size: 16
54+
heartbeat:
55+
server-interval: 15000
56+
client-interval: 15000
4557
oauth:
4658
apple:
4759
token-url: "https://appleid.apple.com/auth/token"

0 commit comments

Comments
 (0)