본문 바로가기

사이드 프로젝트/채팅 프로그램 개발 프로젝트

웹 채팅 프로그램 개발 프로젝트 후기 - 2

안녕하세요.

1편에서는 요구사항, 프로그램 구성, 그리고 유저 간 채팅 메세지 송수신 기능에 대해서 설명하였습니다.

이어서 다른 주요 기능 구현 방식에 대해서도 진행해보겠습니다.

 

3. 주요 기능 구현 방식

  • 2. 채팅 메세지 저장 및 조회 기능

 채팅 이력은 Spring Data Elastic Search를 통해 Elastic Search에 저장되도록 구현하였습니다. 채팅 메세지는 키워드, 저장 시간 등의 기준으로 조회되어야 하므로 Elastic Search에 저장하는 것을 선택했습니다. Spring Data Elastic Search에 대해 정리한 링크는 아래와 같습니다.

 

[Github Link] https://github.com/chrismrkr/WIL/blob/main/infrastructure/elasticsearch/spring-data-elasticsearch-summary.md

 

WIL/infrastructure/elasticsearch/spring-data-elasticsearch-summary.md at main · chrismrkr/WIL

what I Learned. Contribute to chrismrkr/WIL development by creating an account on GitHub.

github.com

 

 앞선 글에서 설명된 웹 소켓 메세지 송수신 기능의 중복 수신 방지 메커니즘이 동작하기 위해서, 메세지는 시간 순서대로 생성된 Sequence가 필요했습니다. 마찬가지로 채팅 메세지 중복 수신을 막기 위해서는 시간 순서와 동일한 특정 값을 ID로 가져야 했습니다.

 또한, 채팅 메세지는 RabbitMQ 메세지 브로커를 통해 다른 컨테이너로도 전달되므로 컨테이너들은 서로 동일한 ID를 생성해서는 안되며, 컨테이너가 다르더라도 ID는 시간 순서에 맞게 생성되어야 합니다.

 

 상황을 정리해보면, 웹 소켓 컨테이너가 여러 대 있는 상황에서 채팅 메세지 중복 수신 방지를 위해 시간의 흐름에 맞는 순차적인 ID 생성 방법이 필요했습니다. 이를 해결하기 위해 고려했던 방법은 아래와 같습니다.

  • 클라이언트에서 Unique ID 전달
    • 클라이언트 개발에 불편함을 제공하므로 채택하지 않음
  • 중앙 집중형 Unique ID Generator(ex. Redis) 운영
    • 여러 컨테이너가 하나의 저장소에 집중하는 형태로 병목이 발생할 수 있으므로 채택하지 않음
  • Snowflake 기법을 활용한 Unique ID 생성
    • 채택함
    • 여러 컨테이너가 독립적으로 시간 순서에 맞게 Unique ID를 생성할 수 있고, 컨테이너 끼리 동일한 ID를 생성할 위험 없음
    • 참고 자료
 

대규모 시스템 설계 기초 - 7. 분산 시스템을 위한 유일 ID생성기 설계

분산 시스템에서 사용될 유일 ID 생성기 요구사항 ID는 유일해야 한다 ID는 숫자로만 구성되어야 한다. ID는 64비트로 표현될 수 있는 값이어야 한다. ID는 발급 날짜에 따라 정렬 가능해야 한다. 초

lannstark.tistory.com

 

트위터에서 사용하는 Snowflake 기법을 도입하여 채팅 메세지마다 순차적인 Unique ID를 갖도록 하였습니다.

 

 

  • 3. 채팅방 생성 기능

 채팅방은 사용자에 의해 생성되고, 사용자와 채팅방은 다대다 관계로 설계하였습니다. 채팅방 생성 트랜잭션 흐름은 아래와 같습니다. 

  • 채팅방 생성: chatRoomRepository.create
  • 사용자 - 채팅방 입장: memberChatRoomRepository.save

Jmeter를 통해 부하테스트를 진행하였고, 약 100RPS에서 아래 에러가 발생했습니다.

  • SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms

해당 문제는 트랜잭션에서 HikariPoolCP Connection 획득을 시도했으나 Timeout에 의해 발생하는 에러로 확인했습니다.

 

HikariCP Dead lock에서 벗어나기 (이론편) | 우아한형제들 기술블로그

