..

집순 프로젝트 후기

  1. 부동산은 불편하다
  2. 컨셉
  3. 구현
  4. 운영환경 가정하기
  5. 챌린지: 공간 쿼리 도입하기
  6. 챌린지: 요청 추적 도입하기
  7. 챌린지: 선택적 리소스 처리

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: 피크 시간대 길이
  • 이제 각 요소를 추정해 보겠습니다.

    • DAU: 집순은 신생 서비스이므로, DAU는 10만으로 가정합니다2.
    • RPU: 한 유저가 평균 30회정도를 요청한다고 가정합니다3.
    • r_peak: 피크 시간대 요청 비율은 0.1(10%)로 가정합니다4. 10%는 다소 낮아보일 수 있으나, 최종 RPS를 보수적으로 책정할 예정이기 때문에 상쇄가 가능합니다.
    • t_peak: 피크 시간대 길이(초)는 7,200초(2시간)으로 가정합니다4.

결론

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에 저장하고, 응답 헤더에도 포함합니다.

성과

  • 다양한 로그 크기를 대상으로 성능 테스트를 수행했습니다.
  • 3,000만 줄의 로그에서 텍스트 기반 검색과 requestID 기반 검색을 비교합니다8.
  • 속도: Request ID 기반 검색은 평균 1530.19ms(약 1.5초) 더 빠릅니다.
  • 확장성: 로그가 지수적으로 증가하더라도 동일한 검색 속도를 유지하며, requestId 기반의 모니터링 툴을 연동할 수도 있습니다.

한계

  • 실제 운영 환경에서는 테스트 환경보다 더 복잡한 로그 패턴과 구조가 발생할 수 있습니다.
  • Datadog, ELK 스택 등과 통합하면 더 효율적으로 운영할 수 있습니다. 다만 분산 시스템에서의 지원은 미비합니다.

7. 챌린지: 선택적 리소스 처리

(작성중)


References

  1. 피크 요청이 평균 요청보다 얼마나 높은지 비율을 알 수 있다면, 피크 요청 수를 구할 수 있습니다.피크\ 요청\ 수 = ➊전체\ 요청\ 수 \times ➋총\ 요청\ 중\ 피크에\ 발생하는\ 요청의\ 비율➊은 DAU(일일 사용자 수)와 RPU(일인당 평균 요청 수)를 활용합니다. 잘 알려진 지표를 활용하면, 다른 기업의 수치를 참고할 수 있기 때문입니다. 따라서 전체 요청 수 = DAU * RPU입니다. ➋는 단순 휴리스틱을 활용합니다. 후술하겠지만 서비스별 피크 요청의 비율을 일반화하기 어렵기 때문입니다. 

  2. 호갱노노, 직방의 2020년 안드로이드 DAU는 각각 30만, 25만입니다. 2024년 총 DAU는 각각 53만, 34만입니다. 이에 근거해 직방의 DAU를 30만 정도로 추산했습니다. 

  3. 집순의 엔드포인트 수와 유즈케이스를 고려했습니다.
    카테고리 엔드포인트 요청 횟수/세션
    매물 관련 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회

  4. 프롭테크 서비스의 시간-요일-계절별 트래픽 패턴은 알려진 바가 없습니다. 다만 이커머스의 경우, 시간대별 구매 패턴이 잘 알려져 있습니다. 한 조사에 따르면 오전 10시와 12시에 가장 많은 구매가 일어납니다.
    시간대 구매 수 퍼센트(%)
    .........
    10566.67
    11536.32
    12566.67
    .........

    간편한 계산을 위해 구매 트래픽을 조회 트래픽으로 단순 변환했습니다. 어떤 서비스들은 피크 시간대에 20-30%의 부하를 겪는다고 조사했으나, 근거가 명확치 않아 제외합니다.  2

  5. 테스트는 최대 ➊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초동안 시뮬레이션합니다. 

  6. 피크타임에서 사용자 95%의 지연 시간을 재현하기 위해 P95값을 추정했습니다. 최대 데이터 셋(150만개)에서 2,000번의 요청을 3번씩 시행해 수렴했습니다.

    계산 결과 ➊PostGIS는 416.88ms, ➋일반 쿼리는 2094.78ms입니다. 이는 피크타임에서 ➊PostGIS 사용자들의 95%는 416.88ms보다 낮은 지연시간을, ➊일반 쿼리 사용자들의 95%는 2094.78ms보다 낮은 지연시간을 경험할 것이라는 뜻입니다.

    보다 자세한 결과는 다음을 참조하세요. 

  7. Jakob Nielsen의 UI/UX 응답 시간 가이드라인에 따르면, 반응 속도가 0.1초는 즉각적, 1초는 방해받지 않는 수준, 10초는 인내심의 마지막 단계라고 설명합니다. 최대 부하에서 데이터베이스 레벨의 416.88ms 지연은 “방해받지 않는 수준”이라고 해석할 수 있습니다. 

  8. 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개