지난번에 시퀀스를 사용해서 누락된 메시지를 전달하는 로직을 구현했다.
위 사진을 보면
a와 b가 서로 메시지를 교환하다, b의 메시지 (4,5,6,7,8)이 db에 저장은 됐지만 전송이 안 됐다.
a가 메시지를 보낼 때는 클라이언트가 갖고 있는 마지막 값 3이 있는데 채팅방에 저장된 마지막 메시지의 시퀀스가 3이 아니라 8이라고 해보자.
그러면 누락된 메시지가 있다는 것을 확인하고, 시퀀스 4(3+1)부터 8까지의 누락된 메시지를 db에서 조회한다.
다른 엣지 케이스들도 한번 생각해보자.
b가 갖고 있는 마지막 메시지의 시퀀스가 8인데, a가 갖고 있는 시퀀스가 5인 상황.
이게 가능할까?
두가지 케이스가 있다.
chat은 저장됐는데 room이 갖고 있는 lastChatSeqeuence가 업데이트가 안 됐다.
이 경우에는 그냥 최신 chat을 불러온 다음에 sequece값을 업데이트 해주면 된다.
이런 경우는 어떤 구조적인 문제가 있을 수 있다는 징후이기에
에러 발생시 로그를 남긴다. 지금은 디스코드 웹훅으로 로그를 전송만하자.
어차피 이렇게 sequence가 안 맞는 걸 알게 되는 게
새로운 메시지를 보낼 때이다.
새로운 메시지를 보낼 때 이 부분에서 sequence 및 마지막 채팅 정보를 업데이트해주기에, 따로 처리를 할 필요는 없을 거 같다.
(다만 이 로그가 계속 쌓이면 로직에 문제가 있다는 것이기에 보수가 필요하다)
나중에 ELK Stack을 활용해서 로그를 수집하는 것도 구축하면 좋을듯하다.
다른 케이스는 아예 메시지가 유실된 상황을 생각해볼 수 있다.
이 경우는 사실... 너무 복잡해질 거 같다.
레디스가 있으니 그 레디스에 메시지를 담아서 저장해놓는 것도 좋은 방법이다.
(물론, 이것 역시 유실될 수 있다)
다만 지금같은 초기 단계에서 이렇게까지?...하는 게 맞나 싶다.
우선, 이건 나중 작업을 남겨놓는 것으로 하자.
동시성 문제로
동시에 room에서 lastChatSequence값을 읽어와서 +1을 해줄 수도 있다.
이러면 sequence가 중복된다.
둘다 DB에 저장이 될 것이기에 엄청난 문제?라고 보기는 어렵지만
데이터 정합성이 맞지 않게 된다.
크리티컬한 이슈가 아니라서 chat_room_id와 lastChatSequence를 복합 유니크 인덱스로 걸기엔 리소스 낭비로 보인다.
그러니 이번에는 @OptimistLock을 사용해서 낙관적으로 동시성 이슈를 처리해보자.
동시성 이슈가 발생하면 예외가 발생하고, 그때 @Retry를 해주는 방식이다.
비관적 락을 쓸 때처럼 테이블 레코드에 락을 거는 방식이 아니라서 DB부하가 크지 않다.
대신 충돌이 많이 발생하지 않는 상황에 주로 쓴다.
jpa에서는 위같은 비관적 락을 제공한다. 레코드에 락을 거는 방식이다.
필드에 @Version 어노테이션을 붙인다. 트랜잭션들은 이 어노테이션이 붙은 필드 값을 읽고서, 이 값을 업데이트하기 전에 the version property를 다시한번 체크한다.
이 값이 바뀐 상황이라면 an OptimisticLockException을 던진다. 그렇지 않으면, 커밋을 한다.
이렇게 사용하면 된다.
다만, 낙관적락은 충돌이 많이 발생할 때 DB요청이 많이 가니까 성능이 떨어질 수 있다고 한다.
채팅은 사람마다 타이핑하는 속도, 메시지를 보내는 속도가 다르니 정확히 같은 밀리초에 DB 조회를 해서 동시성 문제가 발생할 일이 많지는 않을 것을 보인다.
그리고, 일대일채팅은
A가 메시지를 보내면 B가 그걸 읽고, B가 메시지를 보내면 A가 보내는 패턴이 일반적이다.
그러므도 동시성 이슈에 대한 우려가 크지 않다.
그리고 이렇게 retry 로직을 작성해주어야 한다.