안녕하세요! 공통시스템개발팀에서 메세지 플랫폼 개발을 하고 있는 이재훈입니다. 메세지 플랫폼 운영 장애를 바탕으로 HikariCP에서 Dead lock이 발생할 수 있는 case와 Dead lock을 회피할 수 있는 max

techblog.woowahan.com

 

확인해본 문제 원인은 다음과 같습니다.

  • HikariCP Connection Pool Dead Lock
    • ChatRoom 엔티티의 @Id 생성 전략은 Sequence 였습니다. MySQL에서는 Sequence를 지원하지 않으므로 특정 테이블에서 Id를 가져온 후 업데이트하는 방식으로 Sequence를 모방하여 동작합니다.(SELECT FOR UPDATE)
    • 그러므로, ChatRoom 엔티티를 저장하는 트랜잭션은 2개의 HikariCP Connection을 동시에 사용됩니다.
      • 1. ID 채번
      • 2. 엔티티 저장
    • 만약 HikariCP Connection Pool 개수와 동일하게 트랜잭션이 동시에 발생하면, Sequence 테이블에서 Id를 채번하기 위해 Connection이 모두 소비되어 고갈되고, ChatRoom 저장을 위한 Connection을 얻지 못하는 Deadlock이 발생합니다.

아래의 방법을 통해 문제를 해결하였습니다.

 

WIL/JPA/JPASummary.md at main · chrismrkr/WIL

what I Learned. Contribute to chrismrkr/WIL development by creating an account on GitHub.

github.com

  • HikariCP Connection Pool 개수 최적화
    • @Id 생성 전략을 변경하지 않으려면, 아래와 같이 Connection Pool을 조절하여 DeadLock을 피할 수 있습니다.
    • Connection Pool = Tn * (Cn -1) + 1 (Tn: 동시 트랜잭션 수, Cn: 동시 필요 Connection 수)

@Id 생성 전략을 IDENTITY로 변경해 트랜잭션 당 필요 동시 Connection 수를 1로 조절하여 문제를 해결하였습니다. HikariCP Connection Pool에 대해서 알아보면서, Tomcat Thread Pool, 그리고 MySQL maxConnection와 같은 파라미터도 트래픽 처리에 중요한 요소임을 파악했습니다.

 

 

  • 4. 채팅방 입장 기능

최초에는 아래의 트랜잭션 흐름으로 구현하였습니다.

  • ChatRoom 조회 및 MemberChatRoom Fetch Join: chatRoomRepository.findByRoomIdWithMemberChatRoom
  • ChatRoom이 존재하는지 확인하고 없으면 throw Exception
  • MemberChatRoom이 이미 존재하는 Entity인지 확인: memberChatRoomRepository.findById(memberId, roomId)
  • 존재하지 않다면 MemberChatRoom 저장: memberChatRoomRepository.save

 위의 흐름으로 트랜잭션을 생성하고, 100RPS로 부하 테스트를 반복할수록 성능이 느려지는 현상이 나타났습니다.

이유는 ChatRoom 조회 시 MemberChatRoom을 Fetch Join에 의한 Cartesian Product였습니다. MemberChatRoom 엔티티의 개수가 많아질수록 Cartesian Product 결과가 많아지므로 성능에 악영향을 초래했습니다.

 

문제를 해결하기 위해 트랜잭션 흐름을 아래로 변경했습니다.

  • ChatRoom 조회 및 존재하는지 확인: chatRoomRepository.findById
  • MemberChatroom 저장: memberChatRoomRepository.save

 확인해보니 Chatroom을 memberChatRoom과 Join하여 조회할 이유가 없었습니다. 왜냐하면, JPA EntityManager의 save 기능에는 변경감지 기능을 포함하고 있기 때문입니다. 

  • 변경감지: Entity를 SELECT하여 이미 존재하는 엔티티라면 UPDATE하고, 존재하지 않다면 INSERT하는 기능

 변경감지 기능을 이용하여 MemberChatRoom 존재 여부도 확인하고, 불필요한 Cartesian Product도 막을 수 있었습니다. 이를 통해 앞서 발생한 문제는 해소되었습니다. JPA를 사용할 때는 실제로 어떤 Query가 발생하는지, 해당 Query와 관련된 Index는 무엇인지, 또한 부작용은 없을지를 반드시 확인해야 한다는 것을 느꼈습니다.

 

다음 편에서 채팅방 퇴장 기능, 성능 테스트에 대해서 작성하도록 하겠습니다. 감사합니다.