처음에는 LangChain을 활용해 에이전트 방식으로 구현하려 했는데, 생각처럼 잘되지 않았다. 정확한 데이터를 가져오는 데 어려움이 있었다. 그래서 일단 기본적인 API 호출로 정확한 데이터를 수집하고 리스트를 가져오는 테스트부터 하는 방향으로 전략을 바꿨다.
💡테스트 목표
✔ 여행 계획에 맞춘 맞춤형 맛집 추천을 제공하는 것
✔ 리뷰 수와 평점이 높은 곳만 필터링하는 것
✔ 최대한 정확한 데이터를 가져오는 것
현재 Google Geocoding API를 이용해 사용자가 입력한 지역의 좌표를 가져오고, SerpAPI를 활용해 그 지역에서 평점이 높은 맛집을 자동으로 검색하는 기능을 구현했다.
SerpAPI란?

Google 검색 결과를 API로 제공하는 서비스로, Google Maps, 뉴스, 이미지 등 다양한 검색 데이터를 구조화된 형태로 가져올 수 있다.
공식 사이트: https://serpapi.com/
Google에서 제공하는 API vs SerpAPI에서 제공하는 Google Maps API 차이점
| 구분 | Google Maps API (구글 제공) | SerpAPI의 Google Maps API (SerpAPI 제공) |
| 제공사 | SerpAPI (Google 데이터를 크롤링) | |
| 데이터 출처 | Google의 공식 지도 데이터 | Google 지도 검색 결과 (웹 검색 크롤링) |
| 기능 | 장소 검색, 거리 계산, 지도 렌더링, 길찾기, 좌표 변환 등 | Google 지도에서 나오는 검색 결과(리뷰, 평점, 주소 등)를 크롤링하여 제공 |
| 데이터 정확도 | 최신 공식 데이터 | Google 검색 결과 기반 (실시간성은 높지만 정확도는 다를 수 있음) |
| 요금 정책 | 사용량 기반 과금 (무료 사용량 제한 있음) | API 호출당 과금 (무료 티어 제공, 가격이 상대적으로 비쌈) |
| 사용 방식 | 공식 API를 통해 정식으로 제공 | Google 검색 결과를 크롤링하여 데이터 제공 |
| 제한 사항 | 특정 요청 수 이상 시 요금 부과 | Google의 웹 검색 크롤링이므로 Google의 정책 변경 시 영향 받을 가능성 있음 |
Google에서 제공하는 Google Maps API를 사용해도 장소 데이터를 가져올 수 있다. 하지만 SerpAPI를 활용하면 Google 검색 결과를 크롤링하여 보다 직관적인 맛집 정보를 수집할 수 있었다. 그래서 맛집 데이터를 효율적으로 가져오기 위해 SerpAPI를 사용했다.
테스트 과정
1. API 키 로드 및 환경 설정
.env 파일에서 API 키를 로드하기 위해 dotenv를 사용했다. os.getenv("KEY_NAME")을 통해 환경변수에서 API 키를 불러와 보안성을 유지했다.
import os
from dotenv import load_dotenv
import requests
load_dotenv()
api_key = os.getenv("SERPAPI_API_KEY")
google_maps_api_key = os.getenv("GOOGLE_MAP_API_KEY")
2. 위도, 경도 검색 (Google Geocoding API)
SerpAPI에서 제공하는 Google Maps API를 사용하려고 했는데 자꾸 필수 파라미터 입력 오류가 발생했다. 공식 문서를 다시 살펴보니 필수 입력값이 몇 가지 더 필요했으며, 그중에서도 ll(위도, 경도) 파라미터를 빠뜨렸다는걸 알게 되었다.
처음에는 DB에 미리 행정구역의 우도와 경도를 저장할지 고민했지만, 구글에서 위도, 경도를 제공하는 API를 지원한다는걸 확인했다. 그래서 Google Geocoding API를 활용해 실시간으로 좌표를 가져오는 방식을 선택했다.
"search_parameters":
{
"engine": "google_maps",
"type": "search",
"q": "Coffee",
"ll": "@40.7455096,-74.0083012,14z",
"google_domain": "google.com",
"hl": "en"
},
처음에는 Google Geocoding API의 사용 예시 코드를 참고하여 구현했다. 하지만 계속 Google Maps API에서 필수 파라미터 입력 오류가 발생했다. 이유를 고민하다가 "혹시 @40.7455096,-74.0083012,14z 형식으로 값을 전달하지 않아서 그런가?"하는 생각이 들었다. 그래서 응답 받은 위도와 경도 값을 formatted_coordinates = f"@{latitude},{longitude},14z 형식으로 변환호도록 수정했더니 오류가 해결되었다.
def get_coordinates(location, google_maps_api_key):
"""
Gets latitude and longitude for a given location using Google Geocoding API.
"""
url = "https://maps.googleapis.com/maps/api/geocode/json"
params = {"address": location, "key": google_maps_api_key}
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
if data["results"]:
location_data = data["results"][0]["geometry"]["location"]
latitude = location_data["lat"]
longitude = location_data["lng"]
formatted_coordinates = f"@{latitude},{longitude},14z"
print(f"Extracted coordinates: {formatted_coordinates}")
return formatted_coordinates
else:
raise Exception("No results found for the location.")
else:
raise Exception(f"Error: {response.status_code}, {response.text}")
3. 맛집 검색(SerpAPI의 Google Maps API)
test_plan["main_location"] 값을 받아 해당 지역의 좌표를 가져오고 SerpAPI를 이용해 "지역 + 맛집" 검색어로 Google Maps 데이터를 조회한다.
def search_restaurants(test_plan, api_key, google_maps_api_key):
"""
Searches for restaurants using SerpAPI's Google Maps API.
"""
location = test_plan["main_location"]
coordinates = get_coordinates(location, google_maps_api_key)
url = "https://serpapi.com/search"
params = {
"engine": "google_maps",
"q": f"{location} 맛집",
"ll": coordinates,
"hl": "ko",
"gl": "kr",
"api_key": api_key,
"start": 0,
}
4. 검색 결과 필터링 (평점 + 리뷰 개수)
맛집의 기준을 고민하다가 평점 4점 이상이고 리뷰 개수가 500개 이상인 곳만 필터링하도록 설정했다. 또한, 테스트 과정에서 중복된 식당이 자주 포함되는 문제가 발생해서, 이를 해결하기 위해 place_id를 사용하여 중복된 맛집을 제거하도록 수정했다. 그리고 데이터를 더 많이 가져오기 위해 for start in [0, 20]을 사용하여 한번에 최대 20개씩 2번 반복하여 데이터를 수집하도록 했다. (SerpAPI에서는 한 번의 요청당 최대 20개의 데이터를 반환한다.)
- 첫 번째 호출 → start = 0 → 첫 번째 페이지(최대 20개) 데이터를 가져옴
- 두 번째 호출 → start = 20 → 두 번째 페이지(21~40번째 결과)를 가져옴
all_restaurants = []
seen_place_ids = set() # 중복 제거를 위해 place_id 사용
for start in [0, 20]: # Fetch two pages of results (20 each)
params["start"] = start
response = requests.get(url, params=params)
if response.status_code == 200:
data = response.json()
results = data.get("local_results", [])
for result in results:
name = result.get("title", "No name")
address = result.get("address", "No address")
rating = result.get("rating", 0)
reviews = result.get("reviews", 0)
place_id = result.get("place_id", "")
# Filter: 평점 4 이상 & 리뷰 500개 이상
if rating >= 4 and reviews >= 500 and place_id not in seen_place_ids:
all_restaurants.append(
{
"name": name,
"address": address,
"rating": rating,
"reviews": reviews,
"place_id": place_id,
}
)
seen_place_ids.add(place_id)
5. 메인 실행 로직
test_plan에 테스트로 사용할 입력 데이터를 넣고 필터링된 맛집 리스트를 출력하도록 했다. 실패했을 경우를 고려하여 예외처리도 작성했다.
if __name__ == "__main__":
test_plan = {
"id": 1,
"name": "부산 여행",
"main_location": "부산광역시",
"concepts": ["가족", "맛집"],
"start_date": "2025-01-26T11:00:00",
"end_date": "2025-01-27T16:00:00",
}
try:
restaurants = search_restaurants(test_plan, api_key, google_maps_api_key)
for idx, restaurant in enumerate(restaurants, start=1):
print(
f"{idx}. {restaurant['name']} - {restaurant['address']} "
f"(Rating: {restaurant['rating']}, Reviews: {restaurant['reviews']})"
)
except Exception as e:
print(f"An error occurred: {e}")
6. 실행 결과
40개의 데이터 중 평점과 리뷰 개수 조건을 적용하여 필터링된 결과다. 다만, 현재 기준이 적절한지 고민 중이다.
Extracted coordinates: @35.1731121,129.0714122,14z
1. 신발원 - 대한민국 부산광역시 동구 대영로243번길 62 (Rating: 4.1, Reviews: 3101)
2. 냉수탕가든 - 대한민국 부산광역시 부산진구 가야공원로 107 (Rating: 4.2, Reviews: 2107)
3. 금수복국 해운대본점 - 대한민국 부산광역시 해운대구 중동1로43번길 23 (Rating: 4.2, Reviews: 7253)
4. 영진돼지국밥 본점 - 대한민국 부산광역시 사하구 하신번영로157번길 39 (Rating: 4.5, Reviews: 2528)
5. 60년전통 할매국밥 - 대한민국 부산광역시 동구 중앙대로533번길 4 (Rating: 4.2, Reviews: 2463)
6. 이재모피자 본점 - 대한민국 부산광역시 중구 광복중앙로 31 (Rating: 4.4, Reviews: 3948)
7. 본전돼지국밥 - 대한민국 부산광역시 동구 중앙대로214번길 3-8 (Rating: 4.1, Reviews: 4489)
8. 마가만두 - 대한민국 부산광역시 동구 대영로243번길 56 (Rating: 4.1, Reviews: 1328)
9. 톤쇼우 광안점 - 대한민국 부산광역시 수영구 광안해변로279번길 13 (Rating: 4.6, Reviews: 841)
10. 평산옥 - 대한민국 부산광역시 동구 초량중로 26 (Rating: 4.1, Reviews: 664)
11. 수변최고돼지국밥 본점 - 대한민국 부산광역시 수영구 광안해변로370번길 9-32 (Rating: 4.4, Reviews: 2052)
12. 해운대 기와집 대구탕 - 대한민국 부산광역시 해운대구 달맞이길104번길 46 (Rating: 4.1, Reviews: 3136)
13. 합천일류돼지국밥 사상점 - 대한민국 부산광역시 사상구 광장로 34 (Rating: 4.1, Reviews: 5001)
14. 배종관 동래삼계탕 - 대한민국 부산광역시 동래구 동래로116번길 39 (Rating: 4.3, Reviews: 1233)
15. 할매집 돼지국밥 해운대본점 - 대한민국 부산광역시 해운대구 중동2로10번길 7 (Rating: 4.2, Reviews: 886)
16. 부산꼼장어맛집 성일집 - 대한민국 부산광역시 중구 대교로 103 (Rating: 4.1, Reviews: 900)
17. 대건명가돼지국밥 - 대한민국 부산광역시 동구 중앙대로 232 (Rating: 4.4, Reviews: 757)
18. 고옥 - 대한민국 부산광역시 수영구 광남로 6 (Rating: 4.3, Reviews: 1022)
19. 풍원장 꼬막정찬 - 대한민국 부산광역시 해운대구 마린시티2로 38 (Rating: 4.2, Reviews: 1630)
20. 자매국밥 - 대한민국 부산광역시 수영구 민락본동로27번길 56 (Rating: 4.2, Reviews: 716)
21. 기장손칼국수 - 대한민국 부산광역시 부산진구 서면로 56 (Rating: 4, Reviews: 1839)
22. 김유순대구뽈찜 - 대한민국 부산광역시 남구 못골로 83-7 (Rating: 4.1, Reviews: 1445)
23. 국이네 낙지볶음 - 대한민국 부산광역시 수영구 연수로 410 (Rating: 4.3, Reviews: 873)
24. 언양불고기 부산집 - 대한민국 부산광역시 수영구 남천바다로 32 (Rating: 4, Reviews: 1245)
25. 나가하마 만게츠 한국본점 - 대한민국 부산광역시 해운대구 우동1로 57 (Rating: 4.4, Reviews: 1061)
26. 삼오정 - 대한민국 부산광역시 부산진구 서면로68번길 11 (Rating: 4.4, Reviews: 821)
27. 미청식당 - 대한민국 부산광역시 기장군 기장해안로 1303 (Rating: 4, Reviews: 569)
28. 할매재첩국 - 대한민국 부산광역시 수영구 광남로120번길 8 (Rating: 4.2, Reviews: 1560)
29. 쌍둥이돼지국밥 대연점 - 대한민국 부산광역시 남구 유엔평화로 16 (Rating: 4.2, Reviews: 557)
30. 우리돼지국밥 - 대한민국 부산광역시 동구 초량로 27-1 (Rating: 4.1, Reviews: 1042)
31. 엄용백 돼지국밥 - 대한민국 부산광역시 수영구 수영로680번길 39 (Rating: 4.1, Reviews: 1001)'Python' 카테고리의 다른 글
| [Python] 동기 vs 비동기, 언제 그리고 어떻게 사용해야 할까? (0) | 2025.02.07 |
|---|---|
| [Python] FastAPI와 React 연동 및 데이터 흐름과 처리 과정 (0) | 2025.01.31 |
| [Python] SQLAlchemy와 SQLModel의 모든 것: 데이터베이스 작업의 핵심 도구 (0) | 2025.01.26 |
| [Python] Pydantic으로 안전하고 효율적인 데이터 검증하기 (0) | 2025.01.25 |
| [Python] async/await로 배우는 비동기 프로그래밍 (0) | 2025.01.11 |
