본문 바로가기
Project/Server

첫 서버 개발 도전기(2) - 실시간 브레인스토밍 협업 툴, STORM

by 세이(Sayer) 2021. 2. 2.
728x90

본 포스트는 API 명세서 작성부터 API 개발 및 수정까지의 이야기를 담고 있습니다.

STORM의 워크플로우


앱잼에서 개발자 팀빌딩 이후 가장 바쁜 사람은 바로 서버 개발자다.  이유인 즉슨, 클라이언트 개발자들이 서버 연결을 시도하기 전 최대한 많이(정확히 말하자면 1차적으로 모두 끝내두고 수정 작업에 들어가야 한다.) API 개발을 끝내놓아야 하기 때문이다. 해커톤이기 때문에 수정이 매일 발생하고, 클라이언트 개발자와 소통하다 보면 수정사항이 계속해서 나오게 되지만 그래도 첫주에 바쁜 것이 이후 일정을 고려했을 때 가장 바람직하다.


<다시 보는 앱잼에서 서버의 역할>

1. 서비스의 핵심 기능과 워크플로우를 확인한 후, DB를 설계한다.
2. 클라이언트 개발자가 참고할 수 있도록 API 명세서를 빠르게 작성한다.
3. 작성한 API 명세서대로 API를 개발한다.
4. 개발한 API를 배포하여 클라이언트에서 접근이 가능하도록 관리한다.
5. 이후 클라이언트 개발자와의 소통 및 기획 수정 사항에 맞춰 API를 수정한다.

(수정은 언제나 발생한다. 수정 요청을 무조건적으로 거절하거나 승인하는 것은 이후 더 큰 오류를 발생시키는 원인이 된다. 수정을 요청할 때는 적절한 근거를 함께 준비해야 하며, 수정 요청을 받았을 때 그 근거가 타당하면 수정을 미뤄서는 안된다.)

이전 포스트에서 DB 개발에 대한 이야기를 적었으니, 이번엔 API 명세서 작성부터 API 개발까지의 이야기를 적고자 한다. 

---

 
DB 설계

새로운 서비스의 DB를 처음부터 구상하는 것은 어렵고도 즐거운 일이었다. 서버가 처음인 두 명이 머리를 맞대고 어떻게든 DB를 구성해보자며 뷰를 하나 하나 뜯어보던 기억이 아직도 생생하다.



1단계) 뷰를 참고하여 필요한 데이터 목록을 뽑아내기

당시 최신 버전의 뷰와 워크플로우를 펼쳐놓고 STORM 내에서 사용되는 모든 데이터를 체크했다.

"사용자가 라운드를 만들면 어떤 정보가 필요하지?"
➡ "어떤 프로젝트인지 프로젝트 정보가 필요하고, 라운드 목표도 보여줘야되고, 몇 라운드인지도 체크해야 돼!"
➡ "그럼 프로젝트 하나당 라운드가 여러개니까 1:N 관계고, 라운드 테이블은 프로젝트 정보를 참조해야겠다."
➡ "그럼 라운드 하나에 참여자가 여러 명이니까 참가자 테이블은 라운드 정보를 담는 테이블이랑 분리해서 만들어야겠네."

위와 같은 흐름으로 각각의 뷰를 뜯어보며 필요한 데이터들을 쭉 적어내려갔다. 게임처럼 라운드를 계속 생성해야 하는 서비스 특성상 데이터가 꼬리에 꼬리를 물고 연결되는 특징이 있어서(ex - 라운드 참여자 테이블에는 사용자 정보가 담기며, 각각의 라운드 참여자 정보에는 어떤 라운드에 참여하고 있는지를 나타내야 하기 때문에 해당 라운드 정보를 참조하고, 해당 라운드 정보는 어떤 프로젝트의 라운드인지를 나타내야하기 때문에 해당 프로젝트 정보를 참조하고...와 같은 식이었다.) 다른 데이터를 참조를 하거나 참조되는 부분이 많았기 때문에 모든 테이블에 idx column을 가장 먼저 적어넣었다.



2단계) 워크 플로우를 따라가며 나열한 데이터들의 관계를 정리하기 (정규화 작업)

1단계에서 정리한 정보들을 가지고 워크플로우대로 사용자가 하나의 서비스 사이클을 도는 과정을 시뮬레이션했다. 빠트린 정보는 없는지, 정보간의 관계를 잘못 정의한 부분은 없는지 여러 번 체크를 했는데, 이 과정에서 각 테이블간의 관계를 꼼꼼하게 정리하고, 기본키와 외래키를 설정하고, 부분적 종속을 삭제함으로써 테이블을 한층 깔끔하게 정규화할 수 있었다.

