๐Ÿ“‹ Project/OneStack

[OneStack]ํŒ€ ํ”„๋กœ์ ํŠธ ๊ฐœ๋ฐœ ๊ณผ์ • - WebSocket ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…

Kyle99 2025. 3. 4. 22:45

๋ณธ ํ”„๋กœ์ ํŠธ ๊ฐœ๋ฐœ์ž ๊ณผ์ • ํ•™์›์˜ ํŒŒ์ด๋„ ํ”„๋กœ์ ํŠธ ๊ธฐํš, ๊ฐœ๋ฐœ, ๋ฐฐํฌ ๊ณผ์ •์„ ๋‹ด๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. 

 

 

OneStack

์›์ŠคํƒํšŒ์› ๋„ˆ๋ฌด ์ข‹์•„์š” ์ž‘์„ฑ์ž : ์„œ์œค๋‹ฌ์ฝค โญ๏ธ โญ๏ธ โญ๏ธ โญ๏ธ โญ๏ธ

www.onestack.store


๐Ÿ“Œ WebSocket์ด๋ž€?

WebSocket์€ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์˜ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ํ”„๋กœํ† ์ฝœ์ž…๋‹ˆ๋‹ค. HTTP์™€ ๋‹ฌ๋ฆฌ ์—ฐ๊ฒฐ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.


โœ… WebSocket์˜ ์žฅ์ 

  • ์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ : ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ž์œ ๋กญ๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ๊ณ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ
  • ๋‚ฎ์€ ์ง€์—ฐ ์‹œ๊ฐ„: ๊ธฐ์กด HTTP ์š”์ฒญ/์‘๋‹ต ๋ฐฉ์‹๋ณด๋‹ค ๋น ๋ฅธ ๋ฐ์ดํ„ฐ ์ „์†ก
  • ํšจ์œจ์ ์ธ ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ: ์—ฐ๊ฒฐ ์œ ์ง€๋กœ ์ธํ•œ ๋ฐ˜๋ณต์ ์ธ ์š”์ฒญ ์˜ค๋ฒ„ํ—ค๋“œ ๊ฐ์†Œ
  • ์ตœ์ ํ™”๋œ ๋ฐ์ดํ„ฐ ์ „์†ก: ํ—ค๋” ํฌ๊ธฐ๊ฐ€ ์ž‘์•„ ๋„คํŠธ์›Œํฌ ๋น„์šฉ ์ ˆ๊ฐ

๐Ÿ›  WebSocket ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ตฌํ˜„

WebSocket์„ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ๊ณ„๋กœ ์ง„ํ–‰๋ฉ๋‹ˆ๋‹ค.

1๏ธโƒฃ WebSocket ์„ค์ •

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic"); // ๊ตฌ๋… ๊ฒฝ๋กœ ์„ค์ •
        config.setApplicationDestinationPrefixes("/app"); // ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ๊ฒฝ๋กœ ์„ค์ •
    }
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
               .setAllowedOrigins("https://www.onestack.store")
               .withSockJS(); // SockJS ์ง€์›
    }
}

๐Ÿ”น STOMP ํ”„๋กœํ† ์ฝœ์„ ์ ์šฉํ•˜์—ฌ ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ํ™œ์šฉ

๐Ÿ”น SockJS๋ฅผ ์ง€์›ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ์„ ๋†’์ž„


2๏ธโƒฃ ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ๋„๋ฉ”์ธ

@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
    private Long messageId;
    private String roomId;
    private String sender;
    private String nickname;
    private String message;
    private String type;     // ENTER, TALK, LEAVE
    private LocalDateTime sentAt;
    private String socialType;
    private String profileImage;
}

๐Ÿ”น ๋ฉ”์‹œ์ง€ ์œ ํ˜• (์ž…์žฅ, ๋Œ€ํ™”, ํ‡ด์žฅ) ์ง€์ • ๊ฐ€๋Šฅ

๐Ÿ”น ๋ณด๋‚ธ ์‹œ๊ฐ„๊ณผ ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์ •๋ณด ํฌํ•จ


