
구현 웹사이트 : 스페이스클라우드
프론트엔드 : 심택준, 이수정, 이현준, 최호정 (4명)
백엔드 : 임종성, 장이주 (2명)
링크 : 유튜브, 깃허브
백엔드 구현 기능 목록
사용기술 : Python, Django, Mysql, AWS EC2, RDS, S3,Docker, POSTMAN
- 카카오 API를 이용한 소셜 로그인 API
- 공간목록 필터링 및 정렬 API
- AWS S3 를 통한 이미지 호스팅 관리
- 공간등록 API
- 공간상세 API
- 예약 API
- Unit Test / 성능 최적화 / Docker를 이용한 배포 과정
모델링
기능
1. 카카오 API를 이용한 소셜 로그인 API

▶️ 전체적인 흐름
카카오 로그인 API를 이용한 소셜로그인 기능은 크게 세가지의 단계로 이루어진다.
- 인증코드 받기
- 인증코드로 토큰 받기
- 받은 토큰으로 사용자정보 받기
보통 프론트엔드에서 1번과 2번을 수행한 후 카카오 서버로부터 받은 토큰을 백엔드 서버로 보내고, 백엔드에서 그 토큰으로 카카오로부터 사용자 정보를 받아온다.
위의 모든 과정은 백엔드에서 처리할 수 있다. 하지만 이렇게 할 경우엔 가끔 발생하는 오류가 있어서 적당히 프론트와 분업 후 하는 것이 안정적이라는 조언을 얻었다.
▶️ CODE
1
access_token = request.headers.get('Authorization')
- 프론트엔드에서 요청메세지의 헤더에 카카오로부터 받은 토큰을 담아 백엔드 서버로 보내준다.
1
2
3
url = 'https://kapi.kakao.com/v2/user/me'
response = requests.get(url, headers={'Authorization': f'Bearer {access_token}'})
user_response = response.json()
- 프론트로부터 받은 헤더를 카카오서버로 보내 카카오서버에 저장된 사용자의 정보를 받아온다.
1
2
3
4
5
6
7
8
9
10
11
user,created = User.objects.get_or_create(kakao_id=user_response['id'],nickname = user_response['properties']['nickname'])
if created:
user.email = user_response['kakao_account']['email']
user.nickname = user_response['properties']['nickname']
user.point = random.randrange(0,10000)
user.save()
token = jwt.encode({'id':user.id}, SECRET_KEY, algorithm= 'HS256')
return JsonResponse({'access_token':token}, status=200)
- DB에 저장된 사용자의 정보와 비교해 이미 정보가 저장된(이미 회원가입을 한) 사용자라면 토큰을 생성 후 프론트엔드로 반환, 정보가 없는 사용자라면 회원가입을 진행한 후(DB에 사용자 정보를 저장) 토큰을 생성해 프론트엔드로 반환한다.
2-1. 공간목록 필터링 및 정렬 API

