[Spring Boot Websocket] - 커미션 채팅방 (3) DB 설계, 기능 구현

jinvicky·2025년 1월 13일
0

Intro


저번 시리즈
🔗 https://velog.io/@jinvicky/chat-project-1
🔗 https://velog.io/@jinvicky/chat-project-2

실제 DB 설계 및 기능에 관한 글이다.

DB 설계

디비 설계는 주로 액셀 시트로 설계한다. 관계, 제약조건 등을 맨 처음부터 너무 신경썼다가 막상 컬럼 설계에서 필수 정보를 놓치는 경우를 막기 위해서다.

채팅은 1:1 오픈카톡 채팅 목적으로 만들었고, 채팅 읽음 처리 기능을 위해서 부재중 메세지 테이블을 추가했다.

개발 환경

MyBatis에 MySQL을 사용했다.

Flow

📝 현재는 소규모의 단일 채팅 서버로 이루어져 있지만 이후의 확장성을 대비해서 최소한의 레디스 메시지 큐를 도입했다. 보통 레디스를 대규모 애플리케이션에서 채널 역할로서 다른 채팅 서버들에게 전달하는 것을 목적으로 하지만, 나는 채팅방별로 세션들 집합에 웹소켓으로 메시지를 전달하는 것을 목적으로 코드를 작성했다.

개발과 이슈

개발

파일 채팅을 handleTextMessage에서 처리하도록 변경

기존의 DB 없는 채팅방 포스팅 에서는 파일 업로드는 handleBinaryMessage 에서 처리했었다.
하지만 채팅 이력을 DB에 저장하기 위해서 필요한 사용자 정보, 채팅방 정보 등을 handleBinaryMessage 로부터 받아올 수 없었다.

WebSocketSession은 백단의 세션이라서 프론트가 접근할 수 없으며, 프론트는 백엔드에게 소켓 메시지 외의 웹소켓 url에 파라미터를 추가하는 것만 할 수 있다.

    protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {

따라서 handleTextMessage 가 처리하게끔 변경했다. (코드 일부)
이미지 외의 확장자 업로드를 대비해서 resourceType을 받아서 업로드하도록 한다.
Back-End

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String jsonMsg = message.getPayload();

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 
        ChatMsg chatMsg = objectMapper.readValue(jsonMsg, ChatMsg.class);

        // 파일 채팅이면 스토리지에 업로드
        if (chatMsg.getType().equals("F")) {
            byte[] decodedBytes = Base64.getDecoder().decode(chatMsg.getEncodingFile());
            CloudinaryResponse uploadResult = cloudinaryUtil.bytesUpload(decodedBytes, chatMsg.getResourceType());


            chatMsg.setResourceType(uploadResult.getResourceType());
            chatMsg.setPublicId(uploadResult.getPublicId());
            chatMsg.setCloudName(cloudinaryUtil.getCloudName());
            chatMsg.setFormat(uploadResult.getFormat());
        }
        chatService.insertChatMsg(chatMsg);
        // redis에 채팅 메세지를 발행
        redisTemplate.convertAndSend("chatRoom", jsonMsg);
}

기존에 byte[] 로 전송하던 로직을 인코딩으로 한번 더 감쌌다.

Front-End

 const [files, setFiles] = useState<FileList | null>(null);

 const arrayBufferToBase64 = (buffer: ArrayBuffer) => {
   return btoa(String.fromCharCode(...new Uint8Array(buffer)));
 }
 

// ...일부 생략....
 if (files) {
   for(const f of files) {
     const buffer = await f.arrayBuffer();
     const base64String = arrayBufferToBase64(buffer);

     const msgFile =  {
       type: "F",
       encodingFile: base64String,
       chatRoomId: propChatRoomId,
       senderEmail: "wkdu0723@naver.com",
       resourceType: "image"
     }
     onSubmitBySocket(JSON.stringify(msgFile));
   }
 }

