Coding History/Team Project

팀플) REST API 별자리 데이터 구체화중 (api 호출 효율적으로 설계)

BlackBirdIT 2024. 10. 13. 03:56

이건 전의 포스트를 봤다면 대충은 구현해 놓았다는 것을 알 것이다.

우선 UTC 시간을 현지시각으로 변환하는 것이 필요했었으니 별자리를 만들 땐 몰랐던 사실이 있다.

유저가 시간 정보를 입력해 요청하면 그 시간 또한 UTC로 변환해야한다는 사실이..

그렇다면 api요청이 호출에 대해서 명확히 할 필요가 있다. 왜냐면 안그러면 돈이 나가니까..

별자리 데이터 요청은 기간을 설정할 수 있다.

만약에 내 로직에서 offset 데이터에 대한 것을 명확히 해주지 않으면 api 호출이 요청한 기간의 모든 날에 다 이루어질 가능성이 있다.

난 돈을 아껴야된다. (사실 아끼는게 아니고 여기서는 돈이 나가게 두면 안된다)

그러니 이걸 명확히 하고 테스트 하자.

우선 현재의 구조상 루트 (/api/constellations 엔드포인트)에서는 요청한 날짜 범위 내에서 매일 get_constellation_for_date() 함수를 호출하고 있다. 따라서 타임존 정보가 반복적으로 필요할 경우 타임존 API가 여러 번 호출될 가능성이 있다.

그래서 루트를 고쳐야한다.

우선 기존의 루트를 보자면


@main.route('/api/constellations', methods=['GET'])
def get_constellations():
    """
    사용자가 요청한 위도, 경도, 날짜 범위에 따라 별자리 정보를 반환하는 API 엔드포인트
    """
    # 쿼리 파라미터로부터 위도, 경도, 날짜 범위 정보 받기
    latitude = request.args.get('lat')
    longitude = request.args.get('lon')
    start_date_str = request.args.get('start_date')
    end_date_str = request.args.get('end_date')
    hour = request.args.get('hour', default=0, type=int)
    minute = request.args.get('minute', default=0, type=int)

    # 위도와 경도 입력 여부 확인
    if not latitude or not longitude:
        return jsonify({"error": "Latitude and Longitude are required"}), 400

    # 위도와 경도 값을 실수(float)로 변환
    try:
        latitude = float(latitude)
        longitude = float(longitude)
    except ValueError:
        return jsonify({"error": "Invalid Latitude or Longitude format"}), 400

    # 날짜 유효성 검사 및 변환
    try:
        start_date = datetime.strptime(start_date_str, '%Y-%m-%d') if start_date_str else datetime.now()
        end_date = datetime.strptime(end_date_str, '%Y-%m-%d') if end_date_str else start_date + timedelta(days=90)
    except ValueError:
        return jsonify({"error": "Invalid date format. Use YYYY-MM-DD."}), 400

    # 날짜 범위 검증
    if start_date > end_date or (end_date - start_date).days > 365:
        return jsonify({"error": "Invalid date range."}), 400

    # 결과를 저장할 리스트 초기화
    constellation_data = []
    current_date = start_date
    # 날짜 범위를 하루씩 증가시키면서 별자리 정보 계산
    while current_date <= end_date:
        try:
            # 캐시된 결과를 사용해 별자리 계산
            constellation = get_constellation_for_date(latitude, longitude, current_date.year, current_date.month,
                                                       current_date.day, hour, minute)
            # 결과를 리스트에 추가
            constellation_data.append({
                "date": current_date.strftime('%Y-%m-%d'),
                "time": f"{hour:02d}:{minute:02d}",
                "constellations": constellation
            })
        except Exception as e:
            # 별자리 계산 실패 시 에러 메시지 반환
            return jsonify({"error": f"Failed to calculate constellation: {str(e)}"}), 500

        # 날짜를 하루 증가
        current_date += timedelta(days=1)

    # 최종 결과를 JSON으로 반환
    return jsonify({
        "location": {"latitude": latitude, "longitude": longitude},
        "start_date": start_date.strftime('%Y-%m-%d'),
        "end_date": end_date.strftime('%Y-%m-%d'),
        "constellations": constellation_data
    })

이렇다.

