뮤다가 궁금하시다면 여기를 클릭
뮤다에서는 특정 유저에게 1회 추천한 곡들은 1달의 기간동안 다시 추천해주지 않는데요, 이런 필터링 로직을 어떻게 구현하였는지 간단하게 소개해보고자 합니다.
알고 싶진 않았지만 QA를 하다 보니 필요한 기능이라고 생각했어요

일기의 내용을 분석하고 노래를 추천해주는 모델이 있는 서버로 클라이언트가 요청을 보내는 구조에서 크게 다음과 같이 2가지 방안을 떠올렸는데요, 내용은 다음과 같습니다.
기본적으로 모델 서버에 요청 시 유저가 선택한 일기 내의 감정이나 장르 등의 컨텍스트만을 Payload에 포함하였었는데요, 아래와 같이 요청 전달 시 그동안 추천 받았던 곡들의 index 번호를 포함하고 모델 서버측에서 바라보는 음악 Database에서는 다음 곡들을 제외하는 방식이었습니다.
{
data: '일기 텍스트'
// ... 중략
user_song_ids: ['36906004', '24154991', '33233247' ]
}
(2) 모델 서버에서 필터를 걸어버리는 방법
유저가 그동안 추천받았던 곡의 정보들을 저장하는 DB를 생성하고, 해당 DB에서 그간 유저가 추천 받았던 곡을 가져와 음악이 담긴 DB에서 추천 받았던 곡들은 제외하는 방식입니다.
결론적으로 이런 작업을 (2)번 로직으로 진행하기로 했는데요, 모델 서버에서 처리하는 것으로 선정한 것에는 다양한 이유가 있었지만 저희가 사용하게 될 유저가 추천 받았던 곡들의 정보를 저장하는 DB를 DynamoDB로 채택하면서 response 시간의 SLA에 영향을 미치지 않게 되어 모델 서버에서 해당 작업을 진행하는 것으로 결정하였습니다.
마감기한이 굉장히 짧았고, QA 진행 중 새로 생긴 요구 사항이라 간단하게 해야할 일을 다음과 같이 생각해보았습니다.
앞서서도 언급했지만 유저가 추천받았던 곡의 정보를 저장하는 DB로는 DynamoDB를 선택했습니다. DynamoDB의 경우 굉장히 빠르고, DynamoDB에서의 파티션 키와 정렬 키를 잘 조합하면 저희의 요구사항에 굉장히 적합하다고 생각했기 때문입니다!
기본적으로 DynamoDB의 경우 파티션 키와 정렬 키라는 용어를 사용하는데요, DynamoDB에서는 파티션 키가 AWS 내부의 물리적인 저장소를 지정하는 기준이 되기 때문에 빠른 처리를 위한다면 이 파티션 키로 Unique한 값을 지정해주어 Cardinality를 높여주는 것이 중요합니다. 다만 이 파티션 키는 중복된 값을 담을 수 없기 때문에 정렬 키를 이용해서 유저의 고유 아이디 당 여러개의 기 추천곡을 담을 수 있도록 하였습니다. (DynamoDB에서는 동일한 파티션 키에 정렬 키만 달리한다면 여러 item을 담을 수 있습니다)
그리하여 UserId (유저의 고유한 아이디)를 파티션 키로, 추천을 받은 날짜를 정렬 키로 설정하였습니다.

앞전에 언급했듯 한달이 지난 후에는 기존에 추천 받았던 곡도 다시 추천이 가능합니다. 다만 한달이라는 기간이 정확히 시각까지 고려할 필요는 없다고 전달을 받아 %Y-%m-%d 형식으로 날짜를 지정하려고 했습니다.
정렬 키를 %Y-%m-%d 형식으로 설정하게 될 경우, 하나의 유저가 같은 날에 여러번 추천을 받을 경우 기존에 저장되어있던 Item에 업데이트가 되게 됩니다.
따라서 덮어쓰기가 되지 않도록 정렬 키를 날짜로 지정한다면 %Y-%m-%dT%H:%M:%S 처럼 초까지 지정을 해주는 것이 안전합니다.
필터링에는 크게 두가지 함수가 필요했습니다. 첫 번째로는 서버가 요청을 받았을 때 해당 요청에서 UserId를 파싱한 뒤 기존에 추천 받은 곡을 확인하는 함수이고 두 번째로는 모델이 추천해준 곡을 다시 DB에 쌓아주는 함수가 필요합니다.
DynamoDB에서는 파티션 키와 정렬 키를 이용해서 쿼리를 할 수 있는데요, 쿼리를 이용하여 다음과 같이 함수를 작성해주었습니다.
import boto3
from boto3.dynamodb.conditions import Key
from datetime import datetime, timedelta
import pandas as pd
import json
def filtering(userId):
# recommended table
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('테이블이름')
# get userId from payload
# userId = json_data.get('userId')
## for test - dummy user name
end_date = datetime.now()
start_date = end_date - timedelta(days=30)
print(end_date, start_date)
# parsing date time as sort key includes
start_date_str = start_date.strftime('%Y-%m-%dT%H:%M:%S')
end_date_str = end_date.strftime('%Y-%m-%dT%H:%M:%S')
print(start_date_str, end_date_str)
try:
response = table.query(
KeyConditionExpression=boto3.dynamodb.conditions.Key('userId').eq(userId) &
boto3.dynamodb.conditions.Key('date').between(start_date_str, end_date_str)
)
song_ids = [item['songIds'] for item in response['Items'] if 'songIds' in item]
filtering_songids = []
for item in song_ids:
for songid in item:
filtering_songids.append(songid)
return filtering_songids
except Exception as e:
print(f"{e}")
def insert_recommended_songId(userId, songIds):
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('테이블 이름')
date = datetime.now()
# parsing date time as sort key includes
date_parse = date.strftime('%Y-%m-%dT%H:%M:%S')
print(date_parse)
try:
response = table.put_item(
Item={
'userId': userId,
'songIds': set(songIds),
'date': date_parse
}
)
print("songIds saved:", response)
except Exception as e:
print("Error log:", e)
먼저 payload에서 UserId를 파싱하여 해당 UserId를 기준으로 filtering()함수를 이용하여 테이블에 쿼리를 합니다. 리턴받은 곡들이 있다면 해당 곡들은 음악 DB에서 제외한 뒤 추천이 들어가게 됩니다.
추천이 완료되면, UserId를 기준으로 insert_recommended_songId() 함수를 이용하여 추천을 받은 곡들의 시간과 곡의 인덱스들을 테이블에 저장합니다. 날짜 (특히 초 단위까지)를 정렬키로 지정했기 때문에 item이 업데이트 될 일은 없습니다.
특히 DynamoDB에서는 TTL 기능을 무료로 제공하고 있기 때문에 저장 이후 30일이 지난 item들은 삭제를 할 수 있습니다. 참 좋은 기능이군요. 다만 TTL 기능을 이용하기 위해서는 unix timestamp 값을 가진 attribute가 필요합니다.
글 읽어주셔서 감사합니다(_ _)