Search

스타트업 백엔드 개발자의 회고

Intro

2020년 8월부터 Hang5 서비스를 만들고 있습니다.
지인의 소개를 통해 처음 Hang5 팀을 알게 되었는데 좋은 팀원들과 흥미로운 서비스에 반해 두 번의 미팅 만에 팀에 합류하기로 결정했습니다.
MVP를 완성한 이후로 12월 말부터 마케팅을 시작하였고 대략 3주 만에 북미 데이팅 앱 평균보다 낮은 CPI로 1000명 회원가입(다운로드 1500+)을 달성 했습니다.
새로운 도약을 준비하는 시점에서 잠시 지난 5개월을 돌아보는 시간을 가지려고 합니다.
백엔드 개발자로서 초기 스타트 업에서 배우고 느꼈던 것을 적어 보겠습니다.

What I did

1. PyTest

팀에 합류하고 한 달은 테스트 코드를 짰습니다. 기존 프로젝트에 테스트가 없었기 때문에 설계부터 시작했습니다.
테스트를 설계 할 때 많은 고민을 했는데 적은 노력으로 테스트를 관리할 수 있는 방법에 초점을 맞추었습니다.
API의 response 테스트를 위해 schema 라이브러리 사용을 도입했고 쉽고 빠르게 테스트를 짜고 있습니다.
또 효율적인 테스트를 위해 lazy fixture PyTest Plug-in과 unittest.mock 등을 활용하고 있습니다.
만약 위의 설명한 라이브러리와 플러그 인이 궁금하다면 아래의 링크를 참고 해주세요.
테스트를 짤 때 Test Run마다 race condition이 발생하는데 원인을 찾을 수 없던 적이 있습니다.
일주일 정도 디버거와 씨름 했는데 Test Run 이후에 남아있던 캐시 때문에 발생하는 문제였고 이를 해결하는 과정에서 redis와 많이 친해질 수 있는 기회가 되었습니다.
더불어 테스트를 짤 때 fixture 등 데이터의 흐름을 관리하는 게 까다로운 작업이 될 수 있다는 것을 깨달았습니다.
현재는 테스트 함수 별로 필요한 서버 자원 상태에 맞게 fixture를 주입(dependency injection)하여 사용하고 있습니다.

2. Import Mixins

테스트 설계를 완성하고 신규 feature 개발과 버그 해결에 집중 했습니다.
그러던 중 컨텐츠를 쉽고 빠르게 업로드 하고싶다는 컨텐츠 팀의 요청이 있었습니다.
이전까지는 Django admin에서 직접 컨텐츠를 변경하고 업로드 했습니다.
상대적으로 컨텐츠 서빙이 많은 앱인데 이를 매번 수동으로 업로드 하는 건 불편한 점이 많았습니다.
스테이징 서버와 프로덕션 서버에 연결된 두 개의 데이터베이스에 일일이 데이터를 업로드 하는 건 지루하고 비효율적인 일입니다.
더불어 컨텐츠의 버저닝이 필요했고, 컨텐츠를 만드는 분들은 데이터베이스에 대한 지식이 전혀 없었습니다.
협의 끝에 컨텐츠 팀이 Google spread sheet에 최신 컨텐츠를 업로드하면 admin에서 해당 시트를 읽고 파싱해 데이터베이스에 적용하는 import system을 만들었습니다.
기존에 django-import-export라는 훌륭한 라이브러리가 존재하지만 생각보다 필요에 맞게 수정하여 쓰기가 어렵게 설계 되어 있었습니다.
다른 여러가지 옵션을 고민하다가 import mixin을 직접 만들어서 필요한 곳에 붙여서 쓰기로 결정 했습니다.
또한 import 전후로 컨텐츠 변동 사항을 보는 기능도 (django-import-export를 참고하여) diff-match-patch 라이브러리로 구현했습니다.
300줄이 넘는 mixin을 직접 만들어 보면서 class에 대한 이해와 다양한 파이썬 스킬을 활용하고 연습할 수 있는 좋은 경험이었습니다.

3. Passwordless Authentication

