서담서치 프로젝트 리딩 후기

Roeniss Moon·2021년 8월 22일
1

들어가기 전에: 아래 설명하는 내용은 2021년 여름방학을 이용한 첫 배포까지의 과정을 담고 있습니다. 2022년 3월에 저 혼자 진행한 2.0 버전에서는 많은 내용이 달라졌는데 이 내용이 궁금하시다면 댓글 바람.

소개

https://bit.ly/서담서치

서담서치 (ssodam search)는 서강대학교의 컴퓨터 중앙동아리 SGCC 멤버 다섯명이서 만든 크롬 익스텐션이다.



이 익스텐션은 서강대학교 de facto 공식 커뮤니티 사이트인 서담의 내용을 검색하는데 사용할 수 있다. 사용자는 본인이 서담 유저임을 인증하면, 익스텐션 내에서 검색 기능을 사용할 수 있다. 검색은 서담의 모든 게시물을 대상으로 하며, 본문과 제목 또는 둘 중 하나만 골라 형태소 검색을 할 수 있다.

https://bit.ly/ssodamsearch 에서 다운받을 수 있지만 서강대학교 학생이 아니면 사실상 사용이 불가능하다.

프로젝트의 시발점

서담 자체에 검색 기능이 없나요? 있다. 그런데 문제가 많다:

  • 서담은 한 사람이 기획/개발/운영을 담당하고 있다. 초인적인 한 사람의 노고로 만들어졌다는 소리다.
  • 그 사람은 본업이 따로 있다. 개발을 착착 해내기 어렵다는 소리다.
  • 서담의 검색 기능은 매우 기초적인 수준이다. mysql의 LIKE 을 쓰는 것 같다.
  • 부하를 줄이기 위해 본문, 댓글 검색이 막혔다. 현재는 제목 검색만 가능하다.

예시를 들어보자면:

서담의 게시물이 다음과 같이 5개 있다고 해보자:

학교 주변에 미용실 좋은데 추천좀
신촌 주변 미용실 추천해주실 분 계신가요?
미용실추천좀미용실추천좀미용실추천좀미용실추천좀미용실추천좀제발추천좀
다미들아 학교 주변에 괜찮은 미용실 (남자) 추천좀!
대흥역 주변 남자 미용실 추천좀

이 상황에서 '미용실 추천좀'으로 검색했을 때 나오는 결과는 마지막 하나 뿐이다. '정확하게' 일치해야만 한다.

커뮤니티 사이트에서 검색 기능을 쓸 일이 많지는 않다. 보통은 가장 최근에 올라온 인기게시물만 보니까. 그렇지만 막상 검색할 때 불편한 건 사실이다. 이에 대해선 내가 다른 블로그에 쓴 글을 읽어보시길: https://roeniss.tistory.com/entry/모호한-질문의-문제점-부제-플랫폼의-책임

동기

위와 같은 상황에서, 자연스러운 반응은 '업데이트를 건의한다' 내지는 '업데이트를 기다린다'일 것이다. 검색이라는게, 사이트 안에 존재하는게 아주 자연스럽지 않은가.

하지만 그럼에도 불구하고 별도의 익스텐션으로 분리해야겠다는 생각이 들었는데 그건 다음과 같은 이유들 때문이었다:

  • ES (엘라스틱 서치) 체험. 단순히 띄워보고 끝나는게 아니라 그걸로 유의미한 결과를 만드는 것까지.
  • 기다린다고 될 것 같지가 않았음. 서담은 각종 문제가 있는데, 운영자에게 아마도 이 이슈는 극히 마이너한 이슈일 듯.

그리고 아래 설명할 4명의 멤버가 추가됨으로서, 두개의 목표가 더 추가되었다:

  • PM 체험해보기. 즉, 코드를 짜지 않으면서 IT 서비스를 만들어보기.
  • 후배님들이 보다 실용적인 프로젝트를 경험할 수 있게 돕기.

아무튼 그럼에도 불구하고 익스텐션이 있어야만 온전히 검색 기능을 누린다는 디자인은 분명 기괴하다. 하지만 뭐... 그건 일단 그 정도에서 생각을 중지했다.

