일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 누적합
- branch 전략
- 코드 계약
- 레디스 동시성
- gRPC
- 깊게 생각해보기
- docker
- 수신자 대상 다르게
- jwt 표준
- 알람 시스템
- piplining
- spring event
- 검색어 추천
- BFS
- 셀러리
- 프로그래머스
- 객체지향패러다임
- 구현
- 완전탐색
- AWS
- 트랜잭샨
- 카카오
- 결제서비스
- 디버깅
- 이분탐색
- 숫자 블록
- 쿠키
- prg 패턴
- 백준
- 좋은 코드 나쁜 코드
- Today
- Total
코딩관계론
검색어 추천 서비스 V1(SQL + RDBMS + index) 본문
버전 1의 서비스 구성은 다음과 같이 설계했습니다. 사용자의 요청이 오면 캐시 서버나 특별한 자료구조 처리를 사용하지 않고, 데이터베이스의 쿼리문을 통해 응답하도록 구성했습니다.
데이터베이스는 단일 테이블로 설계했습니다. 이는 추천 키워드 기능에 필요한 데이터가 '키워드'와 해당 키워드가 검색된 횟수에 대한 정보만으로 충분했기 때문입니다.
개발 과정은 2024.08.09 - [개발] - 검색어 추천 서비스 개발기에서 분석한 내용을 토대로 진행했습니다. 사용자가 입력을 할 때(1바이트가 추가되면 검색으로 간주), 해당 단어로 검색된 키워드 중 상위 10개를 반환하는 방식으로 구현했습니다.
스프링(Spring)과 JPA를 사용하여 개발했으며, JPA 구현체로는 Hibernate를 선택했습니다. 아래는 JPQL 예제입니다:
public static final String GET_SUGGESTION_KEYWORD = "SELECT r FROM Ranking as r WHERE r.keyword LIKE :target ORDER BY r.searchCount DESC LIMIT 10";
예를 들어, 데이터베이스에 다음과 같은 5개의 키워드가 있다고 가정해 봅시다. 사용자가 "삼"이라는 단어를 검색 중이라면, 서버는 "삼성전자", "삼양식품", "삼지전자"를 반환해야 합니다.
아래 응답에서 예상한 결과를 얻을 수 있음을 확인할 수 있습니다. 이로써 기능적 요구사항은 만족했지만, 성능에 대해서도 검토할 필요가 있습니다.
{
"total": 3,
"keywords": [
{
"keyword": "삼성전자"
},
{
"keyword": "삼양식품"
},
{
"keyword": "삼지전자"
}
]
}
이전 글에서 언급한 것처럼, 성능은 100ms 이내로 유지되어야 합니다. 이를 위해 성능 테스트를 진행하기 전에 성능 예측을 수행했습니다.
현재 5개의 데이터에 대해 6ms 이내의 응답 속도를 확인했습니다. 성능 예측을 위해, 데이터 표본을 8,500개로 증가시킨 후 응답 속도를 측정한 결과, 50ms로 변경되었습니다. 데이터가 약 1,700배 증가했을 때, 응답 속도는 약 8배 느려지는 경향을 보였습니다.
만약 데이터를 17,000개로 증가시켰을 때, 응답 속도는 얼마나 느려질지 예측할 수 있을까요?
- 데이터 수: 5개 → 응답 속도: 6ms
- 데이터 수: 8,500개 → 응답 속도: 50ms
이 비례 관계를 이용하여 예측된 성능은 약 100ms입니다. 실제 결과도 예상과 일치하게 약 100ms로 나타났습니다. 따라서, 데이터가 추가되었을 때의 성능을 테스트 없이도 예측할 수 있게 되었습니다.
그러나 이러한 결과를 바탕으로 성능 개선이 필요하다고 판단됩니다.
쿼리 실행 계획 분석: 성능 최적화의 첫걸음
쿼리 성능을 개선하기 위해서는 먼저 실행 계획을 분석하는 것이 중요합니다. PostgreSQL에서 쿼리 실행 계획을 확인하려면, 쿼리 앞에 EXPLAIN ANALYZE 명령어를 추가하면 됩니다. 예시는 다음과 같습니다:
EXPLAIN ANALYZE
SELECT *
FROM ranking as r
WHERE r.keyword LIKE 'a%'
ORDER BY r.search_count DESC
LIMIT 10;
이 명령을 실행하면 쿼리가 어떻게 처리되는지, 데이터베이스가 어떤 전략을 사용하는지 확인할 수 있습니다.
실행 계획 결과 분석
아래는 위 쿼리를 실행했을 때의 결과입니다. 여기서 주목해야 할 점은 두 명의 워커(worker)가 Seq Scan을 사용하여 모든 데이터를 하나씩 확인하고 있다는 것입니다.
또한 여기서 중요한 부분 중 하나는 Rows Removed by Filter입니다. 이는 테이블에서 조회했지만 조건에 맞지 않아 제거된 행(row)의 수를 나타냅니다. 이 수가 적을수록 쿼리가 더 효율적으로 최적화되었다고 할 수 있습니다.
Limit (cost=13798.51..13808.31 rows=84 width=27) (actual time=75.180..77.152 rows=100 loops=1)
-> Gather Merge (cost=13798.51..13808.31 rows=84 width=27) (actual time=75.172..77.140 rows=100 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Sort (cost=12798.48..12798.59 rows=42 width=27) (actual time=66.441..66.444 rows=77 loops=3)
Sort Key: search_count DESC
Sort Method: top-N heapsort Memory: 38kB
Worker 0: Sort Method: top-N heapsort Memory: 38kB
Worker 1: Sort Method: top-N heapsort Memory: 38kB
-> Parallel Seq Scan on ranking r (cost=0.00..12797.35 rows=42 width=27) (actual time=1.190..65.165 rows=6411 loops=3)
Filter: ((keyword)::text ~~ 'a%'::text)
Rows Removed by Filter: 332619
Planning Time: 0.791 ms
Execution Time: 77.289 ms
인덱스를 통한 성능 최적화
구현하려는 기능은 접두사로 시작하는 검색어를 빠르게 찾아내는 것입니다. 이 경우, 인덱스를 사용하면 쿼리 성능을 크게 향상시킬 수 있습니다. PostgreSQL에서는 인덱스 생성 방식으로 B-tree와 GIN(Generalized Inverted Index) 등 여러 옵션이 존재하지만, 이 시나리오에서는 B-tree 인덱스가 더 적합합니다.
B-tree vs GIN 인덱스: 왜 B-tree를 선택했는가?
- B-tree 인덱스는 기본적으로 데이터가 순서대로 저장되며, 특히 접두사 기반 검색에 적합합니다. 즉, 'a%'와 같이 특정 문자열로 시작하는 값을 찾을 때 B-tree 인덱스는 효율적으로 동작합니다. 이 방식은 인덱스에서 데이터의 정렬이 유지되므로, 순차적으로 검색 범위를 좁혀나갈 수 있습니다.
- GIN 인덱스는 텍스트의 일부분(부분 문자열, 단어 등)을 찾을 때 주로 사용됩니다. GIN은 주로 문서 검색이나 JSON 필드를 검색하는 데 유리하며, 전체 문자열 내의 단어 또는 부분 문자열을 검색할 때 더 효과적입니다. 하지만 우리가 다루는 쿼리는 접두사 검색에만 집중하고 있으므로 GIN 인덱스가 제공하는 이점은 과도한 비용이 될 수 있습니다.
B-tree 인덱스 생성 예시
B-tree 인덱스를 사용하여 성능을 최적화할 수 있습니다. 다만, 서버의 로케일 설정이 C 로케일이 아닌 경우, text_pattern_ops를 사용하여 인덱스를 생성해야 합니다. 로케일 설정에 따라, 인덱스를 생성했더라도 Seq Scan(순차 스캔)이 실행될 수 있기 때문입니다.
CREATE INDEX idx_keywords_index ON ranking (keyword text_pattern_ops);
이 인덱스를 생성한 후, 쿼리를 다시 실행해보면 Bitmap Index Scan을 사용하여 성능이 크게 향상된 것을 확인할 수 있습니다.
Limit (cost=9921.07..9921.09 rows=10 width=27) (actual time=9.776..9.796 rows=10 loops=1)
-> Sort (cost=9921.07..10023.81 rows=41095 width=27) (actual time=9.769..9.771 rows=10 loops=1)
Sort Key: search_count DESC
Sort Method: top-N heapsort Memory: 26kB
-> Bitmap Heap Scan on ranking r (cost=1029.51..9033.02 rows=41095 width=27) (actual time=3.126..8.316 rows=19233 loops=1)
Filter: ((keyword)::text ~~ 'a%'::text)
Heap Blocks: exact=291
-> Bitmap Index Scan on idx_keywords_index (cost=0.00..1019.24 rows=40281 width=0) (actual time=3.069..3.070 rows=19233 loops=1)
Index Cond: (((keyword)::text ~>=~ 'a'::text) AND ((keyword)::text ~<~ 'b'::text))
Planning Time: 1.291 ms
Execution Time: 10.873 ms
성능 향상의 한계와 추가 고려사항
비록 성능이 약 87% 향상되었지만, 여전히 개선이 필요한 이유는 두 가지입니다:
- 쿼리 빈도: 검색어 추천 기능은 매우 자주 사용되기 때문에 쿼리가 빈번하게 실행됩니다 따라서 데이터베이스의 커넥션을 획득하는 작업에서 병목현상이 발생할 수 있습니다.
- 데이터 증가: 검색어 데이터는 계속해서 증가하기 때문에 현재의 성능 최적화가 시간이 지나면서 점차 한계를 보일 수 있습니다.
이를 시즌2에서 다시 한번 고민해보겠습니다
'개발' 카테고리의 다른 글
검색어 추천 기능 V2(Trie + RAM) (0) | 2024.08.11 |
---|---|
검색어 추천 서비스 개발기 (1) | 2024.08.11 |
내부 서버들은 어떤 통신 프로토콜 좋을까..(gRPC를 선택하게 된 이유) (0) | 2024.08.07 |
API Gateway를 직접 구현해야 할까? (0) | 2024.08.05 |
Redis가 제안한 분산락(feat.Redlock) (0) | 2024.07.31 |