3๏ธโƒฃ ์ฑ„ํŒ…๋ฐฉ ๋„๋ฉ”์ธ

@Getter @Setter
public class ChatRoom {
    private String roomId;
    private String roomName;
    private int memberNo;
    private int proNo;
    private int estimationNo;
    private int maxUsers;
    private LocalDateTime createdAt;
    private List<ChatMessage> recentMessages;
    private int roomAdmin;
    private String estimationContent;
    private String memberNickname;
    private String proNickname;
    private int progress;
    private String lastMessage;
    private LocalDateTime updatedAt;
    private int unreadCount;
}

๐Ÿ”น ๊ฒฌ์  ์š”์ฒญ๊ณผ ์—ฐ๋™๋œ ์ฑ„ํŒ…๋ฐฉ ์ƒ์„ฑ ๊ฐ€๋Šฅ

๐Ÿ”น ์ฝ์ง€ ์•Š์€ ๋ฉ”์‹œ์ง€ ์ˆ˜ ๋ฐ ์ฑ„ํŒ…๋ฐฉ ์ง„ํ–‰ ์ƒํƒœ ์ €์žฅ


4๏ธโƒฃ ์ฑ„ํŒ… ์ปจํŠธ๋กค๋Ÿฌ

@Controller
@Slf4j
public class ChatController {
    
    @Autowired
    private ChatService chatService;
    
    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @MessageMapping("/chat/message")
    public void sendMessage(@Payload ChatMessage message) {
        chatService.saveMessage(message);
        messagingTemplate.convertAndSend("/topic/chat/room/" + message.getRoomId(), message);
        chatService.updateChatRoom(message.getRoomId());
    }
    
    @GetMapping("/chat/room/{roomId}")
    public String enterRoom(@PathVariable String roomId, Model model, HttpSession session) {
        ChatRoom room = chatService.findRoom(roomId);
        List<ChatMessage> messages = chatService.getRecentMessages(roomId, 50);
        model.addAttribute("room", room);
        model.addAttribute("messages", messages);
        return "chat/room";
    }
}

๐Ÿ”น WebSocket์„ ํ†ตํ•ด ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฐ ์ฑ„ํŒ…๋ฐฉ ์ •๋ณด ์—…๋ฐ์ดํŠธ

๐Ÿ”น ์ฑ„ํŒ…๋ฐฉ ์ž…์žฅ ์‹œ ์ตœ๊ทผ ๋ฉ”์‹œ์ง€ 50๊ฐœ ๋กœ๋“œ


5๏ธโƒฃ ์ฑ„ํŒ… ์„œ๋น„์Šค

@Service
@Slf4j
public class ChatService {
    
    @Autowired
    private ChatMapper chatMapper;
    
    public String createRoom(ChatRoom room) {
        String roomId = UUID.randomUUID().toString();
        room.setRoomId(roomId);
        room.setCreatedAt(LocalDateTime.now());
        chatMapper.createChatRoom(room);
        return roomId;
    }
    
    public void saveMessage(ChatMessage message) {
        message.setSentAt(LocalDateTime.now());
        chatMapper.insertMessage(message);
    }
    
    public List<ChatMessage> getRecentMessages(String roomId, int limit) {
        return chatMapper.getRecentMessages(roomId, limit);
    }
}

๐Ÿ”น DB์— ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•˜์—ฌ ์ฑ„ํŒ… ๋‚ด์—ญ ์œ ์ง€

๐Ÿ”น ์ฑ„ํŒ…๋ฐฉ ์ž…์žฅ ์‹œ ์ตœ๊ทผ ๋ฉ”์‹œ์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ์ฒ˜๋ฆฌ


๐Ÿ’ก WebSocket ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…์˜ ํŠน์ง•

โœ… ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 

  • WebSocket์„ ํ†ตํ•œ ์ฆ‰๊ฐ์ ์ธ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ
  • ์ฑ„ํŒ…๋ฐฉ ์ฐธ์—ฌ์ž ๋ชจ๋‘์—๊ฒŒ ๋™์‹œ ์ „์†ก

