💻 프로젝트/Spring webflux를 이용한 채팅서버

PlantUML을 이용하여 시퀀스 다이어그램 설계 (1)

미미누 2025. 2. 4. 16:15

현재 Spring Webflux를 이용하여 간단한 채팅 서버를 구현하고 있다.

 

주 기능은 다음과 같다.

1. 사용자가 채팅 요청을 보내면 사용자가 들어간 채팅방의 모든 사용자에게 채팅을 보낸다.

2. 모든 사용자에게 푸시 알림을 보낸다. (비동기적으로 소요시간이 0.5s가 되는 푸시 알림 서버에 Request를 보내고,

Response를 Client에게 보낸다.)

 

클라이언트의 비지니스 로직은 다음과 같다.

비즈니스 로직

- payload 로 넘어온 채팅방 PK가 정말 있는 채팅방인지와 사용자가 채팅방에 들어가있는지 확인

- 해당 채팅방에 채팅 레코드 추가

- 해당 채팅방에 있는 사용자 모두 조회

- 조회된 사용자들의 device 에 모두 push 요청

- push 요청 보내기 시작하면서부터 모든 사용자의 device 에게 요청 다 보낼 때까지의 시간 측정

 

푸시 알림 서버

푸시 알림을 보내는 데에는 0.5s 가 반드시 소요 (response 가 0.5초 후에 온다는 소리)

에러 발생 가능성 (5%) 있음 (실패하더라도 상관 없이 로직 수행)

 

(이하 요구사항 생략...)


일단 푸시 알림 서버는 요청을 보내는데 0.5s가 반드시 소요된다는 요구사항이 있어, 이를 처리하는데 중요하다고 생각한다.

사용자가 만약 채팅 요청을 보내는데, 동기적으로 각각 요청을 보낸다면 이는 소요시간이 누적되어 0.5s가 훌쩍 넘어버리는 시간이 소요된다. 따라서 푸시 알림 서버로 보내는 요청은 비동기적으로 처리해야 한다.

 

일단 코드 구현에 앞서, 시퀀스 다이어그램과 클래스 다이어그램을 이용하여 설계를 진행하고,

코드 구현에 들어가는 것이 스파게티 코드를 방지할 수 있다. 따라서 필자는 PlantUML을 이용하여 시퀀스 다이어그램을 그리고자 한다.

 

[PlantUML 사용법]

IntelliJ plugin 파일에 PlantUML을 설치한다.

 

본 프로젝트 /docs 디렉터리에 chat-server-plow.puml 파일을 생성한다.

 

 

[chat-server-flow.puml 예시]

@startuml
'https://plantuml.com/sequence-diagram

participant Client
participant ChatServer
participant PushServer
participant Database

autonumber
== 사용자 채팅 저장 및 모든 사용자 에게 푸시 알림 전송 시나리오 시작 ==

