๐™จ๐™ฅ๐™ง๐™ž๐™ฃ๐™œ ๐™—๐™ค๐™ค๐™ฉ + ๐™Ž๐™๐™Š๐™ˆ๐™‹

uuuouuoยท2022๋…„ 5์›” 11์ผ
0
post-thumbnail

๐Ÿ“ STOMP๋ฅผ ์ด์šฉํ•œ Socket ํ†ต์‹  ๊ตฌํ˜„


๐Ÿ’ก STOMP (Simple Text Oriented Messaging Protocol)

  • ๋ฉ”์„ธ์ง• ์ „์†ก์„ ํšจ์œจ์ ์œผ๋กœ ํ•˜๊ธฐ ์œ„ํ•ด ํƒ„์ƒํ•œ ํ”„๋กœํ† ์ฝœ
  • Text ์ง€ํ–ฅ ํ”„๋กœํ† ์ฝœ์ด๋‚˜, Message Payload์—๋Š” Text or Binary ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Œ
  • ๋ฉ”์„ธ์ง€ ๋ธŒ๋กœ์ปค๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ pub / sub ๊ตฌ์กฐ๋กœ ๋˜์–ด์žˆ์–ด ๋ฉ”์„ธ์ง€๋ฅผ ์ „์†กํ•˜๊ณ  ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ถ€๋ถ„์ด ํ™•์‹คํ•จ
    โžก pub / sub (๋ฐœํ–‰ ๋ฐ ๊ตฌ๋…) : ๋ฉ”์„ธ์ง€๋ฅผ ๋ฐœํ–‰ํ•˜๋Š” ์ฃผ์ฒด์™€ ์†Œ๋น„ํ•˜๋Š” ์ฃผ์ฒด๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์„ธ์ง• ๋ฐฉ๋ฒ•
  • ๊ฐœ๋ฐœ์ž ์ž…์žฅ์—์„œ ๋ช…ํ™•ํ•˜๊ฒŒ ์ธ์ง€ํ•˜๊ณ  ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ์Œ
  • ์ด์ „์—๋Š” WebSocketSession์— ์ง์ ‘ ๋ฉ”์„ธ์ง€๋ฅผ ์ „๋‹ฌํ–ˆ์ง€๋งŒ, STOMP๋Š” ํŠน์ • ํŒจํ„ด(๋ฌธ์ž์—ด) ๊ตฌ๋…์„ ํ†ตํ•ด WebSocketSession์— ์ง์ ‘ ๋‹ค๋ฃจ์ง€ ์•Š๊ณ , ์ปจํŠธ๋กค๋Ÿฌ๋กœ ์š”์ฒญ
    โžก ์†Œ์ผ“๊ณผ ๋Š์Šจํ•œ ์—ฐ๊ฒฐ๋กœ ๊ฐ€๋ฒผ์›Œ์ง

    Websocket ์œ„์—์„œ ๋™์ž‘ํ•˜๋Š” ํ”„๋กœํ† ์ฝœ๋กœ์จ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ์ „์†กํ•  ๋ฉ”์„ธ์ง€์˜ ์œ ํ˜•, ํ˜•์‹, ๋‚ด์šฉ์„ ์ •์˜ํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜.

โœ” Spring ์—์„œ ์ง€์›ํ•˜๋Š” STOMP

  • Spring์—์„œ STOMP๊ฐ€ ๋‚ด์žฅํ•˜๊ณ  ์žˆ๋Š” Broker์˜ ์—ญํ• 
    - ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ๋ฉ”์„ธ์ง€๋ฅผ ๋ณด๋‚ด๋ฉด ๊ตฌ๋… ์ฃผ์†Œ๋ฅผ Broker์—๊ฒŒ ์ „๋‹ฌ
    - Broker๋Š” ํ•ด๋‹น ์ฃผ์†Œ๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ๋Š” ํด๋ผ์ด์–ธํŠธ๋“ค์—๊ฒŒ ๋ฉ”์„ธ์ง€ ์ „๋‹ฌ
    โžก HTTP ๊ธฐ๋ฐ˜์˜ ๋ณด์•ˆ ์„ค์ •๊ณผ ๊ณตํ†ต๋œ ๊ฒ€์ฆ ๋“ฑ ์ ์šฉ ๊ฐ€๋Šฅ

