본문 바로가기
Dev/[프로젝트] 2022 Winter Dev Camp

2022 스마일게이트 윈터데브캠프를 통해 성장하기 3 - 구현 : 채팅 서비스

by javapp 자바앱 2023. 2. 28.
728x90

 

Implementation

 

 

제가 맡은 파트는 채팅서버, 접속상태서버입니다.

전체 계획으로는 2주간 채팅서버, 1주 접속서버, 1주 통합, 성능 테스트, 1주 최종발표준비로 세웠습니다.

1/22~1/28 (1주) : 채팅 서버
29 (2주) : 채팅 서버
5 (3주) : 접속서버
12 (4주) : 통합, 성능
19 (5주) : 최종발표준비

 

 

2주간 개발

 

1. 서버 세팅

Spring Initializr

 

 

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

 

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

Kafka 핵심 포인트 (tistory.com)

 

 

로그 경로 운영체제 형식에 맞추어 변경

카프카의 메세지들을 로그 세그먼트 파일에 모아서 디스크에 저장할 디렉토리 위치

 

도커 내 설치 & 실행

 

도커 설치

Docker desktop 설치

[Docker] 도커 시작하기 - 1. 도커 설치하기

docker-compose-single-broker.yml
0.00MB

$ 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. 채팅기능 아키텍처 

  1. 채팅 메시지 MongoDB에 저장
  2. 채팅방내 멤버들의 접속상태 확인
  3. 오프라인 멤버는 푸시서버에 알림 요청
  4. 메시지큐에 전달되어 카프카를 통해 컨슈머에 전달
  5. 접속중인 멤버에게 소켓을 통해 메시지 전달

 

3.2. 채팅 메세지 전송 Flow

/chatting/topic/room/{roomId}

해당 URL에 구독한 클라이언트에게 메시지가 소켓을 통해 전달

 

 

4. 채팅 서비스

  1. 웹소켓 설정
  2. config 설정
    1. Producer "KafkaProducerConfig"
    2. Consumer "KafkaConsumerConfig"
    3. Topic "KafkaTopicConfig"
  3. 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 설정

STOMP 웹소켓 전송

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 👩‍💻👨‍💻

https://velog.io/@jay/software-architecture-chat

댓글