▶️ 딕셔너리를 이용해 공간 정렬하기
1
2
3
4
5
prefix = {
"desc" : "-day_price",
"aesc" : "day_price",
"best" : "-like"
}
▶️ Q객체를 이용해서 조건에 맞는 공간 필터링하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
q = Q()
if district_id:
q &= Q(district_id = district_id)
if category_id:
q &= Q(category_id = category_id)
if man_count:
q &= Q(min_count__lte = man_count) & Q(max_count__gte = man_count)
if date:
q_exclude = Q(order__status_id=OrderStatus.Status.COMPLETED.value) & Q(order__date=date)
q_exclude &= ((Q(order__option__option="all") | Q(count_option = 2)))
q &= ~q_exclude
조건에 맞는 공간만 필터링 후 반환한다.
- 지역 (district)
- 공간유형 (category)
- 인원수 (man_count) 최소인원은 2명으로 고정, 사용자가 입력한 인원수를 최대인원으로 설정한다.
- 예약날짜 (date)
q_exclude
- 지정한 날짜에 있는 예약 중에서 결제가 완료된 (Status.COMPLETED) 주문을 저장한다.
- 종일예약을 한 주문(option=”all”)과 오전, 오후 모두 예약한 주문(count_option = 2)을 저장한다.
1
2
3
spaces = Space.objects.prefetch_related("image_set").select_related('district')\
.annotate(day_price=Min('option__price'), count_option = Count('order', distinct=True))\
.filter(q).order_by(prefix.get(order, "-like"))
반환할 공간 객체를 space
에 저장한다.
- 공간 이미지와 지역에 대한 정보는 미리 가져와서 캐싱해두어 DB를 hit 횟수를 줄인다.
- 공간 가격을 최소 가격으로 지정하고 해당 공간의 주문 횟수를 중복없이 카운팅한다.
q
에 저장한 조건을 이용해 필터링한다.- 좋아요 수에 대해 오름차순으로 정렬한다.
2-2. 공간에 대한 지역과 카테고리 정보
프론트단에서 지역
과 카테고리
, 편의시설
에 대한 아이콘과 텍스트를 하드코딩하지 않도록 서버에서 정보를 보내주기로 결정했다.
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
class DistrictView(View):
def get(self, request):
result = [{
"id" : district.id,
"name" : district.name,
"lattitude" : district.lattitude,
"longitude" : district.longitude
} for district in District.objects.all()]
return JsonResponse({'RESULT':result}, status=200)
class CategoryView(View):
def get(self, request):
result = [{
"id" : category.id,
"name" : category.name,
"image" : category.image
} for category in Category.objects.all()]
return JsonResponse({'RESULT':result}, status=200)
class FacilityView(View):
def get(self, request):
results = [{
'id' : facility.id,
'name' : facility.name,
'image' : facility.image
}for facility in Facility.objects.all()]
return JsonResponse({'results':results}, status=200)
3. AWS S3 를 통한 이미지 호스팅 관리
3-1. uuid를 사용해 파일명 중복 방지하기
1
signs = [{'key':'image/' + str(uuid.uuid4()) + image.name, 'image' : image} for image in images]
AWS S3에 파일명이 중복된 채로 업로드되면 새로운 이미지가 기존에 저장된 이미지를 대체한다. 이와 같은 데이터 유실을 막기위해 uuid
를 사용해 경로에 임의의 문자열을 붙여 업로드한다.
경로 : image/’uuid문자열’+’파일명’
3-2. S3 업로드 로직의 모듈화 - boto3
boto3는 AWS SDK(Software Development Kit)로 이를 이용해 S3뿐만 아니라 AWS의 많은 서비스들을 활용할 수 있다.
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
# storage.py
class S3Client():
def __init__(self, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY):
self.s3 = boto3.client(
's3',
aws_access_key_id = AWS_ACCESS_KEY_ID,
aws_secret_access_key = AWS_SECRET_ACCESS_KEY
)
self.base = 'static/'
def __del__(self):
return None
def upload(self, file, file_name, bucket_name):
self.s3.upload_fileobj(
file,
bucket_name,
self.base + file_name,
ExtraArgs = {
'ContentType' : file.content_type
}
)
def delete(self, file_name, bucket_name):
self.s3.delete_object(Bucket=bucket_name, Key = self.base + file_name)
boto3를 이용해 S3에 사진을 호스팅하기 위해 S3Client 클래스
를 생성했다. AWS ACCESS KEY
는 노출되면 안되므로 my_settings 파일에 숨긴다. 메소드로 upload
와 delete
를 정의했다.
1
2
3
4
5
6
7
8
s3_client = S3Client(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY)
Image.objects.bulk_create([
Image(space=space, image = AWS_S3_DIRS + sign['key'])
for sign in signs
])
for sign in signs:
s3_client.upload(sign['image'], sign['key'], AWS_STORAGE_BUCKET_NAME)
로컬 DB에 AWS S3에 저장되는 경로
(AWS_S3_DIRS + sign[‘key’])를 저장한다
S3Client의 메소드 upload
로 이미지파일
(sign[‘image’])를 AWS BUCKET에 지정한 경로
(sign[‘key’])에 올린다.
4. 공간등록 API
4-1. Q객체를 이용해서 조건에 맞는 공간 필터링하기
1
2
3
4
5
6
7
user = request.user
data = request.POST
min_count = request.POST.get('min_count', 2)
like = request.POST.get('like', 0)
images = request.FILES.getlist('image')
facilities = request.POST.get('facility', None).split(',')
signs = [{'key':'image/' + str(uuid.uuid4()) + image.name, 'image' : image} for image in images]
- request에 담아보낸 정보를 변수에 저장한다.
- 모든 데이터는
formData
로 받았으며min_count
와like
는 request에 포함되지 않는 값으로 default 처리를 했다. images
는getlist
를 사용해 복수의 이미지를 저장했다.facilities
는 formdata로 복수의 facility_id를 저장한다. 프론트에서 가공되지 않은 형태로 넘어와 백단에서 split을 이용해 가공했다.
4-2. Transaction
DB의 데이터를 수정하는 로직에선 트랜잭션
을 이용해서 중간에 에러가 발생했을 때 rollback
되도록 한다.
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
with transaction.atomic():
space = Space.objects.create(
user = user,
category_id = data['category'],
district_id = data['district'],
title = data['title'],
sub_title = data['sub_title'],
min_count = min_count,
max_count = data['max_count'],
address = data['address'],
like = like
)
Image.objects.bulk_create([
Image(space=space, image = AWS_S3_DIRS + sign['key'])
for sign in signs
])
Option.objects.bulk_create([
Option(space=space, option="day", price=data['price_day']),
Option(space=space, option='night', price=data['price_night']),
Option(space=space, option='all', price=data['price_all'])
])
[space.facility.add(Facility.objects.get(id=facility))for facility in facilities]
for sign in signs:
s3_client.upload(sign['image'], sign['key'], AWS_STORAGE_BUCKET_NAME)
5. 공간상세 API
▶️ 요청받은 공간에 대한 데이터 가공 후 반환
url parameter로 전달받은 space_id
로 해당 공간 객체의 정보를 가져온 후, 원하는 형태로 가공한다.
space와 foreign key
로 연결된 category
와 district
를 select_related
를 이용해 미리 가져와 쿼리 수를 줄일 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
space = Space.objects.select_related("category", "district").get(id=space_id)
results = [{
'id' : space.id,
'category_id': space.category.id,
'district_id': space.district.id,
'image' : [image.image for image in space.image_set.all()],
'title' : space.title,
'sub_title' : space.sub_title,
'min_count' : space.min_count,
'max_count' : space.max_count,
'address' : space.address,
'like' : space.like,
'price' : [[option.id, option.option, option.price ]for option in space.option_set.all()],
'facility' : [{
'id' : facility.id,
'name' : facility.name,
'image': facility.image
}for facility in space.facility.all()]
}]
6. 예약 API
6-1. 예약가능한 날짜인지 유효성 검사
1
2
date = request.GET.get('date')
option = request.GET.get('option')
쿼리파라미터로 date
와 option
에 대한 정보를 받아온 후 이에 대한 예약정보를 확인한다.
1
2
if not Order.objects.filter(space_id=space_id, date=date, status_id=OrderStatus.Status.COMPLETED.value).exists():
return JsonResponse({'message':'OK'}, status=200)
case1. 해당 날짜에 예약이 아에 없을 경우 ok
1
2
3
4
key = Option.objects.get(id=option).option
if key == 'all' :
return JsonResponse({'message':'DENIED'}, status=400)
case2. 해당 날짜에 예약이 있음 -> 종일(option=all) 예약할 수 없음 Denied
1
2
3
4
orders = Order.objects.filter(space_id=space_id, date=date, status_id=OrderStatus.Status.COMPLETED.value)
if orders.filter(Q(option__option='all') | Q(option__option=key)).exists():
return JsonResponse({'message':'DENIED'}, status=400)
case3. 해당날짜에 종일예약이 있거나, 일치하는 예약이 있으면 Denied
6-2. 예약 API
1
space_id = request.GET.get("space_id", None)
url parameter로 예약하고자 하는 공간의 id를 전달받는다.
1
2
if Order.objects.filter(user_id = request.user.id, status_id = OrderStatus.Status.COMPLETED.value).exists():
Order.objects.filter(user_id = request.user.id, status_id = OrderStatus.Status.COMPLETED.value).delete()
이전에 해당 공간에 대한 예약 정보가 있으면 (이미 결제해서 상태가 COMPLETED
인 예약) 삭제한다.
1
2
3
4
5
6
7
8
9
order = Order.objects.create(
space_id = space_id,
user_id = request.user.id,
status_id = OrderStatus.Status.WAITING.value,
date = data['date'],
count = data['count'],
option_id = data['option'],
booker = None
)
상태를 WAITING
으로 예약을 저장한다.
query debugger로 API 성능 테스트하기
쿼리문을 작성할 때 DB를 얼마나 효율적으로 hit하느냐 . . 가 중요하다. 예를 들어, 외래키 관계의 테이블을 사용할 때 이 데이터와 관련있는 데이터를 불러오기 위해서 매번 DB를 hit하기보다는 미리 외래키 관계의 데이터를 전부 불러와 캐싱해둔 뒤 다시는 DB를 hit할 필요가 없게 하는 방법이 있다. 이젠 기능만 뚝딱 만들고 끝나는 게 아니라 그 기능이 얼마나 효율적으로 굴러가게 할 지 한번 더 생각해야하는 단계로 진입했다. (내가?)
이번엔 프로젝트 중이라서 얕게 이해하고 끝냈지만 언젠가는 데이터베이스에 대한 심도있는 학습을 해야할 필요가 있겠다는 생각이 들었다. (할 게 너무 많아요 ㅠㅠ)
AWS와 Docker 로 프로젝트 배포하기
S3와 씨름하며 그나마 익숙해진 AWS와 완전 새로 접한 개념인 도커
1차 프로젝트 때 하지 못한 배포의 한을 이번에 풀었다. 서버를 켜놓지 않아도 통신할 수 있는 기쁨.. !
현업에서 도커를 사용하는 경우가 많다고 하니 이 또한 심도있게 학습을 해야만 할 것 같다.. ^^
✉️ Review
BLOCKER
일주일 내내 하루에 4시간씩 자며 코드와 대전쟁 . . ! 내가 팀 진도를 못 쫓아가서 민폐 끼치는 상황이 올까봐 하루하루가 두려웠다. 그래서 아침 10시부터 저녁 10시까지 하루종일 코딩하고 집가서 12시부터 새벽3시까지 더 하고 잤다 (ㅠㅠ) 대학생 때 공부를 이렇게 했다면 서울대에 가지 않았을까 싶을 정도로 내 인생에서 역대급으로 열심히 한 일주일이었다. 조급한 감정 자체는 잘 컨트롤하면 엄청난 동기부여가 될 수도 있겠지만 나의 경우엔 이 감정때문에 심신이 너무 불안정해지고 몸이 부서지는 기분이었닼ㅋㅋㅋ 중간점검때도 생각했지만 체력이 절대 무시 못할 blocker인 건 평생 안 바뀔 것 같다.
그럼에도 팀 프로젝트이기 때문에 팀원들끼리 이끌어주고 의지하며 어떻게 잘 버틸 수 있었다. 혼자였다면 절대 완주 못했을 레이스였다. 정말 힘들고 지치는 시간들이었지만 팀원들과 함께해서 행복하고 즐거웠던 2주였다. 시간이 지나도 기억에 길이 남을 좋은 추억을 만들 수 있어서 참 행운이었다 !