멤버 모집

서담 운영자에게 공식적으로 크롤링 허가를 받으면서 (mysql 데이터를 전달받는 것이 더 좋았겠지만, 귀찮아할 것 같아서 별로 안좋아할 것 같아서 아예 얘기도 안꺼냈다) 동시에 전체적인 청사진을 그렸다.

이 모든 것을 구축하는 건 나 혼자로도 어찌저찌 가능했을 것이다. 하지만, 그런 (나 혼자 뚝딱 해치우고 만족하는) 종류의 프로젝트에 순간적으로 염증이 났고, SGCC (Sogang Computer Club) 디스코드에서 멤버를 모집했다. 찾아보니.. 7월 5일이다:

[오후 1:54] roeniss: #서담서치  (좋은 이름 추천받음)
만들어야 하는 것 
- 1. 크롤러 : 서담 글을 주기적으로 크롤링해서 검색 서버에 적용해야됨.
- 3. 크롬 익스텐션 : 아래 첨부한 네이버영어사전 같은 UI를 만들어야 됨. 
- 5. API 서버 (및 CF 세팅) : 회원가입/로그인/계정인증 & 검색엔진에 요청 보내고 결과 리턴
- 6. 검색서버 : 엘라스틱 서치 서버
[오후 1:54] roeniss: (*위에 올린 다이어그램 이미지*)

[오후 1:55] roeniss: 대단한 실력자 찾는 거 아니고, 완벽하지 않아도 괜찮습니다. 어차피 코드 다 볼거니까...
이력서 프로젝트란에 적을 수 있는 형태로 만들어 가시면 될 듯 해요.
참여하실 경우 위에 4개 중 일부를 담당하게 되십니다.
아예 모르는 사람은 곤란하고, 적당히 아는 것 같은데 정확히 뭘 만들어야되는지 모호하다면 오늘 밤 11시 디코 음성채널에서 물어보세용 (10분동안 기다리고 아무도 안오면 감)

후배님들에게 좋은 포트폴리오 소재를 주고 싶다는 생각도 했지만, 이내 그건 본인이 알아서 잘 챙겨가는 거라고 고쳐 생각했다. '나는 밥상을 디자인하고, 상에 올라가는 찬들은 당신들이 만들고. 나는 다 된 밥상을 만들도록 가이드하고, 당신들은 하나씩 만들다가 뭔가 자랑할 게 생긴다면 ㅡ 혹은 자랑할 수준으로 만들 수 있다면 ㅡ 좋은거고.' 그런 생각이었다.

개발하지 않고 프로젝트를 완성하는 것이 나의 목표였다.

"어차피 코드 다 볼거니까"는 PR을 내가 리뷰한다는 뜻이었다. 어차피 멤버들이 가급적 서로 독립된 소스코드를 보도록 할 것이었기 때문에 상호 리뷰는 사실상 포기한 채 출발했다.

최종적으로 4명의 멤버가 추가되었다. 두 명의 파이썬 개발자 (한 명은 크롤링에 대하여, 다른 한명은 엘라스틱 서치에 대하여 오너십을 부여), 한 명의 리액트 개발자, 한 명의 노드(node.js) 개발자. 그 외의 것들 (인프라, 홍보, 대외협력(?), 문의접수, 프로젝트 관리)은 내가 했다.

매우 중요한 지점 하나는, 지원한 사람들에게 별도의 자격 또는 테스트를 요구하지 않았다는 점이다. 나는 그렇게 함으로서 그들에게 자기 제품에 대한 오너십을 줄 수 있을 것이라 판단했다. 가이드는 주되 구현에는 관여하지 않는 방향으로 말이다.

도구 선별

  • 태스크 관리 : Jetbrains Youtrack (Jira + Wiki)
  • 코드 : Github
  • CD : CircleCI (일부 적용)
  • 서버 : Raspberry pi 4 (4cpu, 8G ram)
  • 수시 커뮤니케이션 : 유트랙 > 카카오톡 > 디스코드
  • 정기 미팅 : 주 2회, 스크럼 형태, 디스코드 화상채팅 & 구글밋
  • 운영 환경 : 도커