이렇게 시뮬레이션을 하며 데이터를 체크해가며 검토하다가, 한 카드에 여러 사용자가 각자의 메모를 남길 수 있기 때문에 메모의 경우 카드 테이블의 column으로 두는 것이 아니라, 별도의 테이블로 빼야 한다는 것을 발견할 수 있었다. 이 경험을 통해 DB를 설계할 때는 꼼꼼한 검토가 정말 중요하다는 것을 느낄 수 있었다. 카드 테이블에 넣어둔 채로 개발을 시작했으면 엄청난 후폭풍을 겪을 뻔했다.

왼쪽은 직접 손으로 그린 ERD. 이후 AQueryTool을 발견해 웹으로 옮겼다. 과거의 내가 ERD를 손으로 그릴 생각을 했다는게 아직도 너무 웃기다.



3단계) DB의 효율성을 고민하기 (필요시 역정규화 작업 진행)

테이블을 정리하고 나니 카드 테이블이 우리를 괴롭혔다. 원칙대로라면 라운드 테이블에서 프로젝트 정보(project_idx)를 가지고 있기 때문에 round_idx만을 참조하는 것이 맞지만, 카드를 출력하는 모든 뷰에서 프로젝트 이름을 출력해야했기 때문에 이렇게 DB를 구성하게 될 경우 계속해서 join을 써서 정보를 출력해야 할 것이 눈에 선했다.

project_idx를 카드 테이블에도 추가하여 중복되는 데이터가 공간을 조금 더 차지하더라도 빠른 정보 검색이 가능하도록 설계하는 것이 나을지, 아니면 검색 속도 차이가 그리 크진 않을테니 테이블을 이대로 두고 join을 사용할지 고민했다. 서버 개발자 둘이서 열심히 고민한 결과, 결론은 project_idx를 카드 테이블에 추가하는 것으로 결정했다. project_idx 자체가 공간을 많이 차지하는 데이터도 아닐 뿐더러, STORM은 카드를 생성하는 것이 전부인 서비스이기 때문에 공간이 부족할 정도로 많은 데이터가 쌓일 것 같지 않다는 예상을 했기 때문이었다. 생각보다 사용자들이 한 라운드에서 많은 카드를 사용하지 않는다는 것을 알게 된 지금으로써는 참 귀여운 고민을 했었다는 생각이 든다. 결과적으로 API를 개발할 때 join을 확실히 덜 쓸 수 있어서 깔끔한 코드를 작성할 수 있었기 때문에 효율적인 선택이었다고 생각한다.

우여곡절 끝에 완성된 ERD! 이후로도 호스트 테이블을 따로 분리하는 등, 꽤 많은 수정을 거쳤다.



처음 DB를 설계하는 것 치고는 꽤 많은 사항들을 고려하며 신중하게 설계한 것 같아서 돌이켜보니 새삼 뿌듯하다. 데이터 타입을 신중하게 결정하라는 서버 파트장님의 조언을 참고하여 다양한 상황에서의 데이터 흐름을 고민하고, 여러 번 꼼꼼하게 시뮬레이션하는 과정을 통해 탄생한 위의 ERD를 슬랙에 바로 자랑했었던 기억이 난다. 손으로 그린 ERD를 보고도 멋지다는 이모지를 찍어준 STORM 멤버들이 참 좋은 팀원이었음을.. 새삼 느낀다.


---

⚡API 명세서 작성⚡

서버 개발자에게 있어 가장 중요한 업무 중 하나가 바로 API 명세서 작성이다. 아무리 API 개발을 완벽하게 해도 명세서를 잘못 작성하면 클라이언트 개발자는 서버 연결을 제대로 할 수 없기 때문이다. 따라서 API명세서는 클라이언트가 보고 의문점 없이 바로 이해할 수 있도록 빠짐 없이, 명료하게 작성하는 것이 가장 중요하다.

앱잼 당시 작성했던 API 명세서. 우측 상단은 Request, 하단은 Response의 형태를 적어두었다. 자세한 설명은 아래 참고.

클라이언트가 접근하기 편한 깃허브 위키를 사용해 API 명세서를 작성하였다. 명세서를 작성할 때 고려한 점은 다음과 같다.

