취업코스를 경험하면서 많은 분들을 만났고, 취업을 위한 것만이 아니라 개발 자체에 즐거움을 느끼고 노력하는 모습에서 큰 자극을 받았다.
내가 학습한 부분에 대해 도움을 드리기도 하고, 내가 도움을 받기도 하면서 함께 성장해 나가는 과정을 경험했고, 최종적으로 예약 상품 MSA 프로젝트까지 수행해 나가면서 많은 성장을 할 수 있었던 것 같다.
프로젝트를 마무리하면서 경험들을 되돌아보고 더 좋은 발전을 위해서 회고록을 작성한다.
1. 프로젝트 목표
특정 시간부터 상품구매 버튼이 오픈되고, 짧은 시간에 많은 주문 트래픽이 발생하는 상황을 가정하여 프로젝트를 설계, 구현해보는 프로젝트를 진행하였다.
나는 크게 3가지의 주제에 대해 고민해보는 것을 목표로 잡고 프로젝트를 진행하였다.(특정 시간대에 대량의 주문 트래픽이 발생하는 경우)
- 어떤 아키텍처 구조를 고려해볼 수 있을까(성능적으로)?
- 주문 요청으로 인한 동시성(데이터 정합성) 문제 해결
- 추가적으로 발생할 수 있는 문제점은 없을까?
실제로 기업환경에서 대규모 트래픽을 처리하기 위해서 클라우드 환경을 이용해 해결하는 경우가 많다고 한다.
이번 프로젝트는 클라우드 환경을 이용하는 것이 아니기 때문에, 로컬 서버에서 개발하며 기능별로 서버를 분산시키는 설계에 초점을 맞춰서 진행하였다.
2. 프로젝트 구조 설계하기
프로젝트 초기의 테이블 설계는 아래와 같다.
2 - 1 테이블 설계의 고민사항
위와 같은 구조를 설계하고 보니 아래와 같은 고민사항이 생겼다.
- product와 reservation_product 테이블은 거의 유사하다. purchase_button_activation_at 필드만 다른데 테이블을 분리할 필요가 있을까? 다른 방법은 없을까?
- orders 테이블에 product_number를 통해서 테이블 관계를 가지고 있다. 하지만 추후에 product의 가격등과 같이 상품정보가 변경되면 orders테이블의 데이터도 변경된 정보를 참조하게 될 것이다. 별도로 주문 시점의 orders_history 테이블이 필요하지 않을까?
위와 같은 고민 결과 최종적으로 설계된 테이블은 아래와 같다.
- reservation_time테이블을 추가하여 상품 예약 시간을 관리한다.(예약 상품, 상시 구매 일반 상품 등을 구별해줄 수 있다)
- orders_history라는 기록 테이블을 통하여 주문 시점에 상품정보들을 정확하게 기록할 수 있다.
3. 서버 분리 학습
실제 주문 트래픽이 발생할 때, 재고수량 테이블에 대해 정합성 문제가 발생할 것이라고 예측된다.
현재 모놀리스 서비스에는 주문, 결제, 상품, 재고등등 여러가지 관심사의 API들이 모두 들어가 있었고, 서버 분리를 고려한 이유는 아래와 같다.
- 관심사에 맞게 서버를 분리하여 유지보수성을 높인다.
- 주문, 재고수량의 경우 트래픽이 몰리는 경우가 생길 수 있기 때문에 확장에 용이하도록 서버를 분리 시킨다.
- 특정 시간대에 트래픽이 몰리는 경우에, 일반 상품기능을 이용하는 고객들의 성능등에 대해 영향을 미치지 않도록 한다.
현재 서버를 분리시켜본 경험이 없었기 때문에 간단한 뉴피스드 프로젝트를 진행해보며 분산 서버 환경에 대해 학습하는 시간을 가졌다.
학습 링크 : https://topaz-raincoat-203.notion.site/65cae744cebd44cbabc3771e222e3737?pvs=4
3 - 1 서비스 요구사항 우선순위 고려
서비스를 구현함에 있어서 각각의 요구사항이 있었고, 아래와같이 요구사항에 대한 우선순위를 고민하여 결정했다.
기준을 정하기
- 장애대응 측면 : 발생했을 경우 가장 장애 대응 측면에서 어렵거나 위험한 경우에 대해 우선순위를 결정한다.
- 사용자 편의성 측면 : 사용자 경험 측면에서 가장 불편한 경우에 대해 우선순위를 정한다.
💡 중요도로 표시한다(최상, 상, 중, 하)
장애대응 측면 | 사용자 편의성 측면 | |
어픈 시간 이전 구매 가능(어뷰징) | 최상 | 상(경우에 따라 매우 중요할 수 있음) |
서비스 불가 | 최상 | 최상 |
상품 페이지 재고 수가 정확하지 않음 | 중 | 중 |
[최종 결제 수 > 전체 상품 수] 발생으로 인한 결제 취소 | 상 | 상 |
결제 화면 도입 이후 결제 시도 불가 | 상 | 최상(현재 나의 서비스에서는 결제서비스에 도입 된 경우 결제 시도가 보장되어야 함) |
1순위. 오픈 시간 이전 구매 가능(어뷰징) - (예약 구매라는 개념에 치명적)
- 장애대응 측면 : 예약 시스템의 핵심인 예약기능 자체가 문제가 있기 때문에 반드시 보장 되어야 한다.
- 사용자 편의성 측면 : 예약 시스템의 핵심인 예약이 무시되어버리기 때문에 중요하다.
2순위. 서비스 불가(어뷰징이 보장 되었다는 가정하에 원활한 서비스가 제일 우선시 되어야 함) - (어뷰징이 보장된다는 가정하에 가장 1순위로 대비되어야 함)
- 장애대응 측면 : 시스템 자체가 마비이므로 치명적 장애이다.
- 사용자 편의성 측면 : 서비스 이용 불가 시 유저 경험측면도 가장 안좋을 것으로 예상.
3순위. [최종 결제 수 > 전체 상품 수] 발생으로 인한 결제 취소 - 결제 이후에 취소는건 사용자
- 장애대응 측면 : 시스템 내부 처리속도에 따라 우선순위가 밀릴 수 있고, 상품 수 보다는 절대 결제 수가 많도록 해서는 안된다.
- 사용자 편의성 측면 : 결제가 되었다는 알림 이후에, 취소가 된다면 불편한 경험이 될 수 있음.
4순위. 결제 화면 도입 이후 결제 시도 불가
- 장애대응 측면 : 현재 나의 프로젝트는 결제 화면 도입 이후로는 우선순위(예약 우선순위)가 반드시 보장 되도록 구현해야한다.
- 사용자 편의성 측면 : 사용자 입장에서 그럴 수 도 있지라고 생각할 수 있지만, 그래도 안좋은 느낌을 줄 수 있음.
5순위. 상품 페이지 재고 수가 정확하지 않음
- 장애대응 측면 : 현재 나의 프로젝트는 결제 화면 도입 이후로는 우선순위(예약 우선순위)가 반드시 보장 되도록 구현해야한다.
- 사용자 편의성 측면 : 사용자 입장에서 재고수가 정확한지 아닌지를 체감하기 힘들것으로 예상. ex) 재고가 있다고 해서 주문을 했지만 재고 부족이라고 떠도 사용자가 페이지를 접속하고 클릭하는 과정에서 늦었구나라는 느낌을 받을 수 있음. 그럼에도 가능한 가장 빠르게 최신 재고 정보를 페이지에 출력해주는게 좋을 것임. → 그래야 내가 빨리 들어와서 재고를 확인했고 주문에 성공했다!라는 성공절차를 밟은 느낌을 줄 수 있을 것 같다.
예상되는 우려점
- 한명의 사용자가 동시에 굉장히 많은 주문 요청을 보내는 경우
- 예약 상품의 주문 발생 시, 예약 상품 구매를 하지 않는 일반 사용자들의 조회 성능에도 영향을 미칠 수 있다.
- 분산 환경에서의 응답 시간 지연 문제(redis를 활용하더라도 DB에 동시에 업데이트 해주는 전략을 사용하면 DB만 사용하는 방식과 비슷한 성능을 낼 것으로 예상된다)
- 무거운 작업은 비동기 처리로 변환해야함(비동기로 변환했을 때 유의미한 성능 이점이 있는지 꼭 확인해봐야 함)
- 만약 비동기 처리를 한다면 트랜잭션을 어떻게 할 것인가(에러 처리)
최종적으로 분리한 아키텍처와 시나리오는 아래와 같다.
4. 시나리오 테스트
4 - 1 동시성 문제 고민
전체 구조에 대해 테스트를 수행해보고 재고 정합성 문제와 성능을 측정해보자.
시나리오 테스트 파일
# http_request_tool.py
import time
import requests
import random
import threading
from concurrent.futures import ThreadPoolExecutor
# 한명이 수행하는 시나리오를 순서대로 작성한다.(결제 프로세스)
def sinario(user_id):
# 예약 상품 남은 수량 조회 (완료) -> GET 예약상품 재고 수 조회 v1/reservation-products/{reservation_id}/stock
product_id = "43"
url = "http://localhost:8083/v1/stocks/products/" + product_id
try:
response = requests.get(url)
print(f"재고 수량 - {response.json()['data']['stockCount']}", end=" ")
print(f"사용자 : {user_id}")
# print(f"Request to {url} completed with status code {response.json()['data']['stockCount']}")
except Exception as e:
print(f"Error sending request to {url}: {e}")
return
# 결제화면 들어가기 버튼 클릭 (완료) -> POST 주문 생성 요청 : v1/orders
url = "http://localhost:8083/v1/orders"
response = None
try:
body = {
"productId" : 43,
"productType" : "reservationProduct",
"quantity" : 1,
"memberId" : user_id,
"address" : "서울"
}
response = requests.post(url, json=body)
print(f"주문(결제화면들어가기) {response.json()['desc']}, 누가? {user_id}")
# print(f"Request to {url} completed with status code {response.json()['desc']}")
except Exception as e:
# print("error")
print(f"Error sending request to {url}: {e}")
return
# # 20%는 고객 변심 이탈 (완료) -> DELETE 주문 취소 요청 : v1/orders
random_number = random.randint(0, 99)
order_id = response.json()['data']
if random_number < 20:
url = "http://localhost:8083/v1/orders/" + str(order_id)
try:
response = requests.delete(url)
print(f"고객 변심 이탈 : {response.json()['desc']}")
# print(f"Request to {url} completed with status code {response.json()['desc']}")
except Exception as e:
print("error")
# print(f"Error sending request to {url}: {e}")
print("고객 변심 이탈 재고 + 1")
return
# 결제 버튼 클릭(결제 요청 API) (완료) -> POST 결제하기 요청 : v1/payments
url = "http://localhost:8083/v1/payments"
try:
body = {
"orderId" : order_id,
"memberId" : user_id
}
response = requests.post(url, json=body)
print(f"결제 {response.json()['desc']}")
# print(f"Request to {url} completed with status code {response.json()['desc']}")
except Exception as e:
print("error")
# print(f"Error sending request to {url}: {e}")
return
def main():
# 1만명의 사람들이 결제 시나오리를 수행한다.
num_requests = 10000
with ThreadPoolExecutor(max_workers=100) as executor:
tasks = [executor.submit(sinario, user_id) for user_id in range(1, num_requests + 1)]
for future in tasks:
future.result()
if __name__ == "__main__":
start_time = time.time()
main()
end_time = time.time()
execution_time = end_time - start_time
print("코드 실행 시간:", execution_time, "초")
테스트 수행결과 재고 수량에서 동시성 문제가 발생하였다.
동시성 문제 트러블 슈팅 링크 : https://topaz-raincoat-203.notion.site/328a1c6abaed46a88444b87c243dd698?pvs=4
기존에 경험했었던 synchronized와 테이블과 더불어 redis의 동시성 처리 방식을 학습해볼 수 있었다.
5. 아쉬운 점 및 추가 학습 방향
짤은 기간에 서버 분리, feign clinet, redis 등에 대하여 학습할 수 있어서 좋았다.
하지만 서버를 분리하면서 레플리카와 db커넥션등을 고려하여 조회 성능을 측정해보기를 기대하였지만, 시간적인 문제로 진행하지는 못하였고 아쉬움이 남았다. 추후 학습에서 분산 서버 환경을 깊게 공부해볼 예정이다.
또한 학습을 위해 feign client를 사용하였지만, 이벤트 기반의 통신방식에 대해서도 비교하여 학습해볼 예정이고, 분산 환경에서의 트랜잭션 처리에 대해 학습해볼 예정이다.
짧은 시간이었던 만큼 필요한 기술들을 빠르게 학습하고 적용시키는 실습을 하는 좋은 경험을 했던 것 같다. 하지만 기술을 선택함에 있어서 분명 여러 선택지들이 존재할 것이기 때문에 항상 비교하고 트레이드오프 비용을 생각하면서 고민해야겠다고 느꼈다.
'프로젝트 > 예약상품' 카테고리의 다른 글
[프로젝트 그 이후...] 선착순 주문 요청 프로젝트 개선하기(보충학습) (0) | 2024.07.22 |
---|---|
[프로젝트 그 이후...] 선착순 주문 요청 프로젝트 개선하기 (0) | 2024.07.13 |
[프로젝트] 회복탄력성(CircuitBreaker와 Retry) (0) | 2024.03.02 |
[프로젝트] 상품 오픈시간에 같은 사용자의 여러 요청을 대비하자 (0) | 2024.02.26 |
[프로젝트] redis로 동시성 문제를 접근해보자 (0) | 2024.02.26 |