Skip to content

Commit

Permalink
Merge pull request #31 from mujik-tigers/feat/22-web-socket-enviromnent
Browse files Browse the repository at this point in the history
Feat: 웹 소켓 환경 구축
  • Loading branch information
ghkdgus29 authored Apr 10, 2024
2 parents f3d05ba + 065e7ec commit c96b41b
Show file tree
Hide file tree
Showing 16 changed files with 269 additions and 10 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ dependencies {
implementation "com.redis.om:redis-om-spring:0.8.9"
annotationProcessor "com.redis.om:redis-om-spring:0.8.9"

// web-socket
implementation 'org.springframework.boot:spring-boot-starter-websocket'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/site/youtogether/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,20 @@
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import com.redis.om.spring.annotations.EnableRedisDocumentRepositories;

import lombok.RequiredArgsConstructor;
import site.youtogether.message.application.RedisSubscriber;

@Configuration
@EnableRedisDocumentRepositories(basePackages = "site.youtogether.*")
Expand All @@ -20,6 +25,31 @@ public class RedisConfig {

private final RedisProperties redisProperties;

// 채팅 메시지를 관리하는 채널
@Bean
public ChannelTopic chatChannelTopic() {
return new ChannelTopic("chat");
}

// 채팅 메시지를 처리할 subscriber 메서드 설정
@Bean
public MessageListenerAdapter chatListenerAdapter(RedisSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, "sendMessage");
}

// 채널에 발행된 메시지 처리를 위한 subscriber 메서드를 컨테이너에 등록
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,
MessageListenerAdapter chatListenerAdapter,
ChannelTopic chatChannelTopic) {

RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener(chatListenerAdapter, chatChannelTopic);

return container;
}

@Bean
public JedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisProperties.getHost(),
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/site/youtogether/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package site.youtogether.config;

import static site.youtogether.util.AppConstants.*;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

import lombok.RequiredArgsConstructor;
import site.youtogether.util.interceptor.StompHandshakeInterceptor;

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