Hang5는 간편한 회원가입을 위해(가입 중 이탈을 줄이기 위해) 유저로부터 비밀번호를 받지 않고 휴대폰 인증으로 유저 인증을 대체하고 있습니다.
django-rest-auth를 수정하여 사용했기 때문에 기술적으로 어려운 점은 없었습니다.
Authentication에 관심이 많았는데 인증 코드를 직접 만져볼 수 있어서 무척 재미있었습니다.
django-rest-auth는 더 이상 maintain 되지 않기 때문에 jazzband/dj-rest-auth를 사용하길 권장합니다.

4. Pytz & Datetime

Hang5의 사주 데이터의 정확성은 서비스의 신뢰성과 연결되기 때문에 정확한 시간을 계산하는 것이 아주 중요합니다. 또 미국을 타겟으로 서비스하기 때문에 모든 부분에서 Time-Zone, DST(daylight saving time) 등을 고려하여 작업해야 합니다.
정확한 사주 계산을 위해
1.
Time-zone의 기준 경도와 유저의 출생 도시의 경도를 비교하여 유저의 출생시간을 분 단위로 보정하고
2.
태어난 날의 DST를 계산하여 출생시간을 분 단위 보정하는 등의 계산을 하고 있습니다.
이런 계산은 모두 Python Standard Library인 pytz와 datetime을 이용하고 있습니다. 그 중에서도 특히 pytz를 많이 활용하고 있는데 Python으로 시간을 다루는데 필요한 대부분의 기능을 제공하고 있다고해도 과언이 아닙니다.
만약 Django에서 time-zone을 어떻게 사용하는지 자세히 알고 싶다면 아래의 링크를 참고 해주세요.

5. Face Recognition

프로필 사진의 검증을 위해 최근 AWS Rekogntion을 도입했습니다.
AWS Rekognition을 통해 유저가 프로필 사진을 업로드하면 해당 사진이 프로필 사진으로 적합한지 판별합니다.
프로세스를 간단히 설명하면, presigned URL을 통해 임시 bucket에 검증할 사진을 업로드 하고 Rekognition을 통해 사진을 판별한 뒤 적합한 사진인 경우 전체 사용자의 사진을 관리하는 bucket으로 이동시킵니다.
현재 크게 세 가지의 기준을 통해 프로필 사진의 적합성을 판별하고 있습니다.
1.
사람의 얼굴인지 (동물, 가상 캐릭터, 그림, emoji 등 제외)
2.
얼마나 또렷한 사진인지 (blur가 심한 사진 제외)
3.
유해한 사진이 아닌지 (과도한 노출, 폭력적인 사진, 기타 불쾌한 사진 제외)
세 가지 기능 모두 Rekognition이 알려주는 사진의 판별 값을 필요에 맞게 조정하여 구현하였습니다.
Rekognition을 이용할 때 주의해야 하는 점이 있습니다.
Boto3 client의 region과 Rekognition이 사진을 읽는 S3 bucket의 region이 같아야 합니다.
둘의 region이 다르면 계속해서 403 Access Denied를 일으킵니다.
또 Presigned URL을 통해 S3에 form-data 형식으로 사진을 업로드 할 때 authorization을 위한 credentials과 함께 이미지를 전달합니다.
이 때 이미지는 file이라는 key와 함께 전달해야 하는데 반드시 form-data로 전달하는 값 중 가장 마지막에 있어야 에러가 발생하지 않습니다.

What I realized

1. 모델링의 중요성