유트랙

지라와 위키를 합쳐놓은 무언가인데, 젯브레인이 만들었기에 믿고 써봤다. 전반적으로 다루기 쉬웠는데 바꿔말하면 아주 약하게 다뤘기 때문에 심도있는 평가는 못하겠다.

도합 150개 이슈 정도가 완료 또는 진행중이고, v1.0.0 배포를 위한 이슈는 모두 처리되었다 (8/22 현재 배포 심사 진행중).

위 사진에서 볼 수 있듯 각 컴포넌트 별로 Project를 쪼개는 방법을 택했다. 사전작업을 제외한 각 프로젝트는 각각 깃헙 레포 하나씩 연결되어있어서 PR 머지 커밋으로 Resolve 할 수 있다.

위 사진처럼 Knowledge Base 라고... 팀 내 위키를 운영할 수 있다. API 서버 쪽의 API 관련 문서는 이후 Swagger 로 대체되었고, 다음 스크럼 때 할 얘기는 내가 까먹을까봐 몇가지 사항들을 가끔 적어놓던 것. 나머지는 문서로 남겨둘만하다고 판단되어 적은 것들이다.

개별 이슈는 이런 식으로 생겼다. 참고로 디폴트값으로는 State가 한 여덟개 있는데 Submitted, Resolved, Canceled 빼고 다 지웠다. Progressing인가? 하여튼 '진행중'을 나타내는 것을 지운 이유는 지라가 익숙치 않은 분들을 대상으로 너무 많은 것을 요구하는 것 같았기 때문이다.

깃헙

Free-pan Orgranization 은 PR 리뷰어를 한 명만 지정할 수 있구나.. 그렇구나...

CircleCI

몇몇 컴포넌트는 main 브랜치에 커밋되면 (즉 main에 바로 푸시 또는 PR에서 머지) 라즈베리파이에 바로 반영되도록 하였다. '자주' 변경이 있을 것 같은 api 서버와 crawling 서버에 대해 적용했는데 아주 좋은 판단이었다. 참고로 작동하는 방식은:


circleCI에서 특정 레포의 워크플로우 활성화 
코드 커밋
github에서 circleCI로 웹훅 전송
circleCI의 자체 서버가 돌아가기 시작
circleCI에 등록해놓은 ssh key로 라즈베리파이 접속 
해당 레포 코드가 있는 폴더까지 이동 
코드 최신화 (git pull)
도커 이미지 새로 굽기 (docker build . -t ssodam-search/ss-api:latest)
기존 컨테이너 부시고 새 컨테이너 올리기

그렇다. 무중단 배포 같은 건 없다.

아래에서 다시 설명하겠지만 모든 컴포넌트를 각각 도커로 말아서 적용했다. docker-compose를 사용해볼까 했는데, 여러 서비스가 독립적으로 돌아가는게 맞다고 판단하여 그냥 각각 돌아가게 냅두기로 했다.

라즈베리파이

불쌍한 나의 라즈베리파이 (이하 Betty)는 별 의미도 없이 매일 열을 뿜는 기생충에서, 이번 기회로 유의미한 서버로 레벨업했다. 클라우드 서비스 대신 베티를 사용해야겠다는 생각은, 백준 온라인 저지가 자체 서버를 사용해 채점한다는 점에서 착안했다 (이젠 아닌듯).

그리고 계산해보니, 왜 클라우드가 비싸다고 하는지 알 수 있었다. 베티와 비슷한 성능의 ec2 인스턴스의 가격은 오하이오 리전 기준으로 최소 월 5만원은 필요했다. 반면에 우리의 베티는 대충 32기가 SD카드를 포함해 12만원 정도. 물론 bandwidth와 file I/O는 많이 후달리지만...

실제 구현 결과물

진행상황을 어떻게 요약하지.. 하다가 최종적인 결과물을 보면서 얘기하는게 낫겠다 싶어서 바로 결과로 진행한다.