Client -[#red]> ChatServer: ChatRequest (AccessToken(사용자의 정보가 담긴 엑세스 토큰) / ChatPK(사용자가 속한 채팅방 아이디) / content(입력할 채팅 내용)

note over Client, ChatServer
    header: Authorization: Bearer {accessToken}
    request body:
    {
        "chatPK" : "uuid",
        "content" : "string"
    }
end note

ChatServer -> ChatServer: AccessToken을 통해 userPK 가져오기
ChatServer -> ChatServer: AccessToken을 통해 AppInfo 가져오기

ChatServer -> Client: 유효하지 않는 토큰을 준 경우 에러 메시지 반환
note over Client, ChatServer
    response body:
    {
        "resultCode" : 10001
        "resultData" : "유효하지 않는 엑세스 토큰입니다."
    }
end note

ChatServer -> Client: 기한이 만료된 토큰을 준 경우 에러 메시지 반환

note over Client, ChatServer
    response body:
    {
        "resultCode" : 10002
        "resultData" : "기한이 만료된 엑세스 토큰입니다. 재발급 요청이 필요합니다."
    }
end note

ChatServer -> Database: user_chat_room에 existsby(userPK, chatPK) 쿼리 요청
ChatServer -> Client: 해당 채팅방 없는 경우 에러 메시지 반환

note over Client, ChatServer
    response body:
    {
        "resultCode" : 10100
        "resultData" : "해당 채팅방이 존재하지 않습니다."
    }
end note

ChatServer -> Database: user_chat_room에 findUserBy(chatPK, userPk) 쿼리 요청
Database -[#blue]> ChatServer: UserListRes: List(User) 채팅방에 존재하는 모든 유저 반환

ChatServer -[#red]> Database: chatReq (chatPK, List(user), content) 채팅방에 존재하는 모든 유저에 메시지 저장
ChatServer -> ChatServer: 각 유저의 (isCompletedOnboarding, nickname, email) 값 가져오기

ChatServer -> ChatServer: 시간 측정 시작
ChatServer -[#red]> PushServer: 비동기로 각 유저 정보가 담긴 PushRequest 요청

note over ChatServer, PushServer
    header: Authorization: Bearer {accessToken}
    header: {AppInfo} PUSH_BROADCASTING
    request body:
    {
        "isCompletedOnboarding" : "booleanEnum",
        "nickname" : "string",
        "email" : "string"
    }
end note
PushServer -[#blue]> ChatServer: PushResponse
note over ChatServer, PushServer
    response body:
    {
        "resultCode" : "Int",
        "resultData" : {
            "isCompletedOnboarding" : "booleanEnum",
            "nickname" : "string",
            "email" : "string"
        }
    }
end note
ChatServer -> ChatServer: 시간 측정 완료
ChatServer -[#blue]> Client: PushTimeRes
note over Client, ChatServer
    response body:
    {
        "resultCode" : "Int",
        "resultData" : {
            "time" : "Int"
        }
    }
end note
@enduml

 

이런식으로 plantuml 문법을 이용하여 시퀀스 다이어그램을 그릴 수 있다. 

 


 

위 그림은 초기에 내가 그린 시퀀스 다이어그램이다.

 

하지만 아래와 같은 여러 문제점이 존재한다.

1. 이 요청이 어떤 http method 인지, access token 은 뭐 헤더로 받는지, chat pk 와 content 는 request body 로 받는지 form data 등으로 받는지 등 형식이 정의되어 있지 않다.

2. 검증 시 실패가 났을 때 그 케이스와 함께 어떤 에러코드 또는 데이터가 클라이언트에게 반환되지 않는다. 

3. 결국 DB 에 갔다오며 어떤 행위를 하려는지가 상세하지 않고, ChatReq로 압축적으로 표현했다.

4. 푸시 서버에 요청을 보내고 응답을 받는데 반드시 0.5s 가 소요되므로 10번에 요청을 동기적으로 보내기 위해서는 총 5초가 소요된다. 

5. 클라이언트는 주로 API 내부 로직보다는 어떤 응답이 올지를 더 궁금해한다.

 

위 피드백을 반영하여, 아래와 같이 시퀀스 다이어그램으로 변경하였다.

1. Client가 ChatServer로 Request를 보낼때, 헤더와 바디에 어떤 값을 보내야 하는지 명확히 하였다. (Client 관점)

2. 유효하지 않는 요청을 할때, 어떤 에러 메시지를 보내는지 명확히 하였다.

3. DB 접근시 대략적으로, 어떤 Query를 보내는지 적어주었다. 

4. ChatReq를 세분화 하였다. (chatPK, List(user), content) 채팅방에 존재하는 모든 유저의 메시지 저장

5. 푸시 서버 요청 시 비동기적으로 처리하는 것을 명시하였다.

 

 

[회고]

- 단순 코드 구현보다, 설계하는 개발자가 되려고 노력해야 겠다.