프로젝트 경험이 쌓일수록 데이터베이스 모델링의 중요성을 더 실감합니다.
일반적으로 백엔드 퍼포먼스에 가장 큰 영향을 미치는 부분이 데이터베이스 I/O와 네트워크 통신 부분입니다.
모델링에 따른 쿼리 최적화에 의해 API 서버의 응답속도가 큰 차이를 보이곤 합니다.
그러므로 모델링을 많이 공부하고 고민 해보는 것을 권장합니다.
최근 모델링에 관해 가장 크게 깨달은 점은 정규화에 집착하지 말자는 것 입니다.
예전에 프로젝트를 진행할 때 데이터가 중복되는 것이 좋지 않다고 생각해 모든 테이블을 정규화하여 One-to-many 또는 Many-to-many로 설계했던 적이 있습니다.
그런데 모든 데이터를 정규화하게 되면 instance에서 두 번 이상의 relation을 거쳐서 데이터를 조회해야하는 상황이 많이 발생했고, 쿼리를 할 때도 필요 이상으로 복잡한 ORM을 써야만 했습니다.
불필요하게 많은 조인과 서브 쿼리는 일반적으로 쿼리 성능이 좋지 않기 때문에 퍼포먼스 이슈를 야기합니다.
상황에 맞게 비정규화를 활용하면 코드의 가독성이 높아지고 데이터 관리의 편리성이 높아지는 등 다양한 측면에서 업무 효율성을 증대 시킬 수 있습니다.
또 데이터를 분석할 때도 쿼리 성능이 좋아 훨씬 효율적인 경우가 많습니다.
따라서 비정규화는 상황에 맞게 고려해 볼 만한 전략입니다.
비정규화가 항상 정답이라는 말은 아닙니다. 정규화에 집착하지 말자입니다.

2. URL 설계의 중요성

REST의 장점이자 단점은 표준이 없다는 것입니다.
그렇기 때문에 RESTful URL을 설계 할 때 팀 단위로 또는 개발자 스스로 일정한 기준을 정하지 않으면 클라이언트와의 소통에 불필요한 비용이 발생하고, 최악의 상황에는 API가 제대로 관리되지 않아 프로젝트 자체가 산으로 갈 수도 있습니다.
RESTful API 설계에 관해서 rule of thumb이 있지만 결국 이를 체화하기 위해서는 개발자 스스로 많은 고민을 해야하고 다양한 경험이 필요합니다.
URL은 가급적 general하게 설계하는 것이 좋습니다.
특정 기능을 위해서만 존재하는 URL을 만들기보다는 추상화 된 URL을 먼저 고민하고, 그 안에서 URL parameter를 통해 분기 처리하는 것이 낫습니다.
예를 들어 tier가 premium인 User의 정보만 조회 가능한 API를 만든다면
# Bad /users/premium/
Python
복사
# Good /users/?tier=premium
Python
복사
일반적으로 위의 URL 보다는 아래의 URL이 낫습니다. 이유는 간단합니다.
새로운 tier가 추가된다고 할 때
위의 설계는 새로운 티어에 맞는 새로운 URL을 만들고 controller를 추가해야 하지만
아래의 설계는 parameter를 처리할 코드만 추가해주면 되기 때문입니다. (혹은 코드 수정이 필요 없을 수도 있습니다.)

3. Versioning의 중요성

모바일 앱에 사용되는 API를 설계 할 때 항상 versioning에 대해 고민해야 합니다.
매번 앱을 강제 업데이트 시킬 것이 아니라면 이전 앱 버전에 호환이 가능한 API 설계가 필요합니다.
예를 들어, 앱 1.0.1 버전에서는 다음과 같은 API response를 사용하다가
{ "user": 1 }
Python
복사
1.0.2 버전을 위해 response를 아래처럼 바꾼다면
{ "users": [1] }
Python
복사
아직 1.0.1 버전을 사용하는 사용자는 해당 API를 호출하는 부분에서 충돌이 일어납니다.
기존 response에 새로운 데이터가 추가되는 것은 일반적으로 문제가 되지 않지만 이미 사용 중인 API에서는 가급적 데이터 타입을 바꾸거나 반환되는 컨테이너의 key를 바꾸는 일이 없어야 합니다.
만약 타입이나 key가 바뀌어야 한다면 새로운 버전의 API를 추가하는 것이 낫습니다.

Outro

가벼운 회고를 통해 지난 5개월 간 했던 작업과 깨달은점 몇 가지를 정리 했습니다.
부족한 면이 많지만 항상 좋은 에너지를 나누어 주는 팀원들이 있어 언제나 즐거운 마음으로 일하고 있습니다.
이 자리를 빌어 모든 Hang5 팀원들에게 감사를 표합니다.
읽어주셔서 감사합니다.