โœ” STOMP ํ๋ฆ„

  1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์—๊ฒŒ ์•…์ˆ˜ ์š”์ฒญ (์†Œ์ผ“, ํ† ํฐ ๋ณด๋‚ด๊ธฐ)
  2. ์„œ๋ฒ„๊ฐ€ ์ปค๋„ฅ์…˜ ์—ด์–ด์คŒ (์†Œ์ผ“ ์—ฐ๊ฒฐ โžก ํ•ด๋‹น ๊ตฌ๋… ์ฃผ์†Œ ํ†ต์‹  ๊ฐ€๋Šฅํ•œ ์ƒํƒœ)
  3. ๋ฐœํ–‰ ์ฃผ์†Œ(์ปจํŠธ๋กค๋Ÿฌ)๋กœ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ์š”์ฒญํ•˜๋ฉด, ์„œ๋ฒ„์—์„œ ํ•ด๋‹น ๊ตฌ๋… ์ฃผ์†Œ๋กœ ๋ฉ”์„ธ์ง€ ์ „์†ก

๐Ÿ’ป Spring + STOMP๋ฅผ ์ด์šฉํ•œ Socket ํ†ต์‹  ๊ตฌํ˜„

โœ” bulil.gradle์— ์ถ”๊ฐ€

	implementation 'org.springframework.boot:spring-boot-starter-websocket'
	implementation 'org.webjars:sockjs-client:1.1.2'
	implementation 'org.webjars:stomp-websocket:2.3.3-1'

โœ” WebSocketConfig ํŒŒ์ผ

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
  	public void registerStompEndpoints(StompEndpointRegistry registry) {
    	// websocket์— ์ ‘์†ํ•˜๊ธฐ ์œ„ํ•œ endpoint ์„ค์ •, ๋„๋ฉ”์ธ์ด ๋‹ค๋ฅธ ์„œ๋ฒ„์—์„œ๋„ ์ ‘์† ๊ฐ€๋Šฅํ•˜๋„๋ก CORS ์„ค์ •
    	registry.addEndpoint("/ws-stomp") // WebSocket ๋˜๋Š” SockJS Client๊ฐ€ ์›น์†Œ์ผ“ ํ•ธ๋“œ์…ฐ์ดํฌ ์ปค๋„ฅ์…˜์„ ์ƒ์„ฑํ•  ๊ฒฝ๋กœ
        .setAllowedOriginPatterns("*")
        .withSockJS(); // sockJS ๋“ฑ๋ก
  }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {

        registry.setApplicationDestinationPrefixes("/pub"); //client์—์„œ SEND ์š”์ฒญ ์ฒ˜๋ฆฌ (์ˆ˜์‹ )
        registry.enableSimpleBroker("/sub"); //ํ•ด๋‹น ๊ฒฝ๋กœ๋กœ SimpleBroker ๋“ฑ๋ก, SimpleBroker๋Š” ํ•ด๋‹น ๊ฒฝ๋กœ๋ฅผ ๊ตฌ๋…ํ•˜๋Š” client์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌ (๋ฐœ์‹ )


    }
     									.
										.
                                        .
}

โœ” ์ฑ„ํŒ…๋ฐฉ Entity

@Entity
@ApiModel(value = "๊ฐœ์ธ ์ฑ„ํŒ…๋ฃธ ์ •๋ณด", description = "๊ฐœ์ธ ์ฑ„ํŒ…๋ฃธ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.")
public class ChatRoom {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "CHAT_ROOM_NO")
  @ApiModelProperty(value = "๊ฐœ์ธ ์ฑ„ํŒ…๋ฐฉ ๋ฒˆํ˜ธ")
  private Integer no;

  @ManyToOne(fetch = LAZY)
  @JoinColumn(name="PUBLISHER_NO")
  @ApiModelProperty(value = "๋ณด๋‚ด๋Š” ์‚ฌ๋žŒ")
  private User publisher;

  @ManyToOne(fetch = LAZY)
  @JoinColumn(name="SUBSCRIBER_NO")
  @ApiModelProperty(value = "๋ฐ›๋Š” ์‚ฌ๋žŒ")
  private User subscriber;

}

