이번에 진행하는 프로젝트에서 채팅 서버 관련 개발을 담당하게 되어 학습한 내용을 정리한 포스팅입니다.
개발 환경
SpringBoot : 3.2.2
JDK : 17
Dependency
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
implementation 'org.springframework.boot:spring-boot-starter-websocket'
- 웹소켓을 사용하기 위한 라이브러리
implementation 'org.springframework.boot:spring-boot-devtools'
- 파일 수정 시 재시작하지 않고 자동 컴파일 될 수 있도록 하기위한 라이브러리
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.24'
- DTO 어노테이션을 줄이기 위한 롬복 라이브러리
만약 lombok 적용이 되지 않는다면 아래 문제를 체크
lombok은 Maven 레포지토리에서 가져오기 때문에 build.gradle에 아래 구문을 추가
repositories {
mavenCentral()
}
Config
@Configuration
@EnableWebSocketMessageBroker
public class WebSockConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/sub");
config.setApplicationDestinationPrefixes("/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*").withSockJS();
}
}
@EnableWebSocketMessageBroker를 추가하여 웹소켓 활성화.
스프링에서 제공하는 내장 메시지 브로커(SimpleBroker)를 사용.
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/sub");
config.setApplicationDestinationPrefixes("/pub");
}
해당 메서드에서 두 가지를 설정함.
1. config.enableSimpleBroker("/sub");
> 메시지 브로커의 Prefix를 등록하는 부분.
> 클라이언트는 토픽을 구독할 시 /sub 경로로 요청해야 함.
2. 도착 경로에 대한 Prefix를 설정
> 클라이언트에서 메시지 발행 시 해당 메시지 매핑에 대한 접두사로 사용됨.
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp").setAllowedOriginPatterns("*").withSockJS();
}
웹소켓 연결에 필요한 Endpoint를 지정함과 동시에 setAllowedOriginPatterns 부분을 애스터리스크(*)로 설정하여 모든 출처에 대한 Cors 설정.
추가로 프로젝트를 진행하면서 로컬에서 테스트를 위해 80번포트를 열어 다른 클라이언트의 접속을 시도했을 때 Cors 정책 에러가 발생했었다. 이 문제를 해결하기 위해 추가적인 Config 클래스를 생성.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("Authorization", "Content-Type")
.exposedHeaders("Custom-Header")
.maxAge(3600);
}
}
DTO
채팅방과 메시지에 대한 두가지 DTO 클래스 생성
ChatRoom
@Data
public class ChatRoom {
private String roomId;
private String name;
public static ChatRoom create(String name) {
ChatRoom chatRoom = new ChatRoom();
chatRoom.roomId = UUID.randomUUID().toString();
return chatRoom;
}
}
채팅방에 대한 객체로써 방번호와 방제목 두가지 필드변수를 가짐.
추가로 create메서드를 추가하여 방 생성관련 로직을 해당 DTO에서 처리.
> 방번호는 고유한 Random UUID를 생성하여 값을 주고, 방제목은 매개변수를 통해 지정하여 생성.
ChatMessage
@Data
public class ChatMessage {
public enum MessageType {
ENTER, TALK, EXIT, MATCH, MATCH_REQUEST;
}
private MessageType type;
private String roomId;
private String sender;
private String message;
}
메시지에 관한 객체로써 enum 열거형을 가지는 메시지 타입, 방번호, 송신자, 메시지 내용 총 4가지의 필드변수를 가짐.
MessageType의 경우 이후 메시지를 처리하는 컨트롤러 부분에서 사용됩니다.
필요한 메시지 처리 로직에 따라 추가해서 사용하면 될 것 같습니다.
Controller
컨트롤러 또한 메시지와 채팅방 관련 2가지 클래스 생성.
ChatController
@RequiredArgsConstructor
@Controller
public class ChatController {
private final SimpMessageSendingOperations messagingTemplate;
private final ChatRoomRepository chatRoomRepository;
@MessageMapping("chat/message")
public void message(ChatMessage message) {
messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message);
}
}
@MessageMapping를 통해 메시지 핸들링 처리.
convertAndSend 메서드는 수신한 메시지를 지정된 토픽으로 BroadCasting하는 기능을 수행하는 것으로 위 코드는 단순하게 핸들링한 메시지를 지정된 토픽으로 메시지를 전달하기만 합니다.
클라이언트에서 메시지 발행 시 /pub/chat/message 경로로 발행해야하며, message에 RoomId를 지정해주어야 합니다.
해당 어노테이션을 활용하여 메시지 발행 시 EndPoint를 별도로 분리해서 관리할 수 있습니다.
또한 매개변수로 수신한 message의 type에 따라 개별 로직을 분기시킬 수도 있습니다.
예를들어 type이 ENTER라면 메시지를 발행하지 않고, TALK일 때만 발행할 수 있습니다.
ChatRoomController
@RequiredArgsConstructor
@RestController
@RequestMapping("/chat")
public class ChatRoomController {
private final ChatRoomRepository chatRoomRepository;
// 모든 채팅방 목록 반환
@GetMapping("/rooms")
@ResponseBody
public List<ChatRoom> room() {
List<ChatRoom> list = chatRoomRepository.findAllRoom();
return list;
}
// 채팅방 생성
@PostMapping("/room")
@ResponseBody
public ChatRoom createRoom(@RequestParam String user1, String user2) {
return chatRoomRepository.createChatRoom(user1, user2);
}
// 특정 채팅방 조회
@GetMapping("/room/{roomId}")
@ResponseBody
public ChatRoom roomInfo(@PathVariable String roomId) {
return chatRoomRepository.findRoomById(roomId);
}
}
채팅방과 관련하여 3가지 Endpoint를 두었습니다.
Repository
@Repository
public class ChatRoomRepository {
private Map<String, ChatRoom> chatRoomMap;
@PostConstruct
private void init() {
chatRoomMap = new LinkedHashMap<>();
}
public List<ChatRoom> findAllRoom() {
List chatRooms = new ArrayList<>(chatRoomMap.values());
Collections.reverse(chatRooms); //최신 순으로 정렬
return chatRooms;
}
public ChatRoom findRoomById(String id) {
return chatRoomMap.get(id);
}
public ChatRoom createChatRoom(String name) {
ChatRoom chatRoom = ChatRoom.create(name);
chatRoomMap.put(chatRoom.getRoomId(), chatRoom);
return chatRoom;
}
}
현재 진행중인 프로젝트에서는 외부 IO작업을 처리할 필요가 없다고 생각하기 때문에 LinkedHashMap형태로 방 정보를 프로세스 내부에서 관리.
@PostConstruct의 경우 의존성 주입이 이루어진 후 초기화를 수행(최초 1번)
고도화
위 프로젝트에서 메시지 브로커 및 메시지 큐의 경우 Springboot 내부 Memory에 존재합니다.
이렇게 프로젝트 내부 리소스를 활용하여 관리할 경우 웹소켓 서버가 N개로 확장될 때 리소스 공유가 불가하며 서로 같은 토픽에 대해 접근하지 못하는 문제점이 발생하게 됩니다.
이런 상황에서 고려해야하는 방법으로는 External MessageBroker 즉 외부 메시지 브로커를 사용하는 것입니다.
외부 브로커를 활용하게 될 경우 토픽 및 구독자에 대해 전역으로 관리할 수 있으므로 i번째 서버에서는 발행 및 구독 요청에 대해 외부 브로커의 메시지 큐를 주고받음으로써 각각의 사용자가 동일한 토픽을 구독할 수 있도록 할 수 있습니다.
아래는 스프링 공식 문서에 나와있는 내부 브로커와 외부 브로커의 흐름도입니다.
내부 브로커 사용 시
외부 브로커 사용 시
외부 브로커 사용 시 가져올 수 있는 이점으로는 메시지 유실 가능성 제거 및 모니터링 등 다양합니다.
예로 라이브 스트리밍 채팅방 접속 시 이전 대화내용을 보기 위해서 내부 브로커를 활용하게 된다면 볼 수 없겠지만 외부 브로커를 활용한다면 이전 대화내역 또한 볼 수 있을 것입니다.
고려해볼만한 외부 브로커 소프트웨어로는 RabbitMQ, Redis, Kafka 등이 있습니다.
정리
Spring Boot에서 STOMP를 활용하여 채팅 서비스를 구현 및 테스트를 비교적 간단하게 해볼 수 있었다.
사실 이번 글을 쓰면서 티스토리한테 배신을 당해 글을 시원하게 한번 날리고 다시 써내려갔는데 제대로 쓰고있는지도 모르겠다. 글에서 분노가 느껴질수도..
다시 돌아와서....
이번 프로젝트에서는 External Broker를 사용하지 않고 스프링 내부 SimpleBroker를 활용하여 내부 프로세스 리소스를 사용했는데 시간이 된다면 Redis 혹은 RabbitMQ같은 외부 브로커를 활용하여 다양한 시도를 해보고 싶다.
추가로 프로젝트를 진행하면서 발생하는 수정사항 및 트러블 슈팅에 대해서도 기록하며 할 예정!
'Develop > SpringBoot' 카테고리의 다른 글
[Swagger] 스프링 3.x Swagger 적용(With. SpringDocs) (1) | 2024.01.31 |
---|---|
[SpringBoot] 웹소켓(WebSocket) (0) | 2024.01.31 |
[Jasypt] application.properties 설정 암호화 (0) | 2024.01.29 |
[채팅 서버] STOMP, Message Broker (1) | 2024.01.24 |
[SpringBoot] 스프링부트 프로젝트 Jar파일 생성 (0) | 2024.01.09 |
개발 기술 블로그, Dev
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!