코드스테이츠 메인프로젝트 후기
많이 배웠던 메인프로젝트 후기입니다.
코드스테이츠에서는 마지막 5주를 메인프로젝트로 보내는데요. 프론트 세 명, 백엔드 세 명이 모여서 기획부터 개발, 배포까지 진행합니다.
0. 팀 규칙 정하기
다들 프리 프로젝트(메인프로젝트 전 2주간 하는 프로젝트) 때에 소통에 대한 중요성을 느꼈기 때문에 팀 규칙을 정하는 것을 중요하게 여겼습니다. 저희 팀만의 규칙 중 가장 중요한 것은 "상주" 였습니다. 팀별 디스코드 채널이 있었는데 모든 팀원들이 디스코드 채널에 오전 9시부터 저녁 6시까지 상주하기로 하였습니다. 저는 이 규칙이 저희 팀의 생산성과 결과를 가져다 주었다고 생각합니다. 프론트든 백엔드든 각자에게 물어볼 것이 있으면 거의 실시간으로 질문할 수 있었고, 개발 변경사항 등을 모든 팀원이 공유하였으며, 매일 하는 팀 회의 외에도 회의를 요청하면 바로 회의를 시작할 수 있었기 때문입니다. 이 규칙은 병목 지점 없이 개발을 진행하는 데에 큰 도움이 되었습니다.
0-1. 커밋 컨벤션, 깃 전략 정하기
커밋 컨벤션은 각자가 익숙한 방식을 종합하여 정했습니다. feat(기능 추가), fix(기능 수정)을 가장 많이 사용했습니다. 깃 전략은 git flow를 택하였습니다. 기능에 대한 브랜치를 판 다음 개발 후 dev 브랜치에 PR을 날리면 최소 1명이 내용을 확인하고 confirm을 한 후 머지하였습니다.
1. 서비스 정하기 - 기획
팀원들과 하루 종일 미팅하고, 모두가 만족할 수 있는 토픽을 정하고, 요구사항 설명서를 만드는 등 개발을 시작하기 전 해야 할 것들을 정하는 시간입니다. 프로젝트 주제를 정하는 기준으로는 해 보고 싶은가, 배울 것이 많은가, 5주 안에 개발이 가능한가 등이 있었습니다. 저희 팀은 산책 커뮤니티에서 모임 커뮤니티로 확장하였습니다.
2. 요구사항 정의
모임 커뮤니티가 필요한 기능을 팀원들과 생각하였습니다. 크게 회원, 모임 모집글, 댓글, 모임 채팅 기능, 알람 기능 등이 있어야 했고 백엔드 3명이 각자 어떤 기능을 담당할 지 나누는 시간을 가졌습니다. 저희 팀은 개발 난이도, 필요성에 따라 기능을 네 레벨로 나누었고 각 레벨이 잘 작동하게 만든 뒤 다음 레벨로 넘어가기로 하였습니다.
3. 맡은 기능
저는 답변 기능과 채팅 기능을 맡기로 하였습니다. 답변 기능은 1단계, 채팅 기능은 2단계로 정하였기 때문에 답변 기능부터 개발을 시작하였습니다.
4. 테이블 설계
유저와 모집글, 댓글 등의 테이블은 모두 연결되어 있고, JPA를 쓸 때에도 연관관계 매핑을 해주어야 했기 때문에 엔티티와 리포지토리를 먼저 정의하였습니다. 저는 초기 디비 테이블을 정의하였고 JPA Entity 연관관계를 맺는 작업을 하였습니다. 1 : N 매핑은 일단 모든 경우 @ManyToOne, @OneToMany를 걸어주었고, fetch strategy는 LAZY로 하였습니다. N : N 관계는 중간 테이블을 만들어서 N : 1 - 1 : N 으로 만들었습니다.
5. 댓글 개발
일반적인 댓글 CRUD를 개발하였습니다. 작성, 수정, 삭제는 JPA로 편하게 개발했으며, 모집글 ID를 받고 해당하는 댓글 리스트를 페이징하는 과정이 조금 오래 걸렸습니다. 또한 댓글마다 작성자의 닉네임과 프로필사진 URL을 받아와야 했는데, 댓글 리스트 리턴 Dto로 변환할 때 각 댓글마다 getMember() 메소드를 호출하기 때문에 그 유명한 N + 1 문제를 겪어보았습니다. 다이나믹 프록시 객체 안에 멤버가 채워져 있지 않았기 때문에 생기는 일이었고, 이를 fetch join으로 한번에 댓글-멤버 결과를 가져오게 하여 해결하였습니다.
6. 채팅 개발
가장 많이 시간을 썼던 기능입니다. 구체적으로 1대1 채팅, 단체채팅, 채팅 리스트, 채팅방 초대, 채팅방 나가기 등의 기능이 있었습니다. 어렴풋이 HTTP가 아닌 웹소켓을 사용한다는 것은 알았지만 프로토콜을 이해하는 데에 시간이 꽤 걸렸습니다. 폴링, 롱 폴링의 단점을 알게 된 후 양방향 통신을 할 수 있는 웹소켓에 대해 자세히 알게 되었습니다. 또한 pub/sub 구조도 알게 되었습니다.
6-1 채팅 데이터베이스 설계
사실 처음 설계를 시도했을 때는 채팅 메시지 데이터가 어디에 저장되는 것이 좋은가, 카프카는 어떻게 써야 하는가 등에 많은 시간을 쏟았습니다. 각종 개발 블로그를 참조하였고 항상 나오던 기술인 카프카, Redis pub/sub, 카산드라, HBase 등 들어보지 못한 키워드에 압도당한 면이 있었습니다. 전전긍긍하던 어느날, 시간을 버리느니 성능/규모 문제는 차치하고 일단 MYSQL로 채팅이 되게 한 후 나중에 바꾸겠다는 생각이 들었고, 채팅 테이블부터 만들기 시작하였습니다. 인프라적인 요소를 차치하고 채팅 기능을 위한 설계만 하는데도 생각할 거리가 많았습니다.
위의 ERD는 채팅 기능과 관련된 테이블의 연결 관계를 나타낸 것입니다.
6-2. chatroom 테이블
chatroom 테이블 안에 최대한 많은 정보를 담으려고 하였고, 그 예시로는 방장, 멤버 수, 마지막 메시지 내용, 마지막 메시지 보낸 시간, 채팅방 타입 등을 정의하였습니다. 채팅 목록 요청을 보내면 메세지 테이블을 조인할 필요 없이 chatroom 테이블과 member_chatroom테이블만 조인하면 리턴값을 내줄 수 있게 하였습니다. 물론 이런식으로 할 경우 메세지를 보내는 시점에 서버에서 여러 테이블을 업데이트하는 작업이 필요하기 때문에 트레이드 오프가 있다는 것을 깨달았습니다.
6-3. member_chatroom 테이블
member_chatroom 테이블은 각 멤버의 각 채팅방별 로그인 상태 저장, 읽지 않은 메세지 수를 저장합니다. 또한 마지막으로 읽은 시점도 저장하여 메세지 테이블과의 조인을 하지 않도록 하였습니다. 우선 어떤 유저가 채팅방에 들어가는 시점부터 로그인 상태를 online으로 바꾸고 unread_count를 0으로 바꿉니다. 나가는 순간 offline으로 바꾸는 로직이 실행됩니다. 채팅방의 어떤 사람이 메세지를 보내면 member_chatroom 테이블에서 chatroom_id에 해당하는 유저 중 offline인 행의 unread_count를 1 증가시키는 로직이 실행됩니다.
6-4. message 테이블
유저들이 보내는 메세지를 저장하는 테이블입니다. 내용, 글쓴이, 작성 시간, 메세지 타입을 필드로 가지고 있습니다. participant_type 이란 유저이냐 혹은 공지 메세지이냐를 구분하는 필드입니다. 채팅방 생성시 "~님이 참여하였습니다" 등의 공지메세지를 구분할 수 있도록 저장하였습니다.
6-5. 채팅 구현시 어려웠던 지점
채팅방이나 채팅을 추가하는 것은 괜찮았지만 나가기를 구현할 때 헷갈리는 부분이 많았습니다. 여기서 확실한 기획이 필요하다고 느낀 지점이기도 합니다. 일대일 채팅방을 나갈 때, 단체 채팅방을 나갈 때, 혼자 남은 사람이 나갈 때 등 많은 상황을 고려하였으며 여기서 채팅 담당 프론트엔드 팀원분과 정책/규칙에 대해 많은 것들을 토론하고 정했습니다. 또한 조인을 하지 않기 위해 설정한 필드값들을 동기화해주지 않아 생기는 문제도 있었습니다.
6-6. 아쉬운 부분
인프라적인 부분을 거의 생각하지 못했다는 것이 아쉽습니다. 아파치 카프카나 레디스 pub/sub 대신 스프링 내장 메세지 브로커를 사용하였고 카산드라 대신 MySQL을 사용하였습니다. 성능 테스트도 하지 못했습니다. EC2 프리티어에서는 몇개의 웹소켓까지 연결될 수 있는지, 메세지 개수가 몇억건이라면 시간이 얼마나 걸릴까 등을 생각하지 않고 개발했습니다. 테스트코드도 작성하다가 말았습니다. 캐싱에 대한 기능도 적용하려다가 개발 일정 문제로 생각하지 못했습니다. 조금 더 고도화할 수 있었겠지만 현재의 능력 부족이라고 생각하고 점차 상기한 것들을 생각하며 리팩토링을 진행하고 싶습니다.
7. 배포
AWS EC2 인스턴스 위에서 서버를 구동시켰고, RDS(MYSQL), S3, ElasticCache, Route53 를 연동하였습니다. HTTPS를 적용하는 과정에서 많은 어려움이 있었습니다. CI/CD는 적용하지 못하였지만 git pull 후 스크립트 파일을 통해 빌드 및 서버 실행할 수 있도록 하였습니다.
8. 그 외 개발한 것들
프로필 사진을 저장하는 API를 만들었습니다. MultiPart 데이터를 받고 S3에 저장한 후 URL을 멤버 테이블에 저장하도록 하였습니다. 또한 인터셉터를 추가해 멤버 아이디를 쓰레드로컬에 저장하게 했습니다. 서비스 계층에 있는 거의 모든 메서드가 멤버아이디를 필요로 하는 것을 보고 미리 저장해놓고 쓸 수 있게 했습니다.
9. 후기
데모데이 이후 최우수 프로젝트로 선정되었습니다. 복합적인 이유들이 있겠지만 많은 사람들의 호감을 이끌어내는 서비스를 만들었다는 사실에 매우 기뻤습니다. 채팅 기능에 대한 좋은 피드백도 들을 수 있어서 기분이 좋았습니다. 팀원분들과 5주간 별다른 갈등없이 완성하는 경험을 통해 협업에 대한 자신감도 가질 수 있었습니다. 하지만 좋은 결과와는 별개로, 부족한 점들을 조금씩 고쳐나가야겠다는 다짐을 하였습니다.