Reactive WebSocket Backend application
리액티브 웹소켓 백엔드 애플리케이션
🤫 그 외 엄청 중요하거나 목표한 바는 아니지만..
🌐 web
: 회원, 채팅 저장, 채팅 기록 조회 등 영속성과 관련된 기능 담당
Spring Boot 3, Spring Web (6.0.11)
Spring Data JPA, Spring Data MongoDB
MariaDB, MongoDB
Spring Security
Spring Session Data Redis
Thymeleaf + Javascript + WebSocket
💬 message
: 채팅방 구독, 메시지 발행, 메시지 소비 등 채팅과 관련된 주요 기능 담당
Spring Boot 3, Spring WebFlux (6.0.11)
Reactive WebSocket
Spring Data MongoDB
프로젝트 패키지 : web
web
🔐 인증
├── auth : 어플리케이션 인증 │ ├── app : 어플리케이션 자체 인증 설정 │ │ ├── CustomAuthenticationProvider.java │ │ ├── CustomAuthenticationSuccessHandler.java │ │ ├── CustomUserDetailsService.java │ │ ├── CustomUserDetailsUserAdaptor.java │ │ └── PasswordEncoderConfig.java │ ├── argumentresolver │ │ ├── HasMember.java │ │ └── LoginMemberArgumentResolver.java │ ├── config │ │ └── SpringSecurityConfig.java │ ├── dto : 세션에 인증 정보를 담기 위한 DTO │ │ ├── AuthValidRetrieveRequest.java │ │ ├── AuthValidRetrieveResponse.java │ │ ├── CustomOAuth2Member.java │ │ └── SessionMember.java │ ├── exception │ │ ├── CustomAuthException.java │ │ └── ExceptionAuthController.java │ ├── oauth2 : OAuth2 인증 설정 │ │ └── CustomOAuth2UserService.java │ ├── rest │ │ └── AuthController.java │ ├── session │ │ └── SpringHttpSessionClusterConfig.java : 세션 스토리지 REDIS 설정 │ └── web │ └── AuthWebController.java
web
👤 회원
└── member ├── dto │ ├── MemberJoinRequest.java │ ├── MemberJoinResponse.java │ ├── MemberLoginRequest.java │ └── MemberLoginResponse.java ├── model │ ├── AppRole.java │ ├── BaseTime.java │ └── Member.java : 회원 엔티티 ├── repository │ ├── MemberRepository.java │ └── jpa │ └── MemberJpaRepository.java └── service ├── MemberService.java └── MemberServiceImpl.java
web
🗣️ 채팅
├── chat │ ├── dto │ │ └── ChatExceptionResponse.java │ ├── exception │ │ ├── CustomChatException.java │ │ ├── ExceptionChatRestControllerV1.java │ │ └── ExceptionChatWebController.java │ ├── rest │ │ └── ChatLogController.java : 채팅 로그 조회 REST API 컨트롤러 │ └── web │ └── ChatWebController.java : 채팅, 채팅방, 채팅 로그 view 용도 컨트롤러 ├── message │ ├── model │ │ ├── MessagePayload.java : 채팅 메시지 엔티티 │ │ └── MessageType.java │ ├── repository │ │ ├── MessageRepository.java │ │ └── mongodb │ │ └── MessageMongoRepository.java │ └── service │ ├── MessageService.java │ └── MessageServiceImpl.java └── room ├── dto │ ├── RoomCreateRequest.java │ ├── RoomCreateResponse.java │ ├── RoomMemberResponse.java │ ├── RoomRetrieveResponse.java │ ├── RoomStatusRequest.java │ ├── RoomStatusResponse.java │ └── RoomsRetrieveResponse.java ├── exception │ ├── ExceptionRoomController.java │ ├── RoomError.java │ └── RoomException.java ├── model │ ├── Room.java : 채팅방 엔티티 │ ├── RoomMember.java : 채팅방 사용자 엔티티 │ ├── RoomMemberStatus.java │ ├── RoomPublic.java │ ├── RoomRole.java │ └── RoomStatus.java ├── repository │ ├── RoomMemberRepository.java │ ├── RoomRepository.java │ └── jpa │ ├── RoomJpaRepository.java │ └── RoomMemberJpaRepository.java ├── rest │ └── RoomController.java : 채팅방 REST API 컨트롤러 └── service ├── RoomService.java └── RoomServiceImpl.java
프로젝트 패키지 : message
message
🗣️ 채팅
├── auth : 채팅방 입장을 위한 회원 기본 인증 통신 │ ├── config │ │ └── SpringSecurityConfig.java │ ├── dto │ │ ├── AuthValidRetrieveRequest.java │ │ └── AuthValidRetrieveResponse.java │ └── service │ ├── AuthService.java │ └── AuthServiceImpl.java ├── broker │ ├── config │ │ ├── KafkaConsumerConfig.java │ │ └── KafkaProducerConfig.java │ ├── dto │ │ ├── BrokerChatMessage.java : 카프카로 송수신하는 DTO │ │ └── BrokerChatSendRequest.java : 외부에서 카프카로 전송 요청하는 DTO │ └── kafka │ ├── KafkaChatConsumerAdaptor.java │ └── KafkaChatPublishAdaptor.java ├── chat : 채팅을 위한 payload │ ├── ChatService.java │ ├── dto │ │ ├── BrokerRequest.java │ │ ├── ChannelSubscribe.java │ │ ├── Identifier.java │ │ ├── MessageRequest.java │ │ └── MessageResponse.java │ └── model │ ├── MessagePayload.java │ └── MessageType.java ├── message │ ├── model │ │ ├── MessagePayload.java │ │ └── MessageType.java │ └── repository : 채팅 메시지 저장을 위한 MongoRepository 인터페이스 │ ├── MessageRepository.java │ └── mongo │ └── MessageCrudRepository.java ├── contants │ ├── AuthConstant.java │ └── SimpleConfigConstant.java ├── utils │ └── XSSFilter.java └── websocket ├── MessageWebSocketHandler.java ├── broadcaster │ ├── MessageBroadcaster.java : 메시지 브로드캐스터 │ └── MessageFlux.java : 채널과 세션을 관리하는 FluxSink ├── config │ └── CustomWebSocketConfig.java : WebSocketHandler 구현 ├── dto │ ├── CommandType.java │ ├── Identifier.java │ ├── WebSocketMessageMetadata.java │ ├── WebSocketRequest.java │ └── WebSocketResponse.java ├── roommessage │ ├── KafkaMessageWebSocketHandler.java : 카프카를 백엔드로 두는 웹소켓 핸들러 구현 │ └── StandaloneMessageWebSocketHandler.java : 단독으로 메시지를 송수신 처리하는 웹소켓 핸들러 구현 └── subscription └── SubscriptionManager.java : 채팅방 입장 관리
standalone 모드 기준
🚪 채팅방 입장 ⇢ 웹소켓 세션 관리
+---------+
| web 모듈 |
+---------+
|
| (웹소켓 세션 생성)
v
+----------------+
|WebSocketSession|
+----------------+
|
| (구독 요청: CommandType.SUBSCRIBE)
v
+----------------------+ +-----------------------------------------+
|SubscriptionManager | ---------------> | Map<String, Set<WebSocketSession>> |
|----------------------| (채팅방과 세션 저장) | 채팅방 식별자, 웹소켓 세션 |
|addSubscription() | <--------------- +-----------------------------------------+
+----------------------+
|
| (세션 정보 저장)
v
+------------+ +------------+ +-----------+
|WebSocket |------>|MessageFlux |------>|FluxSink |
|Session | |addSink() | |create() |
+------------+ +------------+ +-----------+
🕊️ 채팅 메시지 전송 ⇢ 웹소켓 Flux 콜백
+---------+
| web 모듈 |
+---------+
|
| (메시지 발송)
v
+----------------+
|WebSocketSession|
+----------------+
|
| (메시지 발송 요청: CommandType.MESSAGE)
v
+--------------------------------+ +-------------------------+
|MessageBroadcaster | -------------------------------------> |SubscriptionManager |
|--------------------------------| (채팅방 식별자로 같은 채팅방의 세션획득) |-------------------------|
|broadcastMessageToSubscribers() | <------------------------------------- |getSubscriptions(channel)|
+--------------------------------+ +-------------------------+
|
| (채팅방 내 세션에 대한 각 메시지 전송)
v
+------------+ +-----------+ +-----------+
|WebSocket |------>|MessageFlux|---------------->|FluxSink |
|Session | |getSink() | (Flux 콜백) |next() |
+------------+ +-----------+ +-----------+
🐤 채팅방 세션 ⇢ 웹소켓 세션 생명주기
+---------+
| web 모듈 |
+---------+
|
v
+----------------+
|WebSocketSession|
+----------------+
|
v
+---------------------+
|SubscriptionManager | (웹소켓 세션 생성 & 채팅방 입장)
|addSubscription() |
+---------------------+
|
+-----------------------|------------------------------+
| | |
| v |
| +--------------+ +---------------------+
| |MessageFlux | |SubscriptionManager |
| |broadcast() | |removeSession() |
| +--------------+ +---------------------+
| | |
| v |
| +--------------------------------+ |
| |MessageBroadcaster | |
| |broadcastMessageToSubscribers() | |
| +--------------------------------+ |
| |
+------------------------------------------------------+
|
| (채팅방 퇴장 or 세션 종료)
v
+-------+
| End |
+-------+
🧪 실행 환경
web/src/main/resources/application.properties
에 OAuth 정보 입력이 필요합니다### Spring Security OAuth
spring.security.oauth2.client.registration.github.client-id=
spring.security.oauth2.client.registration.github.client-secret=
localhost:3120
으로 접속합니다.🧍♂️ standalone
단독 실행 프로파일 (메시지 브로커 비활성)
git clone https://github.com/platanus-kr/plata-anywhere-chat.git pac
cd pac
cd misc
docker-compose -f docker-compose-standalone.yml up -d
docker container ps
cd ..
./gradlew web:bootJar
./gradlew message:bootJar
java -jar web/build/libs/web-0.0.1-SNAPSHOT.jar &
java -jar -Dspring.profiles.active=standalone message/build/libs/message-0.0.1-SNAPSHOT.jar &
👫 kafka
Kafka 를 사용하는 실행 프로파일 (메시지 브로커 활성)
git clone https://github.com/platanus-kr/plata-anywhere-chat.git pac
cd pac
cd misc
docker-compose -f docker-compose-kafka.yml up -d
docker container ps
cd ..
./gradlew web:bootJar
./gradlew message:bootJar
java -jar web/build/libs/web-0.0.1-SNAPSHOT.jar &
java -jar -Dspring.profiles.active=kafka message/build/libs/message-0.0.1-SNAPSHOT.jar &
message/src/main/resources/application-kafka.properties
의 spring.kafka.consumer.bootstrap-servers
항목에 모든 kafka 노드를 추가해야합니다.🎉 production
실제 운영 환경 (메시지 브로커 활성)
환경변수 설정을 합니다. docker-compose-kafka
를 사용하는 로컬 기준입니다.
환경변수 설정 예시 (Linux)
cat << "EOF" >> ~/.bash_profile
export PAC_MESSAGE_HOST=localhost
export PAC_MESSAGE_PORT=3121
export PAC_MESSAGE_FQDN=message.fqdn.com
export PAC_WEB_HOST=localhost
export PAC_WEB_PORT=3120
export PAC_WEB_FQDN=web.fqdn.com
export PAC_MARIADB_DB=jdbc:mariadb://localhost:33306/pac
export PAC_MARIADB_ID=paclocal
export PAC_MARIADB_PASSWORD=paclocaldockercompose
export PAC_KAFKA_MESSAGE_TOPIC=development.pac.chat.message
export PAC_KAFKA_PUSH_TOPIC=development.pac.chat.push
export PAC_KAFKA_KRAFT_NODE=localhost:29092
export PAC_MONGODB_HOST=localhost
export PAC_MONGODB_PORT=27017
export PAC_MONGODB_DB=pac
export PAC_MONGODB_USERNAME=localtest
export PAC_MONGODB_PASSWORD=localtest
export PAC_REDIS_HOST=localhost
export PAC_REDIS_PORT=6379
export PAC_GITHUB_CLIENT_ID=AAAA
export PAC_GITHUB_SECRET=AAAA
EOF
source ~/.bash_profile
# Kafka, MongoDB, Redis, MariaDB 구축은 생략합니다.
MariaDB 스키마로 테이블을 생성 합니다.
테이블 생성은 1회만 합니다.
mysql -u paclocal -p paclocaldockercompose pac < misc/db/mariadb-schema-pac.sql
빌드 및 실행
git clone https://github.com/platanus-kr/plata-anywhere-chat.git pac
cd pac
./gradlew web:bootJar
./gradlew message:bootJar
java -jar -Dspring.profiles.active=production web/build/libs/web-0.0.1-SNAPSHOT.jar &
java -jar -Dspring.profiles.active=production message/build/libs/message-0.0.1-SNAPSHOT.jar &
🪄 스케일아웃 하기