중간중간, 그리고 거의 막바지에 멤버들에게 전체 아키텍쳐를 반복해서 설명했다. 이 프로젝트가 정말로 그들 자소서에 도움이 되려면, 본인이 맡지 않은 부분에 대해서도 이해하고 있어야 한다고 생각했다. 이 프로젝트는 MSA가 되도록 반쯤 의도했고 반쯤은 우연히 맞아 떨어졌기 때문에 더더욱 그러하였다.

그림이 좀 난해할 수도 있는데 위에서부터 보자.

R은 베티를 말하는 거고, 베티 속의 작은 네모들은 모두 도커 컨테이너다.

크롬 익스텐션

  • 유저가 익스텐션 아이콘을 클릭해서 리액트로 구현된 크롬 팝업을 띄움. 상태는 리덕스로 관리하고 (리덕스는 프론트 개발자의 공부를 겸한 목적이 크다), 유저 정보 관련 및 쿼리 API는 api 서버의 도메인으로 연결되어 있다.
  • 도메인은 모 도메인 업체에서 구매했으며, 클라우드플레어에서 관리하고 있다. CF 앞단까지는 https고 그 뒤부터는 http로 통신한다. 즉, flexible mode를 쓰고 있다. 아무튼 CF에서는 도메인을 ip로 바꿔서 이후 뒷단 요청을 진행한다.

API 서버

  • CF의 요청은 nginx로 들어간다. reverse proxy, static file serving 두 가지 목적을 생각했으나 딱히 정적 파일이 없으므로 1번 기능만 사용하는 셈이다. API 서버는 별도의 포트에서 실행중이다. (7777은 예시임..)
  • API 서버는 크게 두 종류의 기능을 하는데, 하나는 유저 정보 제공이고 다른 하나는 쿼리 수행이다.
  • 유저 정보는 회원가입, 로그인 두 가지로 볼 수 있는데 이 중 회원가입이 재미있다. 이 부분은 다른 섹션에서 자세히.
  • 쿼리 수행은 ES(엘라스틱 서치) 클러스터를 직접 찔러서 조회한다.
  • API 서버의 에러는 센트리로 전송하고 있다. ES 쪽 오류가 4xx이라 일단은 400 이상의 모든 예외를 받고 있음.

서강대학교 학생 인증에 관하여

우리 서비스는 매우 특이한 인증 방식을 쓰고 있다. 이름하여 서담 간접 인증 시스템. 방식은 이러하다.

  • 익스텐션을 통해 서담서치에 회원가입한다.
  • 랜덤한 문자열과 서담 게시물 링크를 받는다.
  • 해당 게시물에 부여받은 문자열을 댓글로 단다.
  • 5분뒤 api 서버의 크롤러(봇)가 해당 게시물 댓글을 긁어와 그 문자열이 있는지 확인한다. 문자열이 있으면 정회원으로 등업한다. 즉, 서강대학교 학생이라고 간주한다.

이렇게 하지 '않으면서' 서강대 학생임을 인증하는 유일한 방법은 sogang.ac.kr 이메일로 본인인증을 하는 것이다. 그렇게 안 한 이유는:

  • 서강 이메일로 인증을 하는 것은 '새로' 무언가 가입한다는 인상을 준다. 이는 유저에게 심리적 장벽처럼 작용할 수 있다.
  • 댓글 쓰는게 압도적으로 빠르다.
  • 댓글로 인증하는게 더 재미있다. (한 번도 안해본 인증 구현 ㅎㅎ)
  • 유저가 서담에 강제적으로 글을 쓰도록 만들어서 이 서비스를 홍보하도록 의무화한다 -> 글쓰기에서 댓글쓰기로 바꾸면서 이 효과를 잃었다. 서담에 '최신 댓글' 섹션이 없어서. (생각해보니 이 이유가 1번이었던 듯)

참고로 이건 우리가 처음하는 건 아니다. 초창기 op.ggsolved.ac 가 이 방식을 직/간접적으로 채택했다. 흠흠.

아무튼 이런저런 단점과 장점이 다 있지만, 이미 서강대 학생이라고 잘(?) 인증된 사람만 가입할 수 있는 서담의 회원인지 확인하는 방법으로! 서강대 학생인증을 delegate 했다.