메세지 읽음 처리 (#부재중 메세지)

제일 고민이 많았던 부분인데 프론트에서 사용자가 현재 채팅방에서 focus 안 잡혔을 때 접속 여부 업데이트 api를 호출해주기로 했다.

  • 채팅방 입장 시 기존 부재중 메세지들을 일괄 읽음 처리
  • 부재중 메세지는 채팅방id+채팅방 사용자의 접속여부 컬럼 조건으로 방 안에 없는 사용자들 기준으로 부재중 메세지 테이블에 데이터를 넣었다.
public int insertChatMsg(ChatMsg chatMsg) {
        int mngNo =  mapper.insertChatMsg(chatMsg);
        log.info("mngNo : {}", mngNo);
        // chatRoomId가 동일 + senderEmail과 다르면서 accessYn이 N인 사용자들에게 알림
        List<ChatRoomUser> absentUserList = mapper.selectAbsentChatRoomUserList(chatMsg.getChatRoomId(), chatMsg.getSenderEmail());

        if(absentUserList != null && !absentUserList.isEmpty()) {
            // 알림
            log.info("absentUserList : {}", absentUserList);

            // 부재중 채팅 메세지에 저장
            absentUserList.forEach(user -> {
                ChatAbsentMsg absentMsg = ChatAbsentMsg.builder()
                        .chatRoomId(chatMsg.getChatRoomId())
                        .chatMsgMngNo(mngNo)
                        .readYn("N")
                        .rgtrDt(chatMsg.getRgtrTime())
                        .build();
                absentMsg.setReceiverEmail(user.getUserEmail());
                mapper.insertChatAbsentMsg(absentMsg);
            });
        }
        // ...생략
}

이슈

👀 mapper test 환경 실행 안됨

Caused by: java.lang.IllegalStateException: Attribute 'jakarta.websocket.server.ServerContainer' not found in ServletContext

mapper를 테스트하려는데 에러가 발생한다. 애플리케이션은 동작하는데 junit 한정 오류이므로, 내가 모르는 애플리케이션 환경과 junit 테스트 환경에 차이가 있다고 판단해서 구글링.
(junit test failed jakarta.websocket.server.ServerContainer라고 검색해보자)

🔗 https://stackoverflow.com/questions/73575360/attribute-javax-websocket-server-servercontainer-not-found-in-servletcontext-w

테스트 클래스에 @WebAppConfiguration 어노테이션을 붙이면 말끔히 해결된다.

@WebAppConfiguration
@SpringBootTest
class ChatRoomApplicationTests {

👀 loading 상태일 때 리턴값 처리

useQuery를 사용하는데 loading 상태일 때 리턴값 분기처리를 하지 않으면 can not read of undefined~ 에러가 났다. 쿼리에서 주는 loading 상태값을 내가 체크하지 않고 바로 data 에 접근했기 때문이다.

새로운 도전

useQuery

토스뱅크, 미리디 등 워너비 회사들의 기술 스택에 있는 것을 보고 학습 겸 도전했다.

  • 프론트에서 사용되는 데이터 캐싱 및 최적화 라이브러리.
  • react, vue 대부분의 프론트에서 지원해서 tanstack-query다.
  • swr과 비교하는 글이 많은데 결론적으로 swr이 더 단순해서 소규모 프로젝트에 적합하다.
  • 예외처리, 로딩중 등의 상태값을 받아서 처리할 수 있다는 점이 편했다.

useQuery 짧은 기록

  • 버전이 올라가면서 useQuery 함수에 인자로 옵션 객체({queryKey, queryFn.... })가 들어가도록 변경되었다. 기존에는 개별 인자였다.
  • 여러 api를 처리할 때는 []로 묶어서 처리할 수 있다. 하지만 내부에서 특정 쿼리만 특정 조건이 만족된 후에 실행하고 싶었다. 그래서enabled 속성에 상태값을 넣었음.
const queries = useQueries({
        queries: [
            {
                queryKey: ['chatRoomDetail', propsChatRoomId],
                queryFn: fetchChatRoomDetail,
            },
            {
                queryKey: ['chatMsgHistory', propsChatRoomId],
                queryFn: fetchChatMsgHistory,
            },
            {
                queryKey: ['updateUserAccess', propsChatRoomId],
                queryFn: () => fetchUpdateChatRoomUserAccess({
                    mngId: "JVK",
                    chatRoomId: propsChatRoomId,
                    userEmail: "wkdu0723@naver.com", 
                    accessYn: "Y"
                }),
                enabled: isSocketConnected,  // WebSocket 연결 후에만 호출하도록 트리거 설정
            },
        ],
    });

Outro


백엔드는 회원 연동을 위해서 JWT를 다시 공부중이고, Front-End는 동료분께 부탁을 드렸다. 1월 내로 완성해서 https 사이트로 내놓는 것을 목표로 한다.

Contributor

crud가 많고 너무 자잘한 시행착오는 포스팅에서 제외했습니다. 내용은 Git
🔗 Git https://github.com/jinvicky/cms-chat-prep

jinvicky
Front-End, Back-End Developer
✉️ Email: jinvicky@naver.com
💻 Github: https://github.com/jinvicky

wkdu0723
Front-End Developer
✉️ Email: wkdu0712@naver.com
💻 Github: https://github.com/wkdu0723

profile
Front-End와 Back-End 경험, 지식을 공유합니다.

0개의 댓글