private final StompHandshakeInterceptor stompHandshakeInterceptor;

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(STOMP_ENDPOINT).setAllowedOriginPatterns("http://localhost:3000", "https://you-together-web.vercel.app")
.addInterceptors(stompHandshakeInterceptor)
.withSockJS();
}

}
3 changes: 3 additions & 0 deletions src/main/java/site/youtogether/exception/ErrorType.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ public enum ErrorType {
COOKIE_NO_EXISTENCE(HttpStatus.UNAUTHORIZED, "세션 쿠키가 없습니다"),
COOKIE_INVALID(HttpStatus.BAD_REQUEST, "입력으로 들어온 세션 쿠키값과 대응되는 유저 아이디가 없습니다"),

// User
USER_NO_EXISTENCE(HttpStatus.NOT_FOUND, "유저가 존재하지 않습니다"),

// Room
ROOM_NO_EXISTENCE(HttpStatus.NOT_FOUND, "방이 없습니다"),
SINGLE_ROOM_PARTICIPATION_VIOLATION(HttpStatus.BAD_REQUEST, "하나의 방에만 참가할 수 있습니다");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package site.youtogether.exception.user;

import site.youtogether.exception.CustomException;
import site.youtogether.exception.ErrorType;

public class UserNoExistenceException extends CustomException {

public UserNoExistenceException() {
super(ErrorType.USER_NO_EXISTENCE);
}

}
19 changes: 19 additions & 0 deletions src/main/java/site/youtogether/message/ChatMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package site.youtogether.message;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@AllArgsConstructor
@Getter
@Setter
public class ChatMessage {

private final MessageType messageType = MessageType.CHAT;

private String roomCode;
private Long userId;
private String nickname;
private String content;

}
7 changes: 7 additions & 0 deletions src/main/java/site/youtogether/message/MessageType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package site.youtogether.message;

public enum MessageType {

CHAT, PARTICIPANTS_INFO, ROOM_TITLE

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package site.youtogether.message.application;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.RequiredArgsConstructor;
import site.youtogether.message.ChatMessage;

@Service
@RequiredArgsConstructor
public class RedisPublisher {

private final ChannelTopic chatChannelTopic;
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;

public void publishMessage(ChatMessage chatMessage) {
try {
redisTemplate.convertAndSend(chatChannelTopic.getTopic(), objectMapper.writeValueAsString(chatMessage));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package site.youtogether.message.application;

import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.RequiredArgsConstructor;
import site.youtogether.message.ChatMessage;

@Service
@RequiredArgsConstructor
public class RedisSubscriber {

private final ObjectMapper objectMapper;
private final SimpMessageSendingOperations messagingTemplate;

public void sendMessage(String publishMessage) {
try {
ChatMessage chatMessage = objectMapper.readValue(publishMessage, ChatMessage.class);
messagingTemplate.convertAndSend("/sub/messages/rooms/" + chatMessage.getRoomCode(), chatMessage);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package site.youtogether.message.presentation;

import static site.youtogether.util.AppConstants.*;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;
import site.youtogether.exception.user.UserNoExistenceException;
import site.youtogether.message.ChatMessage;
import site.youtogether.message.application.RedisPublisher;
import site.youtogether.user.User;
import site.youtogether.user.infrastructure.UserStorage;

@RestController
@RequiredArgsConstructor
public class MessageController {

private final UserStorage userStorage;
private final RedisPublisher redisPublisher;

@MessageMapping("/messages")
public void handleMessage(ChatMessage chatMessage, SimpMessageHeaderAccessor headerAccessor) {
Long userId = (Long)headerAccessor.getSessionAttributes().get(USER_ID);
User user = userStorage.findById(userId)
.orElseThrow(UserNoExistenceException::new);

chatMessage.setUserId(user.getUserId());
chatMessage.setNickname(user.getNickname());

redisPublisher.publishMessage(chatMessage);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public RoomDetail enter(String cookieValue, String roomCode) {
userStorage.save(user);
roomStorage.save(room);

return new RoomDetail(room, room.getHost());
return new RoomDetail(room, user);
}

}
6 changes: 3 additions & 3 deletions src/main/java/site/youtogether/room/dto/RoomDetail.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ public class RoomDetail {

private final String roomCode;
private final String roomTitle;
private final String hostNickname;
private final String nickname;
private final int capacity;
private final int currentParticipant;
private final boolean passwordExist;

public RoomDetail(Room room, User host) {
public RoomDetail(Room room, User user) {
this.roomCode = room.getCode();
this.roomTitle = room.getTitle();
this.hostNickname = host.getNickname();
this.nickname = user.getNickname();
this.capacity = room.getCapacity();
this.currentParticipant = 1;
this.passwordExist = room.getPassword() != null;
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/site/youtogether/util/AppConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ public final class AppConstants {
public static final int ROOM_CODE_LENGTH = 10;
public static final int COOKIE_VALUE_LENGTH = 20;
public static final String USER_TRACKING_KEY_PREFIX = "user_tracking:";
public static final String STOMP_ENDPOINT = "/stomp";
public static final String USER_ID = "userId";

}
7 changes: 7 additions & 0 deletions src/main/java/site/youtogether/util/Initializer.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package site.youtogether.util;

import static site.youtogether.util.AppConstants.*;

import java.time.LocalDateTime;
import java.util.Set;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
Expand All @@ -24,11 +28,14 @@ public class Initializer implements ApplicationRunner {

private final RoomStorage roomStorage;
private final UserStorage userStorage;
private final RedisTemplate<String, String> redisTemplate;

@Override
public void run(ApplicationArguments args) throws Exception {
roomStorage.deleteAll();
userStorage.deleteAll();
Set<String> keys = redisTemplate.keys(USER_TRACKING_KEY_PREFIX + "*");
redisTemplate.delete(keys);

for (long i = 0; i < NO_PASSWORD_ROOM_COUNT; i++) {
User host = User.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package site.youtogether.util.interceptor;

import static site.youtogether.util.AppConstants.*;

import java.util.Map;
import java.util.stream.Stream;

import org.springframework.http.HttpHeaders;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import lombok.RequiredArgsConstructor;
import site.youtogether.config.property.CookieProperties;
import site.youtogether.exception.cookie.CookieInvalidException;
import site.youtogether.exception.cookie.CookieNoExistenceException;
import site.youtogether.user.infrastructure.UserTrackingStorage;

@Component
@RequiredArgsConstructor
public class StompHandshakeInterceptor implements HandshakeInterceptor {

private final CookieProperties cookieProperties;
private final UserTrackingStorage userTrackingStorage;

@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
String[] cookies = request.getHeaders().get(HttpHeaders.COOKIE).get(0).split("; ");
String cookieValue = Stream.of(cookies)
.filter(cookie -> cookie.startsWith(cookieProperties.getName()))
.map(cookie -> cookie.substring(cookie.indexOf("=") + 1))
.findAny()
.orElseThrow(CookieNoExistenceException::new);

Long userId = userTrackingStorage.findByCookieValue(cookieValue)
.orElseThrow(CookieInvalidException::new);
attributes.put(USER_ID, userId);

return true;
}

@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

}

}
Loading

0 comments on commit c96b41b

Please sign in to comment.