Projects/Team project - ShellWe

메인 프로젝트 'ShellWe' 일지

마손리 2023. 6. 29. 22:19

프로젝트 목표

물물교환 서비스 플랫폼 "ShellWe" 개발

 

프로젝트 기간

4주

 

개발 인원

6명(프론트 3명, 백엔드 3명)

 

 

프로젝트 아이템 선별

메인 프로젝트를 진행할때가 되어서 어떤 서비스를 개발을 해야할까?... 계속해서 고민해봤지만 도무지 아이디어가 떠오르지 않았다. 새롭고 신선한 서비스를 구상하고 싶었지만 생각해낸 아이디어들이 이미 존재하는 웹 혹은 앱 서비스들의 하위 버전에 속하지 않았다.

 

결국 내 기억에 따라 과거에 내가 불편했던 것들 혹은 아쉬웠던 일들을 생각해보고 그에 맞는 서비스를 개발해보자 생각하던 중 하나의 기억이 떠올랐다.

 

몇 개월 전, 친구가 새 애플워치를 사게되면서 이전에 사용하던 애플워치를 중고로 팔려고 하고있었다. 나는 애플워치를 구입하고 싶었지만 오랜 개발 공부로 최대한 생활비를 아껴야 하는 상황이었기에 구입하지 못하고 있었는데 친구의 제안으로 내 클러치백과 교환하게 된 기억이 떠올라 물물교환 서비스를 개발해보기로 결정하고 진행하게 되었다.

이와 비슷한 서비스가 있나 찾아보았지만 다행이도 없었다.(나중에 팀원에게 듣기론 해외에 이와 비슷한 서비스가 잇다고... 역시 세상에 없는건 없다.)

 

프로젝트의 개성을 살리기 위해 유형의 재산 뿐만 아니라 유형의 재산, 예를 들면 영어 레슨과 같은 이용자의 재능을 이용한 물물교환도 추가하여 프로젝트의 개성을 살렸다.

 

또한 화폐가 존재하기 아주 오래 전, 화폐를 대신했던 물품으로 조개껍데기가 사용됫던 것을 착안하여 조개껍데기의 Shell과 우리 같이 할래?의 뜻을 가진 Shall we를 결합하여 "ShellWe"라 프로젝트 명을 짓고 Project 로고를 조개로 하여 프로젝트의 아이덴티티를 만들었다.

 

 

프로젝트 계획

먼저 팀원들을 모집하기 전, 4주의 시간동안 문서작업 기간 1주일과 배포, 기능테스트 및 버그 fix의 기간으로 1주일을 제외하고나면 정작 기능개발에 사용할 수 있는 기간은 2주일밖에 되지 않았다. 그래서 현실적으로 내가 2주일 동안 구현할 수 있는 기능들 만을 모아 해당 서비스에서 구현할 기능들을 다음과 같이 정하게 되었다.

  • 웹소켓을 이용한 DM기능
  • 회원과 물품의 CRUD
  • 위치 서비스를 이용한 이용자의 주소 찾기, 이후 이용자 위치 근처의 물물교환 추천
  • 답변 형식으로 물물교환 희망자는 자신이 교환할 물품을 올리고 해당 게시글의 이용자는 답변들 중 하나를 골라 물물교환할 상대방을 매칭

 

 

D+1

팀원들을 모집하고 첫번째 회의를 열게되었고 정말 많은 아이디어들이 제안 되었다.

포인트 제도, 이용자 거래 점수, 찜하기 등등 너무 다양한 아이디어들이 제안되었지만 우리에게 정해진 기간은 4주... 기능구현을 위한 기간은 단 2주밖에 되지 않아 프로젝트의 방향성을 다시 정하고 그에 맞게 필요한 기능들만을 골라 개발에 들어가야 했는데 이 과정에만 하루를 소비하게 되었다.

 

결국 답변방식의 매칭이 아닌 게시글과 게시글간의 매칭으로 바뀌게 되었고 위치서비스는 기능구현에서 제거되었다. 이외의 JWT와 세션을 결합한 인증방식들과 같이 있으면 좋지만 서비스를 이용하는데 문제가 될것이 없는 기술들은 모두 중요도 Lv.2로 내려 모든 기능 구현이 끝나고 시간이 남는다면 진행하도록 계획을 만들었다.

 

 

