정해진 기간동안 많은 기능을 구현하기 위해서 음원의 메타데이터는 지니 차트100에서 크롤링해왔다. bs4와 request를 사용해서 섬네일 이미지, 음원 제목, 아티스트를 가져왔으며 음악상세페이지로 들어가는 url을 for loop을 사용해서 들어가 가사, 앨범이미지를 가져왔다.
모델링은 크게 song과 user로 나뉘었다. 유저가 곡을 생산해 공유하는 사운드클라우드의 취지상 유저는 곡의 소비자이자 생산자가 된다. 그래서 user와 song은 one to many 관계에 있다. 한명의 유자가 여러곡을 가질 수 있고, 한개의 곡은 한명의 유저에게만 속하기 때문이다.
음원사이트의 특성상 song, playlist, album이 복잡한 관계를 가진다. 우선 song과 playlist는 many to many관계에 있고, song과 album은 one to many 관계에 있다.
user가 특정 song에 like를 누르고 각각의 유저는 하나의 likelist를 가진다. playlist와 album도 마찬가지이다.
그래서 song_likes, playlist_likes, album_likes와 같이 user와 song의 중간테이블을 만들어 like의 경우를 따로 관리해준다. 중간 like테이블을 통해서 각각의 playlist, song, album은 몇개의 like를 가졌는지 count할 수 있게 되고, user들로 부터 많은 like를 가진 playlist, song, album들은 메인화면에 뿌려질 수 있다.
사운드클라우드에서 가장 의아했던 모델은 repost인데 다른 유저가 만든 playlist를 내 피드에 공유할 수 있는 기능이다. 이는 어떻게보면 playlist_like와 비슷한 기능이지만(내 피드에 들어가서 내가like한 playlist를 볼 수 있다는 점에서) 사운드클라우드에서는 따로 있는 기능으로 두었다.
사운드클라우드 만의 특별한 댓글기능도 있었다.
위와같이 음악을 듣다가 댓글을 달면 해당 음악의 위치에 유저가 단 댓그리 표시된다는 점이다. 그래서 comment테이블에는 song_id와 position(위치)가 같이 저장되어야 한다.
가장 복잡하다고 느꼈던 것은 tag이다. tag는 유저가 song, playlist, album을 등록할 때 생성할 수도 있고, 원래있는 tag를 사용할 수 도 있다. 그래서 모든 tag들을 모아놓은 tag테이블을 두고, song_tag, playlist_tag, album_tag의 중간테이블을 전부 따로두었다. 그리고 song의 경우에는 등록할 때 장르를 입력하게 되어있는데 장르와 tag를 따로불류하지 않고 tag테이블에 is_genre라는 컬럼을 두어 장르를 구분하였다. 그리고 사이트의 홈에 is_genre = True인 song들을 카테고리화 하여 뿌릴 수 있게 처리했다.
- Sign up, Sign in
- Google Social Sign up
- Mesage
- Follow
- Notification
- status bar
- User Recommendation
- Comment
많은 앤드포인트를 만들었고, 기억에남는 기능과 코드를 리뷰 해 보겠다.
try:
validate_email(data['email'])
[0] user = User.objects.create(
email = data['email'],
password = bcrypt.hashpw(data['password'].encode('utf-8'), bcrypt.gensalt()).decode('utf-8'),
age = data['age'],
name = data['name'],
gender = data['gender'],
[1] uuid = str(uuid.uuid3(uuid.NAMESPACE_DNS, data['email']).hex)
)
token = jwt.encode({'user_id' : User.objects.get(email = data['email']).id}, SECRET_KEY, algorithm = ALGORITHM)
[2] return JsonResponse({'token' : token.decode(), 'uuid' : user.uuid}, status = 200)
[0] 생성한 User객체를 변수에 담아주어 추후에 사용 가능하도록 한다.
[1] 유저가 account를 생성할 때 마다 고유 uuid를 저장한다.
[2] 유저마다 고유 uuid를 저장하고 프로필에 접근 할 때 endpoint뒤에 path parameter로서 넣어준다.
토큰을 통해 제한된 서비스에 대한 접근권한을 주지만 프로필은 유저 고유의영역이기 때문에 uuid와 같은 고유한 값으로 유저를 식별 해 보안을 더 강화시키도록 한다.
class StatusView(View):
@login_required
def get(self, request):
user = request.user.id
[1] follow_status = (
Follow.objects
.filter(to_follow_id = user, is_checked = False)
.select_related('from_follow', 'to_follow')
.order_by('created_at')
)
[2] if not len(list(follow_status)):
return JsonResponse({'message' : 'EMPTY_UPDATES'}, status = 400)
return_key = {
'data' :
[{'follower_name' : status.from_follow.name,
'follower_id' : status.from_follow.id,
'follower_follower_count' : status.from_follow.follow_reverse.all().count(),
'follower_song_count' : status.from_follow.song_set.all().count(),
'follower_image' : status.from_follow.profile_image,
'follow_at' : status.created_at,
'is_checked' : status.is_checked,
[3] 'mutual_follow' : True if Follow.objects.filter(from_follow_id = user, to_follow_id = status.from_follow_id) else False}
for status in follow_status]
}
Follow.objects.filter(to_follow_id = user).update(is_checked = True)
return JsonResponse(return_key, status=200)
[1] 우선 토큰을 가지고 있는 유저가 팔로우를 당한 사람, 체크되어있지 않은상태의 팔로우를 동시에 만족하는 팔로우 객체들을 전부 가져온다.
[2] 만약 업데이트 상태가 없다면(필터를 통해 걸러져 가져온 쿼리셋이 없다면) empty update 메세지를 리턴한다.
[3] 상대방이 나를 팔로우 했는데 나또한 상대방을 팔로우 하고 있는 상태라면 True를, 아니면 False를 리턴한다.
mutual_fallow가 False이면 내가 상대방을 팔로우하는 요청을 보낼 수 있는 'Follow back'이 활성화되고, 맞팔 상태이면 'Following'이라는 메세지가 활성화 되어있다.
if Follow.objects.filter(to_follow_id = data['to_follow_id']).exists():
Follow.objects.filter(to_follow_id = data['to_follow_id']).delete()
return JsonResponse({'message' : 'UNFOLLOWED'}, status = 200)
그리고 'Following'을 한번 더 클릭하면 unfollow상태가 되기 때문에 Follow 엔드포인트로 post요청이 들어가게 되고 이미 팔로우한 상태이면 해당 팔로우객체를 삭제해서 언팔로우 상태로 만든다.
파이썬의 딕셔너리 자료형은 key와 value로 이루어져 있고 key를 해시화 해서 메모리상 주소로 가지고 value를 key와 연결하여 저장시킨다. 해시화된 key는 중복된 값을 가질 수 없기 때문에 같은 key가 메모리상 들어오면 이후에 들어온 딕셔너리 객체로 대체된다.
이 딕셔너리 자료형의 특징을 이용하여 진행중인 사운드클라우드 클론 프로젝트에서 나의 대화기록을 불러 올 때 Message객체의 중복값을 제거해보자.
우선 메세지 객체를 전부 불러오자
>>> message_chunk = Message.objects.filter(Q(from_user_id = 1)|Q(to_user_id = 1)).order_by('created_at')
>>> message_chunk
<QuerySet [<Message: Message object (1)>, <Message: Message object (2)>, <Message: Message object (3)>, <Message: Message object (4)>, <Message: Message object (5)>, <Message: Message object (6)>, <Message: Message object (7)>, <Message: Message object (8)>, <Message: Message object (23)>, <Message: Message object (24)>, <Message: Message object (25)>, <Message: Message object (26)>, <Message: Message object (27)>, <Message: Message object (28)>, <Message: Message object (29)>, <Message: Message object (30)>, <Message: Message object (31)>, <Message: Message object (32)>, <Message: Message object (33)>, <Message: Message object (34)>, '...(remaining elements truncated)...']>
내가 id가 1번인 유저라고 가정했을 때, 내가 발신자가 된 메세지(from_user_id=1), 내가 수신자가 된 메세지(to_user_id)의 객체를 Q|Q를 사용하여 전부 가져온다.
하지만 여기서 필요한 것은 그림과같이 내가 한 유저와 주고받은 메세지의 chunk가 하나의 객체가 되는 것이다. 즉, 나와 대화한 상대방을 한 메세지 chunk로 하고 그것을 리스트화 해야한다. 그래서 내가 대화한 상대방과의 메세지를 수신인기준으로 중복을 제거 해 줄 필요가 있다.
여기서 중복제거를 하기전에 일단 필요한 value들을 가져오자.
>>> datas=[{'to_user_id' : message.to_user_id, 'from_user_id' : message.from_user_id, 'last_message' : message.content, 'last_message_time' : message.created_at} for message in message_chunk]
>>> datas
[{'to_user_id': 3, 'from_user_id': 1, 'last_message': 'hi', 'last_message_time': datetime.datetime(2020, 3, 15, 8, 51, 24, 133097, tzinfo=<UTC>)}, {'to_user_id': 3, 'from_user_id': 1, 'last_message': 'hi', 'last_message_time': datetime.datetime(2020, 3, 15, 8, 52, 14, 234454, tzinfo=<UTC>)}, {'to_user_id': 3, 'from_user_id': 1, 'last_message': 'hi', 'last_message_time': datetime.datetime(2020, 3, 15, 8, 58, 4, 335098, tzinfo=<UTC>)}, {'to_user_id': 3, 'from_user_id': 1, 'last_message': 'hi', 'last_message_time': datetime.datetime(2020, 3, 15, 9, 3, 14, 720437, tzinfo=<UTC>)}, {'to_user_id': 3, 'from_user_id': 1, 'last_message': 'hi', 'last_message_time': datetime.datetime(2020, 3, 15, 9, 4, 8, 28525, tzinfo=<UTC>)}, {'to_user_id': 3, 'from_user_id': 1, 'last_message': 'hi', 'last_message_time': datetime.datetime(2020, 3, 15, 9, 4, 36, 846351, tzinfo=<UTC>)}, {'to_user_id': 4, 'from_user_id': 1, 'last_message': 'good', 'last_message_time': datetime.datetime(2020, 3, 15, 9, 9, 23, 417197, tzinfo=<UTC>)}, {'to_user_id': 5, 'from_user_id': 1, 'last_message': 'not bad' .............
위와같이 수신인이 1번유저, 발신인이 1유저인 경우의 메세지 객체들을 전부 가져왔다. 이제 to_user_id를 기준으로 to_user_id가 같은 객체는 중복으로 치부하고 중복제거를 해보자.
message_all = list({data['to_user_id'] : data for data in datas}.values())
>>> message_all = {data['to_user_id'] : data for data in datas}
>>> message_all
{3: {'to_user_id': 3, 'from_user_id': 1, 'last_message': None, 'last_message_time': datetime.datetime(2020, 3, 19, 13, 45, 17, 679253, tzinfo=<UTC>)}, 4: {'to_user_id': 4, 'from_user_id': 1, 'last_message': 'good', 'last_message_time': datetime.datetime(2020, 3, 15, 9, 9, 23, 417197, tzinfo=<UTC>)}, 5: {'to_user_id': 5, 'from_user_id': 1, 'last_message': 'oh also this is my new track', 'last_message_time': datetime.datetime(2020, 3, 16, 11, 46, 4, 365650, tzinfo=<UTC>)}, 1: {'to_user_id': 1, 'from_user_id': 117, 'last_message': '???', 'last_message_time': datetime.datetime(2020, 3, 21, 8, 7, 2, 631564, tzinfo=<UTC>)}, 117: {'to_user_id': 117, 'from_user_id': 1, 'last_message': '굿', 'last_message_time': datetime.datetime(2020, 3, 19, 18, 22, 41, 451767, tzinfo=<UTC>)}, 17: {'to_user_id': 17, 'from_user_id': 1, 'last_message': 'what the?', 'last_message_time': datetime.datetime(2020, 3, 16, 14, 15, 15, 676776, tzinfo=<UTC>)}, 29: {'to_user_id': 29, 'from_user_id': 1, 'last_message': 'how are you', 'last_message_time': datetime.datetime(2020, 3, 19, 13, 56, 34, 160223, tzinfo=<UTC>)}}
message_all을 values()없이 dictionary그자체로 찍어보면, 위와같이 values가 key가 되고 dictionary값 자체가 values가 되었다.
기능이 작동하는 것 뿐만 아닌 코드를 줄이고, 로직을 간단하게 만들기 위해서 노력했고, 복잡한 로직에 대해 어떻게 접근을 해야할지에 대한 자신감이 생겼다는 점에서 1차 프로젝트보다 성장했다고 느꼈다.
이번에도 가장 크게 느낀것은 프론트앤드와의 협업이다. 협업의 차원을 넘어서서 프론트앤드가 api호출을 통해 받은 데이터를 어떻게 처리하는지 어느정도 알고 있어야 기능이 작동하는 큰 그림을 이해 할 수 있다고 생각했고 프론트앤드와 웹의 전반적인 지식을 더 키워야 겠다.