โœ” ์ฑ„ํŒ… ๋ฉ”์„ธ์ง€ Entity

@Entity
@ApiModel(value = "์ฑ„ํŒ… ๋ฉ”์„ธ์ง€ ์ •๋ณด", description = "์ฑ„ํŒ… ๋ฉ”์„ธ์ง€ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.")
public class ChatMessage {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "CHAT_MESSAGE_NO")
  @ApiModelProperty(value = "์ฑ„ํŒ… ๋ฉ”์„ธ์ง€ ๋ฒˆํ˜ธ")
  private Integer no;

  @ManyToOne(fetch = LAZY)
  @JoinColumn(name="PROJECT_ROOM_NO")
  @ApiModelProperty(value = "์ฑ„ํŒ…๋ฐฉ ๋ฒˆํ˜ธ")
  private ChatRoom chatRoom;

  @ManyToOne(fetch = LAZY)
  @JoinColumn(name="USER_NO")
  @ApiModelProperty(value = "์œ ์ € ๋ฒˆํ˜ธ")
  private User user;

  @ApiModelProperty(value = "์ฑ„ํŒ… ๋‚ด์šฉ")
  private String content;

  @ApiModelProperty(value = "์ „์†ก ์‹œ๊ฐ„")
  private LocalDateTime time;

}

โœ” ์œ ์ € Entity

package com.ssafy.proma.model.entity.user;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Builder;
import lombok.Getter;

import javax.persistence.*;

@Getter
@Entity
@Builder
@NoArgsConstructor
@ApiModel(value = "ํšŒ์›์ •๋ณด", description = "ํšŒ์›์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.")
public class User {

    @Id
    @Column(name = "USER_NO", nullable = false, length = 15)
    @ApiModelProperty(value = "ํšŒ์› ๋ฒˆํ˜ธ")
    private String no;

    @Column(length = 15)
    @ApiModelProperty(value = "ํšŒ์› ๋‹‰๋„ค์ž„")
    private String nickname;

        								.
										.
                                        .
}

โœ” ์ฑ„ํŒ…๋ฐฉ Controller

@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class ChatRoomController {

  private final ChatService chatService;

  @ApiOperation(value = "๊ฐœ์ธ ์ฑ„ํŒ… ์ƒ์„ฑ ๋ฐ ์กฐํšŒ", notes = "ํ•ด๋‹น ์œ ์ €์™€ ๊ฐœ์ธ ์ฑ„ํŒ…๋ฐฉ ์ƒ์„ฑ ๋ฐ ์กฐํšŒ")
  @GetMapping("/room/user/{subNo}")
  public ResponseEntity<Map<String, Object>> getChatRoom(@PathVariable String subNo
  	, @RequestParam(required = false) Integer lastMsgNo) {
    
    Map<String, Object> result = new HashMap<>();
    HttpStatus status = HttpStatus.ACCEPTED;

    try{
      result = chatService.getChatRoom(subNo, lastMsgNo);

      if(result.get("message").equals(CHATROOM_SUCCESS_MESSAGE)) {
        status = HttpStatus.OK;
      }
    } catch (Exception e) {
      result.put("message", CHATROOM_ERROR_MESSAGE);
      status = HttpStatus.INTERNAL_SERVER_ERROR;
    }
    return new ResponseEntity<>(result, status);
  }
}
  • ์ฑ„ํŒ…๋ฐฉ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ์ฑ„ํŒ…๋ฐฉ ์ƒ์„ฑ ๋ฐ ์กฐํšŒ (Service๋Š” ์ƒ๋žต)

โœ” ์ฑ„ํŒ… ๋ฉ”์„ธ์ง€ Controller

@Controller
@RequiredArgsConstructor
public class ChatMessageController {