D+2

어제 회의한 내용들을 바탕으로 프로젝트 기능들의 세부적인 내용들을 정해 사용자 요구사항 정의서를 작성하였다.

이후 백엔드 팀원들과 회의를 가저 어떤기능들을 개발할지 정했다.

 

나는 WebSocket을 이용한 DM기능을 맡게 되었는데 이게 참 걱정이다...

Node.js와 Socket.IO를 이용하여 한번 개발한 경험은 있지만 자바에서는 처음 구현해보는 기능이라 걱정이 되어 틈틈이 Spring WebSocket을 공부해야 했다. (일주일 정도 공부해보고 시간내에 개발이 불가능 하다 판단되면 Node.js로 구현해야 겠다...)

 

 

D+3

오늘도 마찬가지로 미팅, 문서작업, 미팅, 문서작업의 반복이었다. 

 

크게 변경된 사항들은 없었고 각 기능들의 세부사항들은 정하고 문서작업 및 깃과 코드 컨벤션들을 정했다.

이제 깃헙의 칸반들을 정리하면 바로 개발에 들어갈 예정이다.

 

특이사항으로는 부트캠프에서 현직개발자분께 멘토링을 받을 수 있는 자리를 마련해 주었는데 WebSocket서버와 Backend서버의 분리, 그리고 WebSocket을 위한 DB와 Backend서버를 위한 DB로 나눠서 개발하는것이 서비스 차원에서  좋을것 같다고 조언해 주셧다. 

 

시간적 제약으로 일단 Backend서버와 WebSocket서버는 분리하되 DB는 공동으로 사용하고 나중에 프로젝트가 끝나면 DB분리를 도전 해봐야겠다. (일단 중요도 Lv2의 다른 기능들부터 모두 구현한 후에...)

 

D+6

Spring WebSocket을 이용하여 WebSocket서버를 만들어주고 하나의 채팅방에 사용자들을 입장시키고 메세지를 주고 받는 기능, 메세지들을 DB에 저장, 이후 채팅방에 다시 입장시 이전에 작성한 메세지들을 불러오는 기능들까지 모든 기능 구현이 끝났다.

 

내일부터는 Spring Security를 접목하여 WebSocket 서버에서 사용자의 인증, 인가 처리를 진행하고 컨텍스트 홀더 혹은 WebSocket의 session에서 현재 로그인된 유저의 정보에 접근하는 로직을 만들 예정이다.

 

D+9

웹소켓 서버에 스프링 시큐리티 적용이 거희 마무리 되었다. 

현재 프로젝트에서는 백엔드 서버와 웹소켓서버를 따로 두었는데 클라이언트가 백엔드를 통해 토큰을 발급받으면 웹소켓서버에 접근할때 해당 토큰을 인증해주기 위해 JWT에 관한 로직만 적용하였다. 

 

클라이언트가 WS 프로토콜로 접근하기 전 발생하는 Hand shake 단계에서 사용자의 JWT를 받아와 인증처리하고 토큰에 담겨있는 사용자의 정보를 HandshakeHandler를 이용하여 WebsocketSession에 개개인의 사용자정보를 넣어주었다. 

 

이 과정에서 나중에 구현할 기능들을 위해서라도 spring-websocket의 STOMP를 사용하려 시도했었는데 클라이언트와의 연결에 계속해서 실패를 했다. 결국 ChatGPT 좌에게 물어보니 다음과 같은 답변을 받았다.

지금 당장 STOMP 프로토콜을 사용하기 위해 클라이언트 공부를 할 시간도 없고 현재 팀원들 중 웹소켓을 경험한 팀원도 없기에 일단 STOMP는 사용하지 않기로 결정했다. (STOMP 때문에 날아간 시간과 피폐해진 정신을 보상해주기 위해 피자를 시켰다...)

 