โœ… ๋ฉ”์‹œ์ง€ ์˜์†์„ฑ

  • DB์— ๋ฉ”์‹œ์ง€ ์ €์žฅ์œผ๋กœ ์ฑ„ํŒ… ๋‚ด์—ญ ์œ ์ง€
  • ์ฑ„ํŒ…๋ฐฉ ์ž…์žฅ ์‹œ ์ด์ „ ๋ฉ”์‹œ์ง€ ๋กœ๋“œ ๊ฐ€๋Šฅ

โœ… ์ฑ„ํŒ…๋ฐฉ ๊ด€๋ฆฌ

  • ๊ฒฌ์  ์š”์ฒญ๊ณผ ์—ฐ๋™๋œ ์ฑ„ํŒ…๋ฐฉ ์ƒ์„ฑ ๊ฐ€๋Šฅ
  • ์ฐธ์—ฌ์ž ๊ถŒํ•œ ๊ด€๋ฆฌ ๋ฐ ์ฝ์ง€ ์•Š์€ ๋ฉ”์‹œ์ง€ ์นด์šดํŠธ ์ œ๊ณต

โœ… ๋ณด์•ˆ ๊ฐ•ํ™”

  • SockJS๋ฅผ ํ†ตํ•œ WebSocket ํด๋ฐฑ ์ง€์› (๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ํ™•๋ณด)
  • ํ—ˆ์šฉ๋œ ๋„๋ฉ”์ธ์—์„œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅ (CORS ์„ค์ •)
  • ์„ธ์…˜ ๊ธฐ๋ฐ˜ ์ธ์ฆ ์ ์šฉ ๊ฐ€๋Šฅ

๐Ÿ” ๊ฐœ์„  ๊ฐ€๋Šฅ ํฌ์ธํŠธ

  • ๋ฉ”์‹œ์ง€ ํ ๋„์ž… (๋Œ€๋Ÿ‰ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋ฐ ์„œ๋ฒ„ ๋ถ€ํ•˜ ๋ถ„์‚ฐ)
  • ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (ํ‘ธ์‹œ ์•Œ๋ฆผ, ์ด๋ฉ”์ผ ์•Œ๋ฆผ ๋“ฑ)
  • ํŒŒ์ผ ์ „์†ก ๊ธฐ๋Šฅ ์ง€์› (์ด๋ฏธ์ง€/ํŒŒ์ผ ์—…๋กœ๋“œ ๋ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์ œ๊ณต)

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ

WebSocket์„ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ์‹œ์Šคํ…œ์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ ๊ฒฌ์  ์š”์ฒญ๊ณผ ์—ฐ๋™๋œ ์ฑ„ํŒ… ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ์˜๋ขฐ์ธ๊ณผ ์ „๋ฌธ๊ฐ€ ๊ฐ„์˜ ์›ํ™œํ•œ ์†Œํ†ต์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ๋Š” ์ฑ„ํŒ…๋ฐฉ ๋‚ด ๊ฒŒ์‹œํŒ๊ณผ ์บ˜๋ฆฐ๋” ๊ธฐ๋Šฅ ๊ตฌํ˜„์— ๋Œ€ํ•ด ๋‹ค๋ค„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


๐Ÿ”—Spring WebSocket๊ณผ STOMP, ๊ทธ๋ฆฌ๊ณ  Kafka๋ฅผ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 

 

Spring WebSocket๊ณผ STOMP, ๊ทธ๋ฆฌ๊ณ  Kafka๋ฅผ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ํ†ต์‹ 

1. ์›น์†Œ์ผ“(WebSocket)์ด๋ž€?1.1 ์›น์˜ ๊ธฐ๋ณธ ํ†ต์‹  ๋ฐฉ์‹: HTTP์˜ ํ•œ๊ณ„์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ๊ตํ™˜์€ ์ผ๋ฐ˜์ ์œผ๋กœ HTTP๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ํ•˜์ง€๋งŒ HTTP๋Š” ๋น„์—ฐ๊ฒฐ์„ฑ(stateless) ํ”„๋กœํ† ์ฝœ๋กœ,

pids.tistory.com