Implementation
제가 맡은 파트는 채팅서버, 접속상태서버입니다.
전체 계획으로는 2주간 채팅서버, 1주 접속서버, 1주 통합, 성능 테스트, 1주 최종발표준비로 세웠습니다.
1/22~1/28 (1주) : 채팅 서버 |
29 (2주) : 채팅 서버 |
5 (3주) : 접속서버 |
12 (4주) : 통합, 성능 |
19 (5주) : 최종발표준비 |
2주간 개발
1. 서버 세팅
2. 서버 환경
Spring Boot : 2.7.8
- Java : Jdk 11
- build : Gradle
- 설정파일 : application.yml
- 인텔리제이 : 소스 코드 문서 UTF-8
- 디펜던시
- Spring Actuator
- 서비스의 상태 정보를 실시간으로 모니터링 가능
- Lombok
- Spring Boot DevTools
- Spring web
- (WebSocket)
- (mongodb)
- +gson
- Spring Actuator
2. STOMP
웹소켓 위에서 동작 pub/sub 구조를 가진 STOMP 프로토콜을 통해 메시지 전달 실습을 하였습니다.
- stomp
- pub/sub, 헤더값 세팅 - 인증처리
- 채팅방 생성 : Topic생성
- 채팅방 입장 : Topic 구독
- 메시지 : Topic으로 메시지 발송(pub), 메시지 받음 (sub)
2.1. 외부 브로커
설계 단계에서 웹소켓 통신에 이벤트 브로커(메시지 브로커) Kafka를 사용하기로 하였습니다.
2.1.1 카프카 설치
로컬 환경 설치
카프카를 설치하고 server.properties를 수정합니다.
C:\kafka
카프카 서비스 포트는: 9092
listeners: 카프카 브로커가 내부적으로 바인딩하는 주소
advertised.listeners: 카프카 프로듀서, 컨슈머에게 노출할 주소. 설정하지 않을 경우 디폴트로 listeners 설정을 따릅니다.
서버는 localhost로 접근하는 내부 서비스와
BIP라는 IP로 접근하는 외부 서비스만 Kafka에 접근 할 수 있게 하고 싶은경우
listeners=PLAINTEXT://localhost:9092
advertised.listeners==PLAINTEXT://BIP:9092
로그 경로 운영체제 형식에 맞추어 변경
카프카의 메세지들을 로그 세그먼트 파일에 모아서 디스크에 저장할 디렉토리 위치
도커 내 설치 & 실행
도커 설치
Docker desktop 설치
$ docker-compose -f docker-compose-single-broker.yml up
주키퍼, 카프카 실행확인
재실행시 주키퍼 - 카프카 순으로 실행
2.1.2 카프카 실행
카프카를 사용하기 위해서는 주키퍼라는 것을 먼저 실행해야합니다.
리눅스
주키퍼
bin/zookeeper-server-start.sh -daemon config/zookeeper.properties
카프카
bin/kafka-server-start.sh -daemon config/server.properties
이슈
kafka java.io.IOException: 현재 연결은 원격 호스트에 의해 강제로 끊겼습니다
→ C:\tmp\zookeeper 삭제
→ C:\kafka\kafkaexecution-logs 삭제
3. 아키텍처
3.1. 채팅기능 아키텍처
- 채팅 메시지 MongoDB에 저장
- 채팅방내 멤버들의 접속상태 확인
- 오프라인 멤버는 푸시서버에 알림 요청
- 메시지큐에 전달되어 카프카를 통해 컨슈머에 전달
- 접속중인 멤버에게 소켓을 통해 메시지 전달
3.2. 채팅 메세지 전송 Flow
/chatting/topic/room/{roomId}
해당 URL에 구독한 클라이언트에게 메시지가 소켓을 통해 전달
4. 채팅 서비스
- 웹소켓 설정
- config 설정
- Producer "KafkaProducerConfig"
- Consumer "KafkaConsumerConfig"
- Topic "KafkaTopicConfig"
- Producers, Consumers 구현
4.1 의존성 추가
implementation 'org.springframework.kafka:spring-kafka'
testImplementation 'org.springframework.kafka:spring-kafka-test'
4.2. 웹소켓 설정
WebSockConfig
@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker
public class WebSockConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// `/chatting/queue/room/{room_id}'
registry.enableSimpleBroker("/chatting/queue","/chatting/topic");
registry.setApplicationDestinationPrefixes("/chatting/pub");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/ws-chat")
.setAllowedOrigins("*");
registry.addEndpoint("/ws-chat").setAllowedOrigins("*").withSockJS();
}
}
@WebSocketMessageBrokerConfigurer
메시지브로커를 사용해 pub/sub 구조를 가진 STOMP 방식으로 웹소켓 통신을 할 것입니다.
url 앞단이 "/chatting/topic" 인 경로로 구독한 구독자에게 값이 전달됩니다.
4.3. Config 설정
KafkaProducerConfig
@EnableKafka
@Configuration
public class KafkaProducerConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapAddress;
@Bean
public ProducerFactory<String, ChatMessageDto> producerFactory(){
return new DefaultKafkaProducerFactory<>(producerConfigurations());
}
@Bean
public Map<String, Object> producerConfigurations(){
Map<String, Object> configurations = new HashMap<>();
configurations.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
configurations.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configurations.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return configurations;
}
@Bean
public KafkaTemplate<String, ChatMessageDto> kafkaTemplate(){
return new KafkaTemplate<>(producerFactory());
}
@Bean
public ProducerFactory<String, RoomMessageDto> roomProducerFactory(){
return new DefaultKafkaProducerFactory<>(roomProducerConfigurations());
}
@Bean
public Map<String, Object> roomProducerConfigurations(){
Map<String, Object> configurations = new HashMap<>();
configurations.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
configurations.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configurations.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return configurations;
}
@Bean
public KafkaTemplate<String, RoomMessageDto> roomKafkaTemplate(){
return new KafkaTemplate<>(roomProducerFactory());
}
}
Producer 와 Consumer는 각각 2개의 토픽으로 서로 다른 메시지 포맷으로 보내기위해 2개씩 설정하였습니다.
채팅메시지인 ChatMessageDto와 룸 메시지인 RoomMessageDto를 각각의 토픽으로 보내기 위해 각각 2개씩 설정하였습니다.
pub에서 메시지를 보내기위해 전용 KafkaTemplate을 설정하여 @Configuration에서 @Bean 으로 등록합니다.
이는 Producers 에서 KafkaTemplate을 주입시켜 메세지를 보낼때 사용될 것입니다.
configurations.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
Object를 메시지로 보내기 때문에 JsonSerializer으로 직렬화하도록 설정하였습니다.
KafkaConsumerConfig
@EnableKafka
@Configuration
public class KafkaConsumerConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapAddress;
@Value("${spring.kafka.chat-consumer.group-id}")
private String groupId;
@Value("${spring.kafka.room-consumer.group-id}")
private String rGroupId;
@Bean
public ConsumerFactory<String, ChatMessageDto> consumerFactory(){
return new DefaultKafkaConsumerFactory<>(consumerConfigurations(), new StringDeserializer(),
new JsonDeserializer<>(ChatMessageDto.class));
}
private Map<String, Object> consumerConfigurations() {
Map<String, Object> configurations = new HashMap<>();
configurations.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
configurations.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
configurations.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configurations.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,JsonDeserializer.class);
configurations.put(JsonDeserializer.TRUSTED_PACKAGES,"*");
configurations.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest"); // earliest: 전체 , latest: 최신 메시지
return configurations;
}
// 멀티쓰레드에 대한 동기화 제공하는 컨슈머를 생산하기 위한 Factory
@Bean
public ConcurrentKafkaListenerContainerFactory<String, ChatMessageDto> kafkaListenerContainerFactory(){
ConcurrentKafkaListenerContainerFactory<String, ChatMessageDto> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
@Bean
public ConsumerFactory<String, RoomMessageDto> roomConsumerFactory(){
return new DefaultKafkaConsumerFactory<>(roomConsumerConfigurations(), new StringDeserializer(),
new JsonDeserializer<>(RoomMessageDto.class));
}
private Map<String, Object> roomConsumerConfigurations() {
Map<String, Object> configurations = new HashMap<>();
configurations.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapAddress);
configurations.put(ConsumerConfig.GROUP_ID_CONFIG, rGroupId);
configurations.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configurations.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,JsonDeserializer.class);
configurations.put(JsonDeserializer.TRUSTED_PACKAGES,"*");
configurations.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,"latest"); // earliest: 전체 , latest: 최신 메시지
return configurations;
}
}
pub에서 보내진 메시지는 topic에 따라 전해질 것이다. 이때 @KafkaListener를 통해 선별됩니다.
@EnableKafka는 빈에서 @KafkaListener 애노테이션을 검출할 수 있도록 하기위해 필요합니다.
KafkaTopicConfig
@Configuration
public class KafkaTopicConfig {
@Value("${spring.kafka.bootstrap-servers}")
private String bootstrapAddress;
@Value("${kafka.topic.chat-name}")
private String topicChatName;
@Value("${kafka.topic.room-name}")
private String topicRoomName;
@Bean
public KafkaAdmin kafkaAdmin(){
Map<String,Object> configurations = new HashMap<>();
configurations.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG,bootstrapAddress);
return new KafkaAdmin(configurations);
}
@Bean
public NewTopic topic(){
return new NewTopic(topicChatName,1,(short)1);
}
@Bean
public NewTopic roomTopic() {
return TopicBuilder.name(topicRoomName)
.partitions(1)
.replicas(1)
.build();
}
}
토픽을 설정합니다.
토픽을 2가지(채팅메시지, 룸메시지) 를 사용하기 때문에 NewTopic을 2개 생성했습니다.
4.4. Producers, Consumers 구현
Producers
토픽에 메시지전송(이벤트 발행)
@Slf4j
@Component
@RequiredArgsConstructor
public class Producers {
private final KafkaTemplate<String, ChatMessageDto> kafkaTemplate;
@Value("${kafka.topic.chat-name}")
private String topicChatName;
private final KafkaTemplate<String, RoomMessageDto> roomKafkaTemplate;
@Value("${kafka.topic.room-name}")
private String topicRoomName;
private ChatMessageService chatMessageService;
private final ChatRoomService chatRoomService;
public void sendMessage(ChatMessageDto chatMessageDto){
if(chatMessageDto.getMessage_type()== MessageType.FIRST){
RespRoomDto respRoomDto = chatRoomService.getChatRoomInfo(chatMessageDto.getRoom_id()); // 채팅방 무조건 있다고 신뢰
List<String> receivers = respRoomDto.getMembers().stream().map(m -> m.getUserId()).collect(Collectors.toList());
receivers.remove(chatMessageDto.getSender_id());
sendRoomMessage(RoomMessageDto.builder()
.receivers(receivers)
.respRoomDto(respRoomDto)
.build());
}else {
ListenableFuture<SendResult<String, ChatMessageDto>> listenable = kafkaTemplate.send(topicChatName, chatMessageDto); // 보낼 메시지 포맷 변경 해야됨
listenable.addCallback(new ListenableFutureCallback<SendResult<String, ChatMessageDto>>() {
@Override
public void onSuccess(SendResult<String, ChatMessageDto> result) {
log.info("채팅방: {}, 보낸사람: {}, 메시지: {}", chatMessageDto.getRoom_id(), chatMessageDto.getSender_id(), chatMessageDto.getContent());
}
@Override
public void onFailure(Throwable ex) {
log.error("Unable to send message=[" + chatMessageDto.getContent() + "] due to : " + ex.getMessage());
chatMessageService.deleteChat(chatMessageDto.getMessage_id());
log.info("메시지 삭제= {}", chatMessageDto.getMessage_id());
}
});
}
}
public void sendRoomMessage(RoomMessageDto roomMessageDto){
ListenableFuture<SendResult<String, RoomMessageDto>> listenable = roomKafkaTemplate.send(topicRoomName,roomMessageDto);
listenable.addCallback(new ListenableFutureCallback<SendResult<String, RoomMessageDto>>() {
@Override
public void onSuccess(SendResult<String, RoomMessageDto> result) {
log.info("Sent message=[" + roomMessageDto.getRespRoomDto().getRoom_id() + "] with offset=[" + result.getRecordMetadata().offset() + "]");
}
@Override
public void onFailure(Throwable ex) {
log.info("Unable to send message=[" + roomMessageDto.getRespRoomDto().getRoom_id() + "] due to : " + ex.getMessage());
}
});
}
}
채팅메시지를 보낼때 Producers를 호출하여 snedMessage메소드로 채팅메시지를 전달할 것입니다.
sendRoomMessage는 채팅방이 생성될 때 채팅방 정보를 전달할 때 호출하는 메소드입니다.
private final KafkaTemplate<String, ChatMessageDto> kafkaTemplate;
@Value("${kafka.topic.chat-name}")
private String topicChatName;
private final KafkaTemplate<String, RespRoomDto> roomKafkaTemplate;
@Value("${kafka.topic.room-name}")
private String topicRoomName;
각각 용도에 따라 2개의 토픽으로 운영할 생각입니다.
KafkaProducerConfig 클래스에서 설정한 KafkaTemplate이 여기서 메시지 전달용으로 사용됩니다.
Consumers
@Slf4j
@Component
@RequiredArgsConstructor
public class Consumers {
private final SimpMessagingTemplate template;
@KafkaListener(groupId = "${spring.kafka.chat-consumer.group-id}" ,topics="${kafka.topic.chat-name}")
public void listenChat(ChatMessageDto chatMessageDto){
template.convertAndSend("/chatting/topic/room/"+chatMessageDto.getRoom_id(), chatMessageDto);
}
@KafkaListener(groupId = "${spring.kafka.room-consumer.group-id}",topics = "${kafka.topic.room-name}", containerFactory = "kafkaListenerContainerFactory")
public void listenGroupCreation(RoomMessageDto roomMessageDto){
RespRoomDto respRoomDto = roomMessageDto.getRespRoomDto();
for(String userId : roomMessageDto.getReceivers()){
template.convertAndSend("/chatting/topic/new-room/"+userId,respRoomDto);
}
}
}
Producers에서 전달된 메시지는 Consumers에서 토픽에 따라 받습니다.
4.5. 컨트롤러
4.5.1. 채팅 메시지 전달 POST
@Operation(summary = "메시지 전송")
@PostMapping(value="/v1/message", consumes = "application/json",produces = "application/json")
public void sendMessage(@Valid @RequestBody ChatMessageDto chatMessageDto){
if(!chatRoomService.existsRoom(chatMessageDto.getRoom_id())){
throw new CustomAPIException(ErrorCode.ROOM_NOT_FOUND_ERROR, "채팅방이 없음-"+chatMessageDto.getRoom_id());
}
ChatMessageDto savedMessage = chatMessageService.saveChatMessage(chatMessageDto);
producers.sendMessage(savedMessage);
// 비동기 푸시 알림
pushService.pushMessageToUsers(chatMessageDto);
}
POST 요청을 통해 메시지를 전송하는 방법
chatMessageService.saveChatMessage(chatMessageDto): 채팅 메시지를 DB에 저장
producers.sendMessage(): 카프카에 채팅메시지 전달
pushService.pushMessageToUsers(chatMessageDto): 푸시알림
4.5.2. 웹소켓으로 채팅 메시지 전달
@MessageMapping("/chatting/pub")
@Operation(summary = "웹소켓메시지 전송")
public void sendSocketMessage(@Valid @RequestBody ChatMessageDto chatMessageDto){
if(!chatRoomService.existsRoom(chatMessageDto.getRoom_id())){
throw new CustomAPIException(ErrorCode.ROOM_NOT_FOUND_ERROR, "채팅방이 없음-"+chatMessageDto.getRoom_id());
}
ChatMessageDto savedMessage = chatMessageService.saveChatMessage(chatMessageDto);
producers.sendMessage(savedMessage);
// 비동기 푸시 알림
pushService.pushMessageToUsers(chatMessageDto);
}
클라이언트에서 소켓을 통해 메시지를 받습니다.
웹소켓 테스트 결과
4.5.3. 그룹채팅방 생성 메시지
@PostMapping("/v1/group-creation")
public ResponseEntity<RespRoomDto> groupCreation(@RequestHeader("Authorization") String jwt, @RequestBody ReqGroupDto reqGroupDto){
String userId = getTokenToUserId(jwt);
reqGroupDto.setCreator(userId);
RespRoomDto respRoomDto = chatRoomMongoService.createGroup(reqGroupDto);
producers.sendRoomMessage(respRoomDto);
return new ResponseEntity<>(respRoomDto,HttpStatus.CREATED);
}
producers.sendRoomMessage(respRoomDto) 를 사용하여 룸 생성 메시지 전달
4.6. 테스트
apic 사이트를 통해 웹소켓 테스트
웹소켓을 통해 채팅메시지가 잘 전달되면 Messages를 통해 전달받은 메시지를 확인할 수가 있습니다.
5. Spring Boot + MongoDB(MongoTemplate) Chat Messages Pagination
한 채팅방에 대한 채팅메시지 페이지네이션
Path: /chatting/room/v1/history?roomid={roomid}&page={page}
5.1. 구현
ChatMessageController
@GetMapping("/room/v1/history")
public ResponseEntity<APIMessage> chatMessagePagination(
@RequestParam(name = "roomid") String roomId,
@RequestParam(name = "page") int page){
APIMessage apiMessage = new APIMessage();
apiMessage.setMessage(APIMessage.ResultEnum.success);
apiMessage.setData(chatMessageService.chatMessagePagination(roomId,page));
return new ResponseEntity<>(apiMessage, HttpStatus.OK);
}
@RequestParam : 쿼리스트링에 대한 변수 획득
ChatMessageService
public Page<ChatMessageDto> chatMessagePagination(String roomId, int page){
Page<MessageCollection> messageCollectionPage = chatMessageRepository.findByRoomIdWithPagingAndFiltering(roomId, page, SIZE);
Page<ChatMessageDto> chatMessageDtoPage = messageCollectionPage.map(new Function<MessageCollection, ChatMessageDto>() {
@Override
public ChatMessageDto apply(MessageCollection messageCollection) {
return convertEntityToDto(messageCollection);
}
});
return chatMessageDtoPage;
}
ChatMessageRepositoryImpl
@Override
public Page<MessageCollection> findByRoomIdWithPagingAndFiltering(String roomId, int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
Query query = new Query()
.with(pageable)
.skip(pageable.getPageSize() * pageable.getPageNumber()) // offset : ~5, ~10
.limit(pageable.getPageSize());
query.addCriteria(Criteria.where("roomId").is(roomId));
List<MessageCollection> messageCollections = mongoTemplate.find(query, MessageCollection.class);
Page<MessageCollection> messageCollectionPage = PageableExecutionUtils.getPage(
messageCollections,
pageable,
()-> mongoTemplate.count(query.skip(-1).limit(-1), MessageCollection.class) // 정확한 도큐먼트 갯수 구하기 위함
);
return messageCollectionPage;
}
채팅메시지에 대한 페이지내이션이기 때문에 생성순 내림차순으로 정렬 - 참고
mongoTemplate로 offset 계산과 roomId 필터링된 채팅들, 정확한 도큐먼트 갯수 계산
PageableExcutionUtils를 사용하여 최종적으로 Page 리스트 생성 - 참고
5.2. 최종 테스트 결과
{
"message": "success",
"data": {
"content": [
{
"message_type": "TEXT",
"room_id": "ce59b3b7-8eae-4b19-a92f-0f0936d28324",
"sender_id": "유저",
"content": "message254",
"message_id": "63dd174c2ec614711c7c10f4",
"created_at": "2023-02-03T23:16:44.856"
},
{
"message_type": "TEXT",
"room_id": "ce59b3b7-8eae-4b19-a92f-0f0936d28324",
"sender_id": "유저",
"content": "message254",
"message_id": "63dceed28afe2e006c10c788",
"created_at": "2023-02-03T20:24:02.264"
},
{
"message_type": "TEXT",
"room_id": "ce59b3b7-8eae-4b19-a92f-0f0936d28324",
"sender_id": "유저",
"content": "message254",
"message_id": "63dcecea1fbe9b54b861e7a8",
"created_at": "2023-02-03T20:15:54.583"
},
{
"message_type": "TEXT",
"room_id": "ce59b3b7-8eae-4b19-a92f-0f0936d28324",
"sender_id": "유저",
"content": "message254",
"message_id": "63dcece91fbe9b54b861e7a7",
"created_at": "2023-02-03T20:15:53.157"
},
{
"message_type": "TEXT",
"room_id": "ce59b3b7-8eae-4b19-a92f-0f0936d28324",
"sender_id": "유저",
"content": "message254",
"message_id": "63dcecdf1fbe9b54b861e7a6",
"created_at": "2023-02-03T20:15:43.052"
}
],
"pageable": {
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"offset": 0,
"pageSize": 5,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 4,
"totalElements": 16,
"size": 5,
"number": 0,
"sort": {
"empty": false,
"sorted": true,
"unsorted": false
},
"first": true,
"numberOfElements": 5,
"empty": false
}
}
6. JWT 파싱
컨트롤러 요청에서 사용자의 정보가 필요할 경우 jwt 파싱을 통해 userId를 얻을 수 있기 때문에
채팅 서비스에 jwt 파싱 클래스를 생성하기로 하였습니다.
build.gradle
implementation 'io.jsonwebtoken:jjwt:0.9.1'
@Data
@Builder
public class JwtUser {
String email;
String userId;
String nickname;
}
JwtTokenProvider
@Component
public class JwtTokenProvider {
@Value("${token.secret_key}")
private String SECRET_KEY;
public JwtUser getUserInfo(String token) {
if(isAccessToken(token))
{
Map<String, Object> payloads = Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
JwtUser user = JwtUser.builder()
.email(payloads.get("email").toString())
.userId(payloads.get("userId").toString())
.nickname(payloads.get("nickname").toString())
.build();
return user;
}
throw new JwtTokenIncorrectStructureException("토큰 인증 실패");
}
public Boolean isAccessToken(String token) {
try {
Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
}catch (ExpiredJwtException jwtException){
throw new JwtTokenIncorrectStructureException("토큰 만료");
}
catch (SignatureException | MalformedJwtException |
UnsupportedJwtException | IllegalArgumentException jwtException) {
throw new JwtTokenIncorrectStructureException("토큰 인증 실패");
}
return true;
}
public String removeBearer(String bearerToken) {
return bearerToken.replace("Bearer ", "");
}
}
이후 채팅 서비스에 대한 성능테스트, 로직변경과 리팩토링을 진행할 예정입니다!
다른 내용도 있지만 생략,,,
참고
Intro to Apache Kafka with Spring | Baeldung
Sup2's blog-Kafka Cluster 구성하고 Spring Boot에서 Kafka 사용하기 (sup2is.github.io)
Realtime Chat app using Kafka, SpringBoot, ReactJS, and WebSockets - DEV Community 👩💻👨💻
'Dev > [프로젝트] 2022 Winter Dev Camp' 카테고리의 다른 글
2022 스마일게이트 윈터데브캠프를 통해 성장하기 6 - 프로젝트 배포 (0) | 2023.03.04 |
---|---|
2022 스마일게이트 윈터데브캠프를 통해 성장하기 5 - MongoDB 쿼리 자유롭게 사용하기 (0) | 2023.03.03 |
2022 스마일게이트 윈터데브캠프를 통해 성장하기 4 - 구현 : 접속상태 서비스 (0) | 2023.03.01 |
2022 스마일게이트 윈터데브캠프를 통해 성장하기 2 - 프로젝트 설계와 마음가짐 (0) | 2023.02.26 |
2022 스마일게이트 윈터데브캠프를 통해 성장하기 1 - 좋은 목표는 좋은 실행에서 나온다. (0) | 2023.02.25 |
댓글