1) 각 문서 이름은 '[HTTP Method] 기능명(최대한 명료하게)'으로 적는다.
문서의 이름만 보고도 어떤 기능을 하는 API인지 알 수 있게 하여, 추후 API 개수가 많아졌을 때 쉽게 찾을 수 있도록 하였다. 내부에는 표의 형태로 HTTP 메소드, 경로, 기능 설명을 넣어 한번에 확인할 수 있도록 했다.

2) 각 문서에 해당하는 뷰 이미지를 삽입한다. (선택사항)
STORM이 기존에 전혀 접해보지 못했던 서비스다보니 전체적인 서비스 사이클은 물론 각 뷰에서 어떠한 API를 활용해야 하는지 어려워하는 개발자들이 많았다. 따라서, 어떤 뷰에서 어떤 API를 사용해야 하는지 명확하게 알 수 있도록 문서마다 각 뷰 이미지를 삽입했다.

3) Request Header, Request, Response를 빠짐 없이 적는다!
서버 연결에 있어 가장 기본적인 요소이다. 셋 중 하나라도 명세서에 없으면 클라이언트 개발자는 깊은 고민에 빠지게 된다(!) Request의 경우 params일 때와 body일 때를 구분하여 예시를 적어두었고, Response도 마찬가지로 success일 경우와 fail일 경우를 나눠 발생할 수 있는 모든 경우의 응답 메세지를 적어두었다. (참고 : header나 공통적인 fail response의 형태의 경우 공통 사항이라는 문서로 따로 묶어 작성하는 경우도 있다.)

4) 이외 특이 사항
보통의 경우에는 위와 같은 항목들만 포함되면 충분하지만, STORM의 경우에는 실시간 통신을 구현하기 위해 socket.io 라이브러리를 사용했기 때문에, 소켓 사용지점에 대한 설명도 위키에 별도로 추가하였다.

소켓 사용 지점을 정리한 위키 문서


위의 사항을 모두 고려하여 API 명세서 작성을 마쳤는데, 한 가지 문제가 발생했다. STORM이 레퍼런스가 없는 새로운 서비스다보니 팀원들이 전체적인 서비스의 흐름을 이해하기 어려워했다는 것이 그 문제였다. 기존 API 연결 이외에도 실시간 통신 기능이 있어 소켓 연결 및 연결 해제 시기를 정확히 공유할 필요가 있었는데, 이 사항들이 모두 API 명세서로만 전달되는 것이 아무래도 무리가 있다고 느껴졌다. 그래서 내가 선택한 방법은 워크 플로우를 따라 각 API를 사용하는 지점을 표기한 설명서를 제작해 전달하는 것이었다.

투머치인 느낌이 없지 않아 있지만 의미 있는 노력이었다고 생각한다. 이 설명서를 제작해 배포한 이후로 클라이언트 개발자들이 흐름을 오해해 잘못된 API를 연결하는 일이 줄어들었기 때문이다. 물론 앞에 앉혀놓고 설명해주는 것이 더 효과적이긴 하지만 리모트로 작업을 했어야 했기 때문에..

이처럼 각 프로젝트에 맞게 적절한 방법을 취하여 클라이언트 개발자가 잘 이해할 수 있는 API 명세서를 작성하는 것이 정말 중요하다는 것을 느꼈다. 물론 모든 프로젝트에 위와 같은 별도의 설명서를 제작할 필요는 없으며, API 명세서에 필요 이상으로 힘을 들이는 것은 오히려 리소스 낭비라고 생각한다. 위의 설명서는 STORM이라는 새로운 서비스의 워크 플로우에 대해 팀원 모두가 동등한 이해도를 가지고 개발에 참여할 수 있도록 하기 위한 나만의 한 가지 방법이었을 뿐이다.



---

 

⚡API 및 소켓 통신 개발⚡

STORM의 서버 아키텍처는 아래 그림과 같다. 사용한 클라우드 서비스와 node.js에 대한 공부는 별도의 포스트에서 정리하기로 하고, 이번 포스트에서는 협업을 하는 과정에서 만났던 문제, 고민, 새롭게 알게 되었던 점 등을 위주로 이야기하고자 한다.

일러스트레이터 프로그램을 사용해 만들었던 광기 어린 아키텍쳐...

1) socket.io에 관하여

STORM에는 라운드에 입장할 경우 현재 입장한 다른 유저들의 목록을 확인하는 기능이 필요했기 때문에 실시간 통신을 구현해야 했다. 이 부분을 어떻게 구현하면 좋을지 주변 개발자들에게 조언을 구하고, 열심히 구글링을 해서 socket.io 라이브러리를 찾을 수 있었다. STORM은 프로젝트 코드를 입력해 프로젝트 및 라운드에 입장하기 때문에, 소켓 room을 프로젝트 코드로 구분했다.