엘라스틱 서치 (세팅)

  • 파이썬 멤버 둘 중 한 명은 ES 에 대한 오너십을 가져가도록 했다. 즉, 서담에서 뭘 크롤링해서 (최종안: 본문과 제목) 어떻게 넣어서 어떻게 쿼리할것인지에 대한 전체 판단을 맡겼다. 의도한 바는 가중치를 조절해가면서 유저가 제일 원하는 결과가 조회되도록 하는 것이었지만... 조금 어드밴스드였을지도? 호호
  • nori tokenizer를 사용해 (이것이 이 프로젝트에 ES를 쓴 근본적인 원인이다) 형태소를 분석했고, 이후 ngram 을 추가적으로 적용해 쿼리의 정확성을 높이려고 했다.
  • 베티가 armv7 프로세서를 쓰기 때문에, 공식 이미지가 아닌 arm 이미지를 찾아서 사용해야 했다. 완전 latest 는 아니지만 얼추 최신버전이고 잘 돌아가는 것 같아서 그냥 가져다 썼다.
  • 싱글 클러스터임 ㅎㅎ

크롤러

  • 서담에서 주기적으로 게시물을 가져오는 크롤링 코드가 구현되어 있다. 새로 글이 추가되기도 하고 기존 글이 삭제 또는 수정되기도 하므로 무한히 빙글빙글 도는 크롤러다.
  • 서담의 게시물은 양이 굉장히 많다. 그리고 크롤러가 서버에 부담을 주면 안되므로, 속도를 굉장히 낮췄다. 그 결과, 현재 세팅 기준으로 모든 게시물을 한 바퀴 조회하려면 현실 시간으로 약 14일 정도 소요된다. 글이 늘어날수록 이 사이클은 점점 커질것이다. 따라서 '가장 최근 500개 글', '가장 최근 10000개 글', '전체 글' 을 조회하는 세 개의 크롤러를 만들어서 돌리고 있다. 환경변수가 서로 다른 3개의 컨테이너가 떠 있다는 소리다.
  • '전체 글 크롤러'의 경우, 중간에 서버가 다운되면 다시 컨테이너가 뜨더라도 처음부터 다시 조회하는 걸로는 부족하다. 어디까지 크롤링 했는지 체크를 해야했고, 이 부분을 위해 크롤러는 일정 갯수 크롤링 후 sqlite 의 파일에 자신이 마지막으로 조회한 게시물 ID를 저장한다. 그리고 이 sqlite 파일은 docker bind mounting 을 이용해 로컬의 디렉토리랑 붙어있다.
  • 이 로컬의 sqlite file은 또다른 컨테이너에서도 돌고 있다. 바로 db 백업 전용 컨테이너다. 여기는 파이썬 스크립트가 크론잡으로 세팅되어있으며, 하루에 한번 sqlite file의 백업본을 만든다. 위 그림에서 오른쪽 아래의 컨테이너다. ((4)는 잘못 기록한 것 같다. (3)이 맞다. 3개의 크롤러 각각에 로컬 디렉토리가 고유하게 마운팅 되어있고, 각각의 볼륨을 디비 백업 컨테이너가 마운팅해서 공유하고 있다)
  • 크롤링은 성능을 위해 selenium 대신 requests 를 사용했다.
  • 에러가 나면 메일이 전송되도록 세팅했는데, 별로 쓸모는 없었다.

자투리 - ip updater

  • 공유기가 dhcp를 쓰고 있어서 매우 희박한 확률이지만 public ip가 바뀔 수 있다. 따라서 CF의 api를 사용해서 주기적으로 현재 베티의 ip를 CF에 갱신해주고 있다. 이 컨테이너는 베티의 크론잡으로 실행되며, --rm 옵션이 붙어있다.

자투리 - ES warmer

  • 일정 시간동안 쿼리 요청이 없으면 ES의 메모리 캐싱이 풀리면서(?) 그 다음 first quest request 가 어마어마하게 느린 현상을 확인했다. cold start... 라고 말할 수 있을 것 같다.
  • 이를 방지하기 위해, 5분에 한 번씩 별 의미없는 쿼리를 요청하는 작은 컨테이너다. ip updater와 마찬가지로 베티의 크론잡으로 걸려있는 --rm 컨테이너다. 컨테이너로 할 필요는 없었는데 그냥 함.