여기서 조건을 조금더 빡빡하게 걸어주자. 음.. 예를 들면 첫 번째 날짜에 대해서만 타임존 오프셋을 계산하고 나머지 날짜들은 그 오프셋을 재사용하는 방식으로 수정해야될 것 같다.

    # 첫 번째 날짜에 대해 타임존 정보 가져오기
    try:
        timezone_info = get_timezone_info(latitude, longitude, int(start_date.timestamp()))
        offset_sec = timezone_info['rawOffset'] + timezone_info.get('dstOffset', 0)
    except Exception as e:
        return jsonify({"error": f"Failed to get time zone information: {str(e)}"}), 500

해당 코드를 추가해줬다.

그리고 혹시 모르니까 api 호출시에 로그를 찍도록 했다.

우선 서버를 켰고 로그가 제대로 찍히는지 일몰 일출 요청해보자.

일단 로그는 잘 찍힌다.

그럼 이제 별자리 요청을 3일로 줄여서 api 요청이 얼마나 들어오나 확인해보면 된다.

4번이 들어오네..?

아 utc시간으로 변환하는 것 까지 생각하면 맞긴하다.

음.. 아무래도 일몰 일출 데이터에서도 기간으로 요청할 수 있는 로직이 필요할 것 같다.

기간으로 요청하고, 루트에서 offset을 호출 저장해서 한번만 사용하게 하는 것이다.

우선 한번만 요청하는지 확인하는중에 캐싱도 제대로 작동하는 것을 확인, 로그에 찍히질 않아서 왜 호출을 안하지 생각해보니까 캐싱 로직 때문에 호출을 할 필요가 없어서 하지 않았던 것 같다.

날짜를 26년으로 바꿔서 3일 치 요청

여기 보면 한번만 요청한 것을 확인 할 수 있다.

그리고 일몰 일출 값이 list화 되면서 기존 로직들에 약간의 변경이 필요했다.

전의 로직인 행성 정보 데이터를 예로 들자면,

데이터 가공이 되지 않아서 NoneType이 뜬다.

제대로 가공해주고 받은 결과

// 20241013022949
// http://localhost:5555/api/planet_visibility?planet=Mercury&lat=37.5665&lon=126.9780&date=2024-10-10

{
  "best_time": "낮 시간대 (관측 불가)",
  "date": "2024-10-10",
  "declination": "-8.05°",
  "location": {
    "latitude": 37.5665,
    "longitude": 126.978
  },
  "planet": "Mercury",
  "right_ascension": "13.36h",
  "visible": false
}

우선 별자리 로직에 대한 결과부터 말하겠다.

api 요청은 한번만 받았다.

결과도 정상적으로 뜬다.

어떻게 로직을 짰는지 설명을 하자면,

현재 API의 데이터 흐름.

  1. 일출 및 일몰 정보의 일괄 계산:
    • 사용자가 여러 날짜에 걸쳐 데이터를 요청하면, 해당 날짜 범위에 대한 일출 및 일몰 정보를 한 번의 호출로 모두 가져오도록 처리.
    • calculate_sunrise_sunset_for_range 함수를 사용하여 일출 및 일몰 데이터를 한번에 계산.
  2. 캐싱된 타임존 오프셋 재사용:
    • 첫 번째 날짜에 대한 타임존 정보를 Google Time Zone API로 가져옴.
    • 이렇게 얻은 타임존 오프셋 (offset_sec)을 모든 날짜에 대해 재사용하도록 했음.
    • 따라서 여러 날에 대해 각각 Google Time Zone API를 호출할 필요 없이, 한 번의 호출로 얻은 오프셋을 이용해 다른 날짜의 일출, 일몰 계산에 사용.
  3. 일출 및 일몰 데이터 사용:
    • 가져온 일출 및 일몰 데이터는 별자리 계산 시 필요함.
    • 각각의 날짜에 대해 UTC 시간 변환과 별자리 계산을 반복(반복문사용)하면서 이미 계산된 일출, 일몰 데이터를 사용하기 때문에 API 호출이 중복되지 않음.

이렇게 로직을 설계했다. 여러 날짜를 요청해도 Google Time Zone API에 대한 중복 호출을 방지하고, 요청 성능을 높일 수 있게 최적화된 구조로 구현 했다.

와 이게 비용 문제가 들어가니까 효율적으로 짜려고 노력을 할 수 밖에 없구나 싶다..

지금 사용하는 저 api가 무료긴한데 데이터가 일정량 이상 넘어가면 유로로 전환되면서 200$를 가져가서 6개월치를 한번에 테스트하기엔 좀 무서워서 로직을 아예 이런식으로 돌아가게끔 설계했다.

여튼 별자리데이터 일출 일몰과 엮어서 처리 끝.

이제는 해가 떠있는 시간대인지 아닌지 검증하고 반환하게 만들면 된다.