    private final SimpMessageSendingOperations messagingTemplate;
    private final ChatService chatService;

    @ApiOperation(value = "์ฑ„ํŒ… ๋ฉ”์„ธ์ง€ ์ „์†ก", notes = "ํ•ด๋‹น ์ฑ„ํŒ…๋ฐฉ์œผ๋กœ ๋ฉ”์„ธ์ง€ ์ „์†ก")
    @MessageMapping("/chat/msg")
    public void privateMessage(@RequestBody ChatMessageReq message) {
        ChatMessageRes response = chatService.savePrivateMessage(message);
        messagingTemplate.convertAndSend("/sub/chat/room/user/" + response.getRoomNo(), response);
    }
}

โœ” ์ฑ„ํŒ… ๋ฉ”์„ธ์ง€ Service

@Service
@RequiredArgsConstructor
public class ChatService {

  private final UserRepository userRepository;
  private final ChatRoomRepository chatRoomRepository;
  private final ChatMessageRepository chatMessageRepository;

  public ChatMessageRes savePrivateMessage(ChatMessageReq request) {

    chatRoom chatRoom = findChatRoom(request.getRoomNo());
    User user = findUser(request.getPubNo());
    String content = request.getContent();
    LocalDateTime time = LocalDateTime.now();

    ChatMessageRes response = new ChatMessageRes(chatRoom.getNo(), user.getNo(),
            user.getNickname(), content, time);

    ChatMessage chatMessage = ChatMessageDto.toPrivateMsgEntity(chatRoom, user, content, time);
    chatMessageRepository.save(chatMessage);

    System.out.println("์ฑ„ํŒ…์ €์žฅ์™„๋ฃŒ.");

    return response;

  }
}

โœ” ์ฑ„ํŒ…๋ฐฉ ํ™”๋ฉด ์ผ๋ถ€๋ถ„

<div class="container" id="app" v-cloak>
    <div>
        <h2>์ฑ„ํŒ…๋ฐฉ</h2>
    </div>
    <div class="input-group">
        <div class="input-group-prepend">
            <label class="input-group-text">๋‚ด์šฉ</label>
        </div>
        <input type="text" class="form-control" v-model="content" v-on:keypress.enter="sendMessage">
        <div class="input-group-append">
            <button class="btn btn-primary" type="button" @click="sendMessage">๋ณด๋‚ด๊ธฐ</button>
        </div>
    </div>
    <ul class="list-group">
        <li class="list-group-item" v-for="message in messages">
            {{message.nickname}} - {{message.content}} {{message.time}}</a>
        </li>
    </ul>
    <div></div>
</div>
<script>
    var sock = new SockJS("http://localhost:8080/ws-stomp");
    var ws = Stomp.over(sock);
    var reconnect = 0;
    // vue.js
    var vm = new Vue({
        el: '#app',
        data: {
            roomNo: '',
            pubNo: '',
            content: '',
            messages: []
        },
        created() {
            this.roomNo = "${roomNo}";
            this.pubNo = "${userNo}";
        },
        methods: {
            sendMessage: function() {
                ws.send("/pub/chat/msg", {"JWT":""}, JSON.stringify({roomNo:this.roomNo, pubNo:this.pubNo, content:this.content}));
                this.message = '';
            },
            recvMessage: function(recv) {
                this.messages.unshift({"nickname":recv.nickname,"content":recv.content, "time":recv.time})
            }
        }
    });

    function connect() {
        // pub/sub event
        ws.connect({"JWT":""}, function(frame) {
            ws.subscribe("/sub/chat/room/user/"+vm.$data.roomNo, function(message) {
                var recv = JSON.parse(message.body);
                vm.recvMessage(recv);
            });
        }, function(error) {
            if(reconnect++ <= 5) {
                setTimeout(function() {
                    console.log("connection reconnect");
                    sock = new SockJS("http://localhost:8080/ws-stomp");
                    ws = Stomp.over(sock);
                    connect();
                },10*1000);
            }
        });
    }
    connect();
</script>

0๊ฐœ์˜ ๋Œ“๊ธ€