오늘 몇시간의 구글링을 통해 알게 된건, Spring Websocket에 대한 정보가 다른 웹소켓 프레임워크들에 비해 많이 부족하다는 점이었다. 특히나 웹소켓에 대한 기술적인 정보들은 자바 보다는 노드의 정보가 더 깊이있고 많다고 느껴 졌고 웹소켓 기능을 구현할때에는 자바보다는 노드를 더 선호하는 것 같았다. 

 

이번 프로젝트를 진행하면서도 느낀것이지만 Spring Websocket을 사용하여 클라이언트에 저장된 토큰을 받아오는 것조차 구글에 정확한 답이 없었다. 반면 노드의 경우 Socket.IO를 사용하면 손쉽게 헤더에 정보를 저장하여 노드서버에서 받아 사용할 수 있었다. 

 

결론적으로 다음에 웹소켓을 사용해야하는 일이 생긴다면 노드로 구현을 하고 굳이 자바를 사용해야 한다면 Jetty 서블렛을 공부해서라도 Jetty websocket을 사용해야겠다.

 

 

 

D+12

이번 프로젝트에서 사용될 웹소켓의 모든 기능들의 구현을 완료했다. 

  • 두명의 사용자를 하나의 룸에 연결
  • 각각의 룸에서의 메세지 전송 및 해당 메세지 DB에 저장
  • 룸에 재 입장시 이전 메세지 불러오기
  • 각각의 룸에 읽지 않은 메세지의 수 및 마지막 메세지 표시
  • 룸 삭제(DB에서 룸을 삭제하는 것이 아닌 삭제 상태만을 주어 한명의 사용자가 룸을 삭제하더라도 다른 사용자의 채팅 목록에서는 사라지지 않음)

 

이제 남은 작업은 테스트코드 작성, 클라이언트서버 및 백엔드서버와 연결하여 작동해보기, 배포 정도만 마무리하면 모든 작업이 끝날것으로 예상 된다.

 

특별사항으로는 멘토분과 두번째 미팅을 가졌는데 한가지의 지적사항과 한가지의 제안을 받았다. 

 

지적사항은 기능 구현당시 비동기 처리를 위해 new Thread().run을 사용했었는데 이경우 thread를 계속해서 생성 및 제거를 반복하게 되어 오버헤드가 발생한다는 것이었다. 

 

이경우 ThreadPoolTaskExecutor를 이용하여 쓰레드 풀관리에 관해 설정해주고 @Async 어노테이션을 이용하여 비동기 처리해주었다.

 

나머지 제안사항으로는 현재 프로젝트의 경우 백엔드서버와 웹소켓서버가 분리되어있지만 DB의 경우 하나로만 구성되어 있다. 그래서 백엔드와 웹소켓 서버간에 같은 엔티티들 그리고 스프링시큐리티를 사용하게되어 코드의 수정이 꽤나 번거로운 상황인데 멀티모듈 기술을 적용하면 좋을것같다는 조언을 받았다. 

 

일단 멀티모듈 부분은 모든 일정이 성공적으로 마무리 되면 적용해보기로 계획했다.

 

 

D+20

슬라이스 테스트와 유닛 테스트를 마치고 프론트엔드 팀도 DM기능을 적용시키는 작업이 진행되어 구현된 웹소켓 서버를 외부 주소로 가동시키는 작업을 진행했다.

 

첫번째 방법으로는 ngrok을 이용한 방법이었는데 ngrok에서 tcp주소를 임시로 받아 외부로 노출 시켜보려 했지만 안됬다...

구글에 검색해보니 나와같이 안되는 사용자들도 있었는데 뚜렷한 해결법은 없었다. (이에 대한 정보도 많지 않았다...아마도 스프링 웹소켓은 안되는듯 했다.)

 

두번째 방법으로는 AWS EC2에 웹소켓 서버를 올려 클라이언트에서 접근하는 방식이었다. 다행이도 이 방법으로 클라이언트에서 접근이 가능했지만 한가지 문제가 발생했다.

 

쿠키를 이용한 방법을 제외하고는 다른 특정한 헤더에 사용자 토큰을 담아 보낼 수 있는 방법이 없어 어쩔수 없이 쿠키를 이용하여 토큰을 건내받는 방식을 선택하게 되었는데 문제는 로컬서버에서는 잘 전달되던 쿠키가 EC2 서버에서는 전달이 되지 않았다. 

 