room은 말 그대로 채팅방과 같은 개념이다. join과 leave를 이용해 socket은 특정 room에 들어가고 나갈 수 있으며, 서버에서 특정 room에 이벤트를 emit할 경우, 해당 room에 join한 socket들은 동시에 이벤트를 감지할 수 있다. 우리는 이 점을 활용해 새로운 사용자가 라운드에 입장하거나 나가기 버튼을 눌렀을 때 등 상황에 맞는 다양한 이벤트를 생성해두고, 각 이벤트가 발생했을 때 해당 라운드에 join되어 있는 socket들에게 참여자 목록을 리로드하라는 이벤트를 emit하여 참여자 목록을 갱신하도록 했다.

코드는 참 간단한데 처음에 코드를 짤 때는 꽤나 고민을 했었다. 매일 같이 읽었던 공식 문서 https://socket.io/



2) Git Flow에 관하여

앱잼 전에 진행됐었던 클라이언트-서버 합동세미나에서는 단순히 본인의 이름을 딴 브랜치를 생성하여 작업했었다. 그래서 브랜치를 나누는 기준이 있을 거라고는 상상도 못했는데, 안드로이드 파트의 레포를 슬쩍 구경했더니 브랜치가 10개가 넘어서 눈을 휘둥그레 뜨고 살펴봤었다. 대체 브랜치가 왜 이렇게 많지? 안드로이드 파트원들에게 물어보고 열심히 구글링한 결과 'git flow'라는 놈을 알 수 있었다. 처음에는 '대체 귀찮게 왜 브랜치를 여러 개 만드는거지?'라는 생각을 했는데, API를 많아야 4~5개 만들던 합동세미나와는 달리 20~30개씩 만들어야 한다는 것을 깨닫고 나서는 아주 흔쾌히 깃 플로우를 적용해서 개발을 시작했던 기억이 난다.

앱잼 당시 정했던 서버 파트의 코드 컨벤션과 감격적인 첫 깃 플로우. 효과적으로 커밋 메세지를 작성하기 위한 방법이 있다는 것도 처음 알았다.

git flow에 관한 개념적인 내용은 추후 별도의 포스트로 정리하고자 한다. 소스트리에서 여러 갈래로 뻗어 나가는 브랜치를 보며 '참 예쁘다'라고 생각하는 내 자신이 꽤나 개발자스러워서 혼자 웃었던 기억이 난다. 그게.. 개발자의 요건이 아닐텐데.. 이외에도 깃 이슈와 칸반보드를 활용해 서로의 작업 현황을 체크하고, 풀리퀘를 날릴 경우 상대방이 해당 내용을 확인한 후 승인을 해주는 등 원활한 협업을 위한 여러 가지 방법을 고민하고, 적용했었다.

이슈와 브랜치를 연동해 풀리퀘를 날림과 동시에 이슈 close가 가능하다는 것을 알게 된 것은 먼 훗날의 이야기..



3) 상태 코드에 관하여


상태 코드에 대해 열심히 구글링을 해서 각 오류 상황에 맞는 에러 코드를 넣어뒀는데, 신기하게도 4xx 코드는 클라이언트에서 아예 에러로 처리해버려 그 이후에 코드에 따라 분기 처리를 할 수 없다는 사실을 알게 되었다. 그래서 클라이언트 개발자와 열심히 머리를 맞대 우리 팀만의 약속으로 여러 상태코드를 활용했던 것이 기억에 남는다. (현업에서는 상태 코드를 어떻게 활용하는지 정말 궁금하다. 한 API에서 상태 코드 여러 개가 필요한 경우가 거의 없으려나?)

CREATED: 201,
CANNOT_JOIN : 202,  //열심히 고민한 이름들이다. 추억이 새록새록
NOT_PREPARED : 204,
BAD_REQUEST: 400,
INTERNAL_SERVER_ERROR: 500,
DB_ERROR: 600

이외에도 여러 상태 코드를 사용했다.



4)  프로젝트 구조에 관하여

* routes : 특정 엔드포인트에 대한 클라이언트 요청에 애플리케이션이 응답하는 방법을 결정
* controllers : 모델로부터 요청된 데이터를 얻어내거나, 사용자에게 알맞는 HTTP Response를 전달
* dao : 요청에 따라 DB에 접근해 데이터를 받아와 반환
(* middleware : 요청에 대한 응답을 하는 과정 중, 특정한 기능을 수행하기 위해 중간에 거쳐가는 메소드)

