Yapp 동아리의 개발 프로젝트였던 화상 모의면접 연습 플랫폼 "위더뷰" 개발에 백엔드 개발자로 중도 합류했다.
github.com/witherview/witherview_backend
기본적으로는 자바 스프링부트를 사용하지만, WebRTC의 경우 Node JS를 사용하는 구조다. 프론트는 React 기반이다.
중도에 합류해서 기존 코드와 구조를 어떻게 분석했는지 / 어떻게 개선방안을 찾아갔는지 생각을 정리하는 용도의 포스트.
처음 프로젝트에 합류했을 때, 먼저 개발해야 하는 영역은 채팅이었다.
모의면접 방에서 일정을 잡고, 참여자끼리 자유롭게 채팅할 수 있는 공간을 구현해야 했다.
여러 사용자가 실시간으로 채팅할 수 있는 API 개발이 필요했다.
자바 스프링의 Stomp로 기본적인 pub/sub 형태의 실시간 메시징은 구현되어 있었고,
참여자가 서로의 면접 피드백을 채팅으로 남긴 경우 redis에 임시 저장했다가 데이터베이스로 저장하는 구조로 되어 있었다.
채팅 데이터는 전부 MySQL RDBMS로 저장되고 있었고, 사용자 테이블과 N:1 형태로 relation이 연결되어 있었다.
데이터베이스와 Spring Data JPA의 객체가 어떤 식으로 매핑되어 있는지 체크하고
채팅의 저장로직을 확인하다 보니, 개선하고 싶은 지점이 있었다.
- Redis에서 해당 면접에서 있었던 피드백 채팅을 전부 가져온다
- findUser로 피드백을 받은 User의 repository에서 사용자를 조회한다.
- findUser로 피드백을 제공한 User의 repository에서 사용자를 조회한다.
- 해당 면접이 있었던 방의 정보를 StudyHistory Repository에서 조회한다.
- FeedbackChat이라는, 피드백 채팅 객체에 조회한 객체를 매핑한다.
- 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를 정의할 수 있었다.
채팅과 피드백에 걸려 있는 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를 몇 개 추가하는 것으로 충분히 효율적인 형태를 만들 수 있다고 생각했다.
'프로그래밍 > 이것저것_개발일지' 카테고리의 다른 글
Streamlink로 유튜브 멤버십 스트리밍 영상 다운로드하기 (2) | 2021.09.27 |
---|---|
화상 모의면접 연습 플랫폼 개발 프로젝트 (2) - KeyCloak 활용해서 서비스 DB에 OAuth 인증 붙이기 (0) | 2021.04.18 |
Java WebSocket과 Stomp로 간단한 채팅프로그램 만들기 (0) | 2021.02.04 |
IBM Kubernetes Cluster에 SpringBoot Application 구동 실습하기 - 2. deploy (0) | 2020.11.10 |
IBM Kubernetes Cluster에 SpringBoot Application 구동 실습하기 - 1. dockerizing and 환경설정 (0) | 2020.11.09 |