이를 해결하기 위해선 SameSite=none, Secure=true 속성을 부여하고 도메인을 구매 한뒤 해당 도메인을 https를 위한 등록까지 하면 EC2에서도 쿠키전달이 가능했다.(하지만 이 방법으로 WS 프로토콜에서도 쿠키가 전달되는지에 대한 정보는 없었다...)

 

일단 정보가 불확실하고 지금 당장 외부 클라이언트와 연결을 해야하는 상황에서 위의 방법은 적절치 못해 보여 기존의 쿠키를 이용한 토큰 전달 방식에서 URI를 통한 토큰 전달 방식으로 로직을 변경 하기로 했다. 썩 마음에 들지는 않았지만 HTTP 프로토콜과 다르게 WS 프로토콜은 URI가 외부로 노출되지 않기때문에 어쩔수 없는 상황이라면 이 방식을 많이 사용하는것 같다. (이럴줄 알았으면 처음부터 스택오버플로우 말 들을껄...)

 

결국 토큰 인증에 관한 로직 변경 이후, 외부 클라이언트와의 연결을 무사히 마칠수 있게 되었다.

 

이번 프로젝트 기간동안 계속해서 느꼈던 거지만 예전 노드jssocket.io를 이용한 웹소켓서버를 구현할때와 자바의 Spring Websocket을 이용할때가 너무나도 달랐다. socket.io는 몇가지 설정만으로도 특정 헤더에 정보를 담아 전달 받을 수도 있었고 특정 룸에 특정 메세지를 전달하는 것도 손쉽게 가능했다. 하지만 같은 기능들을 Spring Websocket으로 구현하기 위해선 STOMP 프로토콜을 사용하거나 만약 WS 프로토콜을 사용한다면 세부적인 로직들을 일일이 구현하거나 그마저도 안되는 기능들이 많았고 검색을 통해 해결하려해도Spring Websocket에 대한 정보가 한정적이고 양도 적었다.

 

 

결론

  1. 쿠키를 전달 받기 위해선 도메인이 필요하다.
  2. 웹소켓 서버를 구현할때 다시는 Spring Websocket을 사용하지 말자...

 

 

D+23


이제 모든 개발을 마무리한 뒤 배포를 시도하고 1차 기능 테스트를 진행하였다.


배포는 이미지 파일과 클라이언트를 위한 AWS S3와 백엔드와 DB를 위한 AWS EC2를 개설하고 깃헙 Actions를 이용하여 배포 자동화를 진행했다. 

몇몇 작은 에러들을 해결하고 배포자동화까지 무사히 진행되었지만 클라이언트와의 연결에서 1차 문제가 생겻다...

현재 어플리케이션은 Nginx를 이용하여 로드밸런싱을 해주려고 했었다. 문제는 클라이언트와 Nginx를 연결하면서 CORS 에러가 발생했고 하나의 CORS 에러를 해결하면 또다른 CORS 에러가 나타났는데 프로젝트 계획상 오늘 내로 배포를 완료해야 했기에 Nginx의 사용은 다음으로 미루게 되었다... 

다행이라고 해야할지 Nginx 가동을 멈추고 Spring Security에서 CORS 설정을 마처주니 클라이언트와 연결이 잘되어 1차 테스트를 진행했다.

역시나... 엄청난 양의 버그들이 발견됬다. 다행이도 백엔드의 버그는 몇개 발견되지 않아 한명은 버그를 잡고 한명은 DNS를 진행, 나는 방금 완료한 테스트에 대한 문서를 정리하도록 역할분담을 하였다.

이렇게 내일 다시 2차 테스트를 진행하고 3일 뒤 모든 버그들을 해결한 뒤 프로젝트를 완성하기로 계획했다.


 

D+25

발견된 모든 버그들의 수정을 완료하고 배포까지 아슬아슬하게 계획한 날짜에 마춰 어플리케이션이 완성되었다. 

 

프로젝트 진행 중 2가지 큰 실수를 했는데 마무리 단계에서 다시한번 집고 넘어가려고한다.

 

실수1