dao 폴더랑 service 폴더를 구분해서 직접적으로 DB에 접근하는 query들을 따로 분리해 transaction으로 관리를 해보자, 리프레시 토큰을 사용해 보안을 강화해보자, 등등 여러가지 의견을 냈었는데 아무래도 서버 파트 두 명 모두 처음으로 서버에 도전하는 것이다보니 여러 가지 시도를 하는 것보다 '일단 핵심 기능이 에러 없이 잘 돌아가는 서버를 만들자!'라는 목표를 달성하는 것이 더 의미 있다고 판단해 가장 기본적인 패턴(routes, controllers, dao(models)로 분류)으로 개발을 진행했다. 만약 리팩토링을 진행하게 된다면, 소셜 로그인 등 다양한 기능을 더 시도해보고 싶다.



5) 협업에 용이한 툴 활용에 관하여

실시간 통신 기능이 있었기 때문에 마지막 이틀은 클라이언트 개발자와 소켓 통신을 테스트하느라 정신이 없었다. 계속해서 로그를 찍어보고 플로우에 맞춰 코드 수정을 해야했는데, 이 수정사항을 바로 EC2에 반영하면 다른 부분을 테스트하고 있는 다른 클라이언트 개발자에게 혹시라도 방해가 될까 걱정이 됐었다. 그때 감사하게도 임시로 base url을 생성할 수 있는 툴을 알게 되었다!

Ngrok는 NAT 및 방화벽 뒤의 로컬 서버를 보안 터널을 통해 공용 인터넷에 노출합니다. 즉, 개발 머신에서 네트워크 환경 설정 변경 없이 로컬에 실행중인 서버에 대한 공개 HTTPS URL을 생성하여 외부에서 접근 가능하도록 하는 도구입니다. (공식 홈페이지 : ngrok.com/)

ngrok를 활용하여 임시 url을 전달한 덕분에 서버 개발자 둘이서 역할을 분담하여 동시에 여러 클라이언트 개발자들의 수정 요청 사항을 바로 적용하고 로그를 확인할 수 있었다. 사실 이 과정에서 url이 만료되면 클라이언트 개발자들이 앱을 다시 빌드해야 된다는 단점이 있긴 했는데, 만료되기까지 유효 시간이 8시간 정도로 꽤 길기도 하고, 클라이언트 개발자들도 서버 개발자들이 각각 다른 부분을 맡아 본인들의 수정 요청 사항을 빠르게 적용해주는 것을 더 선호했기 때문에 큰 문제가 되진 않았다. 만약 추후 다른 프로젝트에서 ngrok를 사용할 경우, 이러한 부분을 고려할 필요가 있어 보인다.

 

---

 

앱잼을 하는 동안 언제나 고민했던 것은 '어떻게 하면 클라이언트 개발자와 효율적으로 소통할 수 있을까?'였다. 처음부터 완벽한 API는 존재할 수 없을 뿐더러, 해커톤 특성 상 기획이 계속해서 수정되기 때문에 어떻게든 수정 사항은 반드시 발생할 수밖에 없다. 따라서 처음부터 완벽한 API를 개발할 수 없다면 최대한 클라이언트 개발자와 원활하게 소통해서 해당 수정 사항을 빠르고 정확하게 반영하는 것이 서버 개발자의 역량이라고 생각했기 때문이다.

지금 다시 돌아보면 정말 효율적인 선택을 했다고 생각되는 부분도 있고, 어떻게 저렇게까지 무모한(!) 방법을 시도했을까 내 자신이 놀라운 부분도 있다. 하지만 클라이언트 개발자와 협업하는 과정에서 여러 가지 유의미한 고민들을 하고 나름의 해결 방법을 생각해내기 위해 노력했던 경험들이 있었기 때문에 지금의 내가 (전보다는 더) 효율적으로 개발 협업을 할 수 있는 개발자가 되었다는 것은 분명하다. 물론 앞으로 더 배워나가야 할 점이 산더미지만, 그 시작을 즐겁게 해낼 수 있도록 도와준 STORM은 나에게 있어 참 소중한 프로젝트다.


이번 포스트가 서버 개발 관련 고민에 초점을 맞춘 포스트였다면, 다음 포스트에서는 첫 협업을 통해 느낀 점과 배운 점들을 중심으로 이야기를 풀어보고자 한다 :)

댓글