공부하고 기록하는, 경제학과 출신 개발자의 노트

프로그래밍/이것저것_개발일지

화상 모의면접 연습 플랫폼 개발 프로젝트 (1) - 채팅 DB 아키텍처 고민하기

inspirit941 2021. 2. 20. 00:08
반응형

Yapp 동아리의 개발 프로젝트였던 화상 모의면접 연습 플랫폼 "위더뷰" 개발에 백엔드 개발자로 중도 합류했다.

 

github.com/witherview/witherview_backend

 

witherview/witherview_backend

🎯 위더뷰 Backend. Contribute to witherview/witherview_backend development by creating an account on GitHub.

github.com

기본적으로는 자바 스프링부트를 사용하지만, WebRTC의 경우 Node JS를 사용하는 구조다. 프론트는 React 기반이다.

중도에 합류해서 기존 코드와 구조를 어떻게 분석했는지 / 어떻게 개선방안을 찾아갔는지 생각을 정리하는 용도의 포스트.

 


 

처음 프로젝트에 합류했을 때, 먼저 개발해야 하는 영역은 채팅이었다.

모의면접 방에서 일정을 잡고, 참여자끼리 자유롭게 채팅할 수 있는 공간을 구현해야 했다.

여러 사용자가 실시간으로 채팅할 수 있는 API 개발이 필요했다.
자바 스프링의 Stomp로 기본적인 pub/sub 형태의 실시간 메시징은 구현되어 있었고,
참여자가 서로의 면접 피드백을 채팅으로 남긴 경우 redis에 임시 저장했다가 데이터베이스로 저장하는 구조로 되어 있었다.

채팅 데이터는 전부 MySQL RDBMS로 저장되고 있었고, 사용자 테이블과 N:1 형태로 relation이 연결되어 있었다.

 


 

데이터베이스와 Spring Data JPA의 객체가 어떤 식으로 매핑되어 있는지 체크하고
채팅의 저장로직을 확인하다 보니, 개선하고 싶은 지점이 있었다.

 

 

  1. Redis에서 해당 면접에서 있었던 피드백 채팅을 전부 가져온다
  2. findUser로 피드백을 받은 User의 repository에서 사용자를 조회한다.
  3. findUser로 피드백을 제공한 User의 repository에서 사용자를 조회한다.
  4. 해당 면접이 있었던 방의 정보를 StudyHistory Repository에서 조회한다.
  5. FeedbackChat이라는, 피드백 채팅 객체에 조회한 객체를 매핑한다.
  6. FeedbackChatRepository에 채팅 한 개를 저장한다.

화상면접이 이루어지는 시간 동안 만들어지는 채팅 데이터의 개수는 많을 텐데,
각각의 채팅을 하나씩 저장하는 동안 DB에서 조회를 세 번이나 해야 했다.

채팅을 저장할 때 병목현상의 원인이 될 수 있어 보였다.

 


이런 현상이 발생한 이유는
사용자라는 User와 해당 사용자가 참여한 방 StudyHistory,
그 안에서 이루어지는 채팅이 전부 Relation으로 엮여 있었기 때문이라고 보았다.

 

물론 논리적으로는 한 명의 사용자가 여러 개의 방에 들어갈 수 있고,
각 방에는 여러 개의 채팅이 생길 수 있으며, 그 채팅의 owner는 글을 쓴 사용자가 맞다.
MySQL에서 누군가 처음에 테이블을 정의할 때, 이 관계를 충실히 객체에도 구현해 두었다.

 

하지만 나는 논리적으로 관계가 있는 것과 RDBMS에서 관계를 설정하는 것에는 실용적인 면에서 차이가 있다고 생각했다.

  • RDBMS에서 관계를 설정하면,
    lazy loading을 사용한다 해도 데이터를 조회할 때마다 relation이 걸린 쿼리가 데이터베이스로 날아간다.

    개발자가 의도하지 않은 쿼리가 데이터베이스로 날아가는 것이므로, relation이 복잡해지고 많아질수록
    개발자가 통제할 수 없는 영역이 늘어난다.
  • 그 부담을 감수하고라도 relation을 걸기 위해서는
    RDBMS의 장점 중 하나인 잦은 데이터 변경을 반영해 주거나
    사용자 또는 면접 방을 조회할 때 항상 같이 딸려올 만큼 중요해야 했다.
  • 채팅 데이터는 그 특성상 한 번 생성한 뒤 수정할 일이 별로 없다.
    또한 사용자가 자신의 정보를 조회할 때, 지금까지 본인이 적었던 채팅 데이터가 항상 필요할 것 같지는 않았다.
  • 그리고 현재의 데이터 저장 구조는 1회의 저장을 위해 3번의 DB조회가 필요한 방식으로, 개선이 필요해 보였다.

 

