집순 프로젝트 후기
1. 부동산은 불편하다
- 저는 네이버 부동산으로 남의 집을 염탐하는 것을 좋아합니다. “저기서는 어떻게 살아볼까” 새로운 삶을 꿈꿔보곤 해요.
- 그런데 그거 아시나요. 내 기호에 딱 맞는 부동산을 찾아주는 서비스는 어디에도 없습니다.
- 네이버 부동산이나 직방, 피터팬 뿐만이 아닙니다. 북미의 zillow, realtor 등도 마찬가지입니다.
- 다들 이렇게 말하는 듯합니다. “네가 뭘 원하는 지는 모르겠어, 여기 다 줄테니 알아서 체크박스에 클릭해. 방 2개, 에어컨 1개, 욕조 1개.”
- 하지만 이것만으론 부족합니다.
- 누군가는 편의점이 가깝기를, 누군가는 체육시설이 주변에 있기를 바랄 수도 있죠. 누군가는 지하철만 좋아하고 버스는 싫어하지만, 누군가에겐 둘 다 상관 없을 수도 있습니다. 주차장에 물이 잠기는게 무서워 지형상 고지대를 선호할수도 있구요. 상대적으로 제일 낮은 범죄율의 도시에 살고싶어할 수도 있습니다.
집순 프로젝트가 만들어진 이유입니다.
다양한 부동산 정보를 조합해 의사 결정을 돕고 싶다.
2. 컨셉
- 유저마다 개인화된 매물 점수를 제공하고 싶었습니다.
- 유저에게 여러 가지
평가 방식
을 제시한 뒤, - 유저가 스스로 평가 방식을 조합해 나만의 매물 점수를 가지게 하고 싶었습니다.
- 그런데 잘 생각해보면, 평가를 하기 위해서는 데이터가 필요합니다.
- 가령 매물의 공원 접근성을 평가하려면, 적어도 전국에 있는 공원의 위치나 면적 등은 가지고 있어야 하죠.
- 그래서 각 평가의 원천 정보를 수집하고 적재하는
파이프라인
도 만들기로 했습니다.
3. 구현
➊ 매물 수집 → ➋ 매물 점수 계산에 필요한 정보 수집 → ➌ 매물 점수 계산 → ➍ 정규화
-
집순의 데이터 처리 과정은 네 단계로 구성됩니다.
-
각 단계는 Job으로 구성돼 있습니다. DataPipelineService가 job을 순서대로 실행합니다.
-
각 단계는 수평적 확장에 열려있습니다.
- 가령 새로운
➌ 매물 점수 계산
컴포넌트를 새로 만들고 싶다면, ScoreCalculator 인터페이스를 따르는 새 클래스를 만들어 추가하면 됩니다.
- 가령 새로운
➊ EstateJob
부동산 매물을 찾아 DB에 저장합니다. 이를테면 네이버 부동산에서 네이버 매물을 json으로 가져오는 식이죠.
➋ SourceJob
매물 점수 산정에 필요한 데이터를 DB에 저장합니다. 이를테면 전국 공원 정보가 담긴 csv 파일을 테이블에 저장하는 식이죠.
➌ ScoreJob
특정 척도에 따라 매물별로 점수를 계산합니다. 이를테면 공원의 개수, 면적 등을 고려해 11, 20, 19 따위의 점수를 내는 식이죠.
➍ NormalizeJob
계산된 점수를 0-10점 사이로 정규화합니다. 이를테면 11, 20, 19 따위의 점수를 5, 10, 7점 따위로 변환하는 식이죠.
4. 운영환경 가정하기
- 일부 챌린지는 요청을 얼마나 효율적으로 처리하는지를 다룹니다.
- 그러므로 집순 서비스의 운영환경을 가정하고, 부하를 추정해 보겠습니다.
초당 요청 수(RPS)
- RPS는 서버의 처리 성능을 나타내는 지표입니다. 1초에 얼마나 많은 요청을 처리하는지를 나타냅니다. RPS = \frac{Total\ Requests}{Total\ Time\ (seconds)}
피크타임 초당 요청 수(RPS peak)
-
저는 높은 부하가 걸리는 시간대의 RPS를 추정하고자 했습니다. 다음과 같이 계산해 보겠습니다1. RPS_{peak} = \frac{Peak\ Requests}{Peak\ Time\ (seconds)} = \frac{DAU \times RPU \times r_{peak}}{t_{peak}}
- DAU: 일 사용자 수
- RPU: 1인당 평균 요청 수
- r_peak: 피크 시간대 요청 비율 (총 요청 중 피크에 발생하는 요청의 비율)
- t_peak: 피크 시간대 길이
-
이제 각 요소를 추정해 보겠습니다.
결론
RPS_{peak} = \frac{100,000 \times 30 \times 0.1}{7,200} = \frac{300,000}{7,200} \approx 41.67
- 집순은 최대 초당 약 42개의 요청을 처리할 수 있어야 합니다.
- 여유롭게 설계하기 위해 목표 RPS는 50으로 설정하겠습니다.
5. 챌린지: 공간 쿼리 도입하기
문제
- 부동산 매물은 위치 기반 검색이 필수적입니다.
- 일반적인 좌표 기반 쿼리(
WHERE X >= ? AND X <= ? AND Y >= ? AND Y <= ?
)는 데이터가 증가할수록 성능 저하를 겪을 수 있습니다. - 최대 부하 상황(50명의 동시 접속자)에서도 안정적인 성능을 제공해야 합니다.
해결
- PostGIS의 공간 인덱스를 도입했습니다.
- 위치 정보를 효과적으로 처리하기 위해 Geometry 객체를 사용하고, 그에 맞는 TypeHandler를 구현했습니다.
성과
- 다양한 데이터를 대상으로 성능 및 부하 테스트를 거쳤습니다.
- 부하(피크) 상황에서, PostGIS와 일반 쿼리의 응답 시간을 비교했습니다5.
- 속도: PostGIS는 일반 쿼리보다 5배 빠른 응답 시간을 제공합니다6.
- 안정성: PostGIS는 응답 시간의 변동성을 낮춰 일관된 사용자 경험을 제공합니다.
- 확장성: 전국 단위로 확장하더라도, 부하 상황에 흔들리지 않는 서비스를 제공할 수 있습니다7.
한계
- 테스트에 testcontainer를 사용했기 때문에, 실제 운영 환경에서의 수치와는 차이가 있을 수 있습니다. 실제 환경에서는 여러 독립적 구성 요소(CPU, 디스크 I/O, 네트워크 등) 지연 시간이 곱으로 누적됩니다.
6. 챌린지: 요청 추적 도입하기
문제
- 로그 크기가 늘어날수록 텍스트 기반 검색은 선형적으로 느려집니다.
- 서비스가 확장되며 분리된다면, 사용자 요청과 로그가 분산되기에 문제 추적이 어려워집니다.
해결
- 분산 환경에서의 요청 추적을 위해 RequestID 필터를 구현했습니다.
- 모든 요청에 UUID 기반의 RequestID를 발급하여 MDC에 저장하고, 응답 헤더에도 포함합니다.
성과
- 속도: Request ID 기반 검색은 평균 1530.19ms(약 1.5초) 더 빠릅니다.
- 확장성: 로그가 지수적으로 증가하더라도 동일한 검색 속도를 유지하며, requestId 기반의 모니터링 툴을 연동할 수도 있습니다.
한계
- 실제 운영 환경에서는 테스트 환경보다 더 복잡한 로그 패턴과 구조가 발생할 수 있습니다.
- Datadog, ELK 스택 등과 통합하면 더 효율적으로 운영할 수 있습니다. 다만 분산 시스템에서의 지원은 미비합니다.
7. 챌린지: 선택적 리소스 처리
(작성중)
References
-
피크 요청이 평균 요청보다 얼마나 높은지 비율을 알 수 있다면, 피크 요청 수를 구할 수 있습니다.피크\ 요청\ 수 = ➊전체\ 요청\ 수 \times ➋총\ 요청\ 중\ 피크에\ 발생하는\ 요청의\ 비율➊은 DAU(일일 사용자 수)와 RPU(일인당 평균 요청 수)를 활용합니다. 잘 알려진 지표를 활용하면, 다른 기업의 수치를 참고할 수 있기 때문입니다. 따라서 전체 요청 수 = DAU * RPU입니다. ➋는 단순 휴리스틱을 활용합니다. 후술하겠지만 서비스별 피크 요청의 비율을 일반화하기 어렵기 때문입니다. ↩
-
호갱노노, 직방의 2020년 안드로이드 DAU는 각각 30만, 25만입니다. 2024년 총 DAU는 각각 53만, 34만입니다. 이에 근거해 직방의 DAU를 30만 정도로 추산했습니다. ↩
-
집순의 엔드포인트 수와 유즈케이스를 고려했습니다.
카테고리 엔드포인트 요청 횟수/세션 매물 관련 GET /estates/map (지도 로딩 및 이동) 10-15회 GET /estates/{id} (매물 상세 조회) 5-8회 POST /estates/{id}/favorite (찜하기) 1-2회 DELETE /estates/{id}/favorite (찜 해제) 1-2회 점수 유형 GET /score-types (점수 유형 조회) 1-2회 POST/DELETE /score-types/{id} (활성/비활성) 1-2회 사용자 GET /users/me/favorites (찜 목록 조회) 1-2회 총 요청 수/세션 20-30회 -
프롭테크 서비스의 시간-요일-계절별 트래픽 패턴은 알려진 바가 없습니다. 다만 이커머스의 경우, 시간대별 구매 패턴이 잘 알려져 있습니다. 한 조사에 따르면 오전 10시와 12시에 가장 많은 구매가 일어납니다.
시간대 구매 수 퍼센트(%) ... ... ... 10 56 6.67 11 53 6.32 12 56 6.67 ... ... ... 간편한 계산을 위해 구매 트래픽을 조회 트래픽으로 단순 변환했습니다. 어떤 서비스들은 피크 시간대에 20-30%의 부하를 겪는다고 조사했으나, 근거가 명확치 않아 제외합니다. ↩ ↩2
-
테스트는 최대 ➊150만개의 레코드 테이블에 ➋2,000번의 요청을 보내는 것으로 진행합니다.
➊의 값은 2023년 기준 대한민국의 총 건물 수에서 추정했습니다.
➋의 값은 Analytica의 표본 크기 선택 가이드에서 구했습니다.m = p(1-p) \times \left(\frac{c}{\Delta p}\right)^2 = 0.95 \times 0.05 \times \left(\frac{2}{0.01}\right)^2 = 1,900 \approx 2,000RPS를 50으로 가정했으므로, 초당 50번의 요청을 40초동안 시뮬레이션합니다. ↩ -
피크타임에서 사용자 95%의 지연 시간을 재현하기 위해 P95값을 추정했습니다. 최대 데이터 셋(150만개)에서 2,000번의 요청을 3번씩 시행해 수렴했습니다.
계산 결과 ➊PostGIS는 416.88ms, ➋일반 쿼리는 2094.78ms입니다. 이는 피크타임에서 ➊PostGIS 사용자들의 95%는 416.88ms보다 낮은 지연시간을, ➊일반 쿼리 사용자들의 95%는 2094.78ms보다 낮은 지연시간을 경험할 것이라는 뜻입니다.
-
Jakob Nielsen의 UI/UX 응답 시간 가이드라인에 따르면, 반응 속도가 0.1초는 즉각적, 1초는 방해받지 않는 수준, 10초는 인내심의 마지막 단계라고 설명합니다. 최대 부하에서 데이터베이스 레벨의 416.88ms 지연은 “방해받지 않는 수준”이라고 해석할 수 있습니다. ↩
-
3,000만줄은 1일 추정치입니다. 앞서 DAU를 10만명, RPU를 30회/일로 가정했으므로 일\ 평균\ 총\ API\ 요청\ 수 \approx 10만명 \times 30회 = 300만\ 요청/일 각 요청이 평균 10줄의 로그를 생산한다고 가정하면 일\ 평균\ 로그\ 수 = 300만\ 요청/일 \times 10줄 = 3,000만줄 각 요청에서 생성되는 로그의 양은 다음과 같이 가정했습니다.
카테고리 로그 유형 발생량/API 요청 API 요청당 평균 로그 API 입출력 로그 2개 서비스 레이어 로그 3-5개 데이터베이스 조회 로그 2-3개 총 로그 발생량/API 요청 7개-10개