어플리케이션 설계 단계에서 나는 서버 과부화를 걱정하여 웹소켓 서버와 백엔드서버를 분리하여 설계하였다. 의도는 좋았으나 코드 구현 단계에서 Spring Security와 몇몇 엔티티들을 두 서버에 중복적으로 만들어야 하는 상황이 발생했는데 한서버에서 해당 코드들의 수정이 있을때 마다 다른 서버 또한 똑같이 수정해야만 하는 정말 비효율적인 작업 방식이 이루어 젔다...또한 비용 문제로 두서버 모두 하나의 EC2 인스턴스에 빌드하여 배포하게 되어 정말 처음의 목적은 사라지고 불편함만 남은 서버들이 구현되었다. 

 

서버 과부화를 염두하여 다시 계획을 짜게 된다면 두 서버를 하나의 서버로 통합시키고 로드밸런싱을 이용하거나 두 서버를 분리 시키는 대신 멀티 모듈 기술을 사용하여 좀더 효율적으로 서버를 구성할 것이다.

 

위와 같이 비효율 적인 설계가 발생한 원인으로는 첫번째로 경험부족, 팀 프로젝트를 처음 기획하고 진행하다보니 의도는 좋았지만 올바른 선택을 하지 못했다. 두번째 원인으로는 CS 기반 지식 부족이다. 처음부터 로드밸런싱이나 멀티 모듈과 같은 기술들을 인지하고 계획했으면... 하는 아쉬움이 많이 남았다. 

 

실수2

이 프로젝트에서 DM기능을 넣으려고 한 이유는 NodeJS와 Socket.IO로 웹소켓 서버를 한번 구현해 본적이 있어서 제한된 프로젝트 기간내에 여유롭게 개발이 가능할 거라 생각해서 였다. 그당시 Socket.IO를 사용하면서 난이도가 엄청 높다고 생각하지 않았고 내가 생각한 기능들을 구현하기도 쉬웠으며 클라이언트와의 연동도 쉬웠었다.

하지만 자바의 다른 프레임워크들을 보니 정말 구현 방식이 너무나도 달랐다. 어쩔수 없이 지금 당장 간단히 구현하기에 무리가 없어보이는 Spring Websocket으로 결정을 하고 (그 마저도 STOMP 구현에는 진행이 막혔다...) 모든 기능들을 일일이 수동적으로 구현해야 됬다... 

 

다행이 계획한 내가 맡은 기능들은 모두 구현했지만 다음부터 웹소켓은 그냥 노드JS로 구현해야겠다...

 

두번째 실수 또한 분석해볼 필요도 없이 원인은 경험부족과 지식 부족이었다... 앞으로 공부 많이 하자!!

 

 

 

 

 

 

 

완성된 서류

프로젝트 기획서 - 

https://drive.google.com/file/d/1QOQejNBIK81FRUzaMBc4HRVnhUCIgMN2/view?usp=drive_link 

 

사용자 요구사항 정의서 - 

https://drive.google.com/file/d/18yYjZBhytMul4DuM2MaQZSGn1lOBNnQR/view?usp=drive_link 

 

화면 정의서 - 

https://drive.google.com/file/d/1Z3rpJ-YYRb09Rrp85dHF51dvmqMXOlK1/view?usp=drive_link 

 

테이블 ERD - 

https://lucid.app/lucidchart/c09c5447-f251-438c-a51e-8c4dbeccb4bd/edit?invitationId=inv_b6eb560a-775a-49ac-8c43-8611e6725478&page=0_0# 

 

테이블 명세서 - 

https://drive.google.com/file/d/1DGmXD13h9hvCdTRy6ygjlWufMOeED8i9/view?usp=drive_link 

 

API 명세서 -

https://drive.google.com/file/d/1MJjJz7-uduEgY69PnBdJ4QJbJNjoJucK/view?usp=drive_link 

 

서비스 메뉴얼 - 

https://drive.google.com/file/d/1ve7AJY5Br1bZC3rMv1wG9nJeXMa8fF-l/view?usp=drive_link 

 

개발자 테스트 체크리스트 -

https://drive.google.com/file/d/1ryhg52BtxELCGpO9LVSt-hwAg3UMyUVa/view?usp=drive_link