그래서 이 프로젝트에 중도 참여한 뒤 내가 맡은 첫번째 작업은 현재의 채팅 저장로직을 변경하는 것이었고,

나는 채팅 데이터에서만큼은 RDBMS가 아니라 NoSQL을 적용하는 게 좋다고 생각했다. 그 중에서는 MongoDB를 선택했다.

 

  • 채팅 데이터는 양이 많고, 수정보다 조회하는 횟수가 많으며, Consistency가 상대적으로 덜 중요한 데이터라고 봤다.
  • 그렇다면 ACID를 보장하는 RDBMS보다는 BASE 특성을 보장하는 NoSQL이 더 좋아 보였다.
  • 피드백 채팅 데이터를 조회할 때, '받은 사람 -> 해당 스터디 방 -> 보낸 사람 -> 보낸 시간' 형태로 데이터를 조회할 수 있어야 했다.
    예컨대 "내가 받은 피드백 채팅 중에서 A라는 사람이 보낸 채팅만 확인할 수 있어야 한다" 라는 비즈니스 요구사항이 있었고,
    채팅 데이터는 시간순으로 정렬된 채 나타나야 했다.
  • NoSQL로 Cassandra도 생각해 봤지만, 위와 같은 조건에는 CompositeIndex를 적용하는 게 보다 적절하다고 봤다.
    CompositeIndex를 지원하는 NoSQL이면서 학습 소스가 많은 데이터베이스로는 MongoDB가 좋아 보였다.

 

그러면, NoSQL 중에서도 MongoDB의 데이터 모델링하는 방법을 알아봐야 했다. 한 번도 해본 적 없었기 때문이다.

T아카데미의 MongoDB 데이터베이스 모델링 강의를 참고했다.

 

 

www.youtube.com/watch?v=2boOF8zndns&list=PL9mhQYIlKEheyXIEL8RQts4zV_uMwdWFj

 

 

비즈니스 요구사항에 맞아 보이는 모델링 기법인 Materialized Path가 있었다.

사용자 - 스터디방 - 보낸 사용자 - 시간 순서대로 데이터를 자동으로 정렬하는 key 구조를 갖출 수 있었기 때문이다.

 

Spring mongoDB에서는 @CompoundIndex라는 어노테이션으로 composite index를 정의할 수 있었다.

docs.spring.io/spring-data/data-mongodb/docs/current/api/org/springframework/data/mongodb/core/index/CompoundIndex.html

 

CompoundIndex (Spring Data MongoDB 3.1.5 API)

Mark a class to use compound indexes. NOTE: This annotation is repeatable according to Java 8 conventions using CompoundIndexes.value() as container. @Document @CompoundIndex(def = "{'firstname': 1, 'lastname': 1}") @CompoundIndex(def = "{'address.city': 1

docs.spring.io

 


채팅과 피드백에 걸려 있는 RDBMS의 Relation은 제거했다.

어차피 데이터를 조회할 때 사용자 id값 / 스터디 방 id값을 파라미터로 제공하면 된다고 생각했기 때문이다.

 

@Document
@CompoundIndex(def = "{'receivedUserId': 1 , 'studyHistoryId': -1, 'sendUserId': 1, 'timestamp' : 1}")
public class FeedBackChat {
    @Id
    private String id;
    @NotNull(message = "피드백 보낸사람 아이디는 반드시 입력해야 합니다.")
    private Long sendUserId;
    @NotNull(message = "피드백 받는사람 아이디는 반드시 입력해야 합니다.")
    private Long receivedUserId;
    @NotBlank(message = "피드백 메세지는 반드시 입력해야 합니다.")
    private Long studyHistoryId;

....

 

JPA에 붙어 있던 @Entity 대신 @Document를 사용했고, 여러 필드값을 사용해 key index를 지정하는 @CompoundIndex를 붙여줬다.
조회해야 할 데이터의 조건이 달라지면, 조건에 맞는 CompoundIndex를 몇 개 추가하는 것으로 충분히 효율적인 형태를 만들 수 있다고 생각했다.

 

 

 

반응형