추가사항

아키텍쳐와 그 구현에 대한 나의 총평은 "Recoverability는 확보했지만 Observability가 없음"이다. 장애가 났을 때 빠르게 인지하기 힘들고, Logging 이 부실하고 중앙화되지 않아 상황 파악이 어렵다. 따라서 어쩌다 한번씩 내가 베티에 접속해 떠있는 컨테이너들의 상태를 보지 않는 이상... 문제가 발생했는지도 모르는 상황이 생긴다. :(

이정도면... 설명해야 하는 건 다 설명한 것 같다. 이러한 종합적인 결과물을 구상한 가장 큰 이유는 (위에서도 말했지만) 후배님들이 '뭔가 실무스러운 것' 혹은 '자소서에 적을만한 신선할 것' 혹은 '그동안 못해본 프로젝트' 를 체험했으면 좋겠다는 생각이 있었기 때문이다. 그렇다. 나는 참말로 은혜로운 선배이다.

이 이상의 디테일한 설명이 필요한 부분은 댓글로 문의 주시라. 내가 답할 수 있는 선에서 최대한 자세히 얘기해드리겠다.

느낀 것들

많이 있는데, 생각나는대로 두서없이 적어본다. 자세한 설명도 적지 않는다. 길게 말하는 편이 구차할테니.

그리고 혹시라도 우리 팀원 중 누군가 이걸 볼까봐 미리 말해두는데, 여러분은 단 한 명도 빼놓지 않고 모두 잘 임무를 완수했다. 그걸 잊으면 안된다. 물론 내가 생각한 것보다 살짝 지체되긴 했지만, 어쨌든 원래 계획대로 9월 개강 전에 할 수 있는 걸 다 했지 않은가. 고생하셨고, 잘해주셔서 감사.

  • 팀을 무작위로 (혹은 열정만으로) 선발하면 안된다: 수준 차이가 너무 나는 멤버들이 한 팀에 있으면 최소 한 쪽이 지친다. 그리고 나도 톤을 바꿔가면서 대해야 되서 힘들다.
  • 마감기한을 정해두어야 한다: 러프하게라도. 각 이슈에 대해.
  • 로깅을 놓치지 말자. 중앙화도.
  • CD는 확실히 개발자의 부담을 덜어준다. CI는 내 (인프라담당자의) 부담을 줄여주는 것 같다. 후자는 멤버들의 수준이 테스트를 완벽히 구사할 레벨이 아니었기 때문에 느낀 것임.
  • PM은 ㅡ 내가 한 걸 PM이라고 부를 수 있다면 ㅡ 쉽지 않다. 특히나 멤버들의 역량을 정확히 파악하지 못한 상태라면.
  • PM의 역할은 어디부터 어디까지인가, 를 파악하는 과정에서 나의 성격과 마주할 수 있었다. 별로 좋은 기분은 아니었지만.
  • 막판에 마음이 급해져 내가 코드를 짠 건 지금 생각해도 잘한 짓인지 못한 짓인지 그냥 짓인지 잘 모르겠다. 선택지가 없었나? 있었나? 모르겠다.
  • 내가 알고 있는 것을 저 사람도 알고 있는지 확인할 방법이 필요하다. 당장 생각나는 방법은 없는데 아무튼 그걸 확인하는게 진짜 어려운 것 같다.
  • 팀으로 일하는 것도 나쁘지 않은 것 같다. 매니징은 처음 해보는 거라서 더더욱 그랬던 것 같고.
  • 항상 그랬지만, 나는 불만이 많은 타입이다. 안으로나 밖으로나. 그걸 컨트롤할 재주가 더 필요한 것 같다. 바꿔 말하면... 불만을 잘 얘기하는 법을 터득해야 할 것 같다.

ps) 미래의 나에게: https://muchtrans.com/translations/10xdeveloper.ko.html?hide-original=true

profile
기능이 아니라 버그예요

1개의 댓글

comment-user-thumbnail
2021년 8월 31일

😄

답글 달기