백엔드 개발 포트폴리오 | 워크숍 중개 사이트
내일배움캠프 4기 웹 개발 과정 Node.js 트랙 수료생 최종 프로젝트 '워커벤치'를 소개합니다.
Aug 16, 2023
- 서비스 둘러보기 (PC 환경)
유저 및 강사 페이지
(접속 불가능 할 시 www.msdou46-94.shop 으로 접속)
ID : user3@test.com
PW : 12345
관리자 페이지
(접속 불가능 할 시 www.msdou46-94.shop/admin/login 으로 접속)
ID: admin3@test.com
PW: 12345
📌 페이지 이용 가이드
① 일반 유저 가이드 :
회원 가입 → 수강하고 싶은 워크샵 클릭 → 문의하기 → 마이페이지 이동
→ 수강내역 확인 (결제/환불)
❗결제 시 테스트 결제로 100원 결제 된 후, 당일 자정 전에 모든 결제 내역이 환불됩니다.
② 강사 유저 가이드 :
회원 가입 → 마이페이지 → 강사로 등록하기 → 업체 등록 or 업체 가입 신청 → 워크샵 등록/관리
❗업체 등록 시 모든 정보는 실제 정보가 아닌 임의로 작성하셔도 됩니다.
③관리자 가이드 :
📌페이지 접속 시 스웨거 페이지 보안으로
ID : msdou
PW: 12345 입력 → 로그인 → 관리자 기능 사용
- Github 주소:
- ERD 구조도: 📑 workerBenchProject-ERD
🛠️ 아키텍처
🔧 기술적 의사 결정
MySQL | 관계형 데이터베이스를 통해 정형화된 데이터를 저장하고, 테이블 간 관계, 제약조건을 설정하여 데이터 중복을 줄이기 위해 사용 |
Redis | 사용자 로그인 시 refresh token의 경우, 영구적으로 필요한 데이터가 아니므로 상대적으로 빠르고 가벼운 인 메모리 상태에서 처리하기 위해 사용 |
S3 | 사이트 트래픽이 증가할 경우 워크샵 이미지와 영상 컨텐츠를 등록 시 서버를 증설할 필요 없이 저장 용량을 늘릴 수 있어 사용 |
CloudFront | Edge Location에 캐싱 데이터를 업로드하여 일정한 속도로 정적 파일(워크샵 이미지 및 영상)을 불러올 수 있고 HTTPS로 보안성을 높이기 위해 사용 |
Lambda | 백엔드에서 이미지 리사이징을 처리하는 경우, 가상 메모리를 많이 소비하게 되므로 서버리스 방식을 사용 |
Elemental MediaConvert | 워크샵 등록 시 업로드 되는 영상의 해상도를 여러 가지로 컨버팅하여, 다양한 네트워크 환경에서도 안정적으로 영상이 재생될 수 있도록 사용 |
🛠 트러블 슈팅
이미지 리사이징 시 발생하는 서버 부담 및 S3 CreateObject 이벤트 핸들링에 대한 문제
- 문제 상황: 이미지 리사이징(sharp 패키지 사용) 을 서버에서 실행할 시, 이미지 파일 사이즈에 따라 가상 메모리를 많이 소모하여 서버 응답 속도가 저하될 것이 우려됨. 추가로 S3 PutObject 이벤트에 대한 핸들링이 다소 빈약할 수 있다 판단됨.
const s3OptionForThumbImg = { Bucket: this.AWS_S3_BUCKET_NAME_IMAGE_INPUT,. Key: `images/workshops/${workshop.identifiers[0].id}/original/${thumbImgName}`, Body: image.buffer, }; await this.s3Client.send(new PutObjectCommand(s3OptionForThumbImg)); // 800 픽셀로 리사이징된 이미지를 Put const resizedImage = await sharp(image.buffer).resize(800).toBuffer(); await this.s3Client.send( new PutObjectCommand({ Bucket: this.AWS_S3_BUCKET_NAME_IMAGE_INPUT, Key: `images/workshops/${workshop.identifiers[0].id}/800/${thumbImgName}`, Body: resizedImage, }), );
기존의 경우, 이미지 리사이징 작업을 node.js 백엔드에서 시행하고 있음. 사용자가 업로드 하는 이미지의 개수나 용량이 적을 경우 큰 문제는 없으나, 이미지의 용량이 늘어날 경우 서버 컴퓨터의 가상 메모리 사용량이 증가하고 업로드 응답 속도가 저하되는 상황이 발생함.
→ 현재 약 4MB 의 이미지를 네 장 업로드 및 리사이징 하는데에 걸리는 시간은 455ms
추가로, 현재 로직에서는 원본 이미지 파일을 버킷에 저장한 후 이에 대응하는 가로 800 픽셀 사이즈의 이미지를 추가로 함께 저장하고 있는데, 확실성을 위해서 원본 이미지가 저장된 후 S3 버킷의 어느 경로에 어떤 이름으로 저장 되었는지를 감지한 후 이에 대한 추가 작업으로서 리사이징을 진행하고자 함.
- 해결 방안: S3의 input용 버킷(원본이미지 저장) 에 원본 이미지가 저장될 시 이를 트리거로서 감지하여 AWS Lambda 함수가 발동, 해당 함수를 사용하여 서버리스로 이미지 리사이징을 실행.
- 백엔드에서는 다음과 같이 원본 이미지에 대한 업로드 코드만을 구현.
const s3OptionForThumbImg = { Bucket: this.AWS_S3_BUCKET_NAME_IMAGE_INPUT, Key: `images/workshops/${workshop.identifiers[0].id}/original/${thumbImgName}`, Body: image.buffer, }; await this.s3Client.send(new PutObjectCommand(s3OptionForThumbImg));
- 원본 이미지 저장용 input 버킷에 이미지가 업로드 됨을 트리거로 하여, 리사이징 람다 함수 발동.
- 람다 함수는 내부적으로 input 버킷의 Create Object 이벤트를 감지하여 리사이징 코드를 실행.
exports.handler = async (event) => { const Bucket = event.Records[0].s3.bucket.name; // 업로드된 버킷명 const Key = event.Records[0].s3.object.key; // 업로드된 키명 const s3objOption = { Bucket, Key }; /* AWS 내부적으로 이벤트를 감지하여 Lambda 함수를 실행할 경우, 발생한 이벤트에 대한 정보를 조회하고 핸들링 하기가 용이함 */ if (Key.includes("video")) { return { statusCode: 400, message: "Video is not edited" }; } const workshop_id = Key.split("/")[Key.split("/").length - 3]; // workshop id const imageCategory = Key.split("/")[1]; // workshops or reviews const filename = Key.split("/")[Key.split("/").length - 1]; // 파일명 try { // put 된 이미지 객체 불러오기 const S3ImageObject = await s3Client.getObject(s3objOption).promise(); // 리사이징 const resizedImage = await sharp(S3ImageObject.Body).resize(800).toBuffer(); // 리사이징된 객체 넣기 await s3Client .putObject({ Bucket: "workerbench-msdou-image-output", Key: `images/${imageCategory}/${workshop_id}/800/${filename}`, Body: resizedImage, }) .promise(); .....
- S3 output용 버킷에 리사이징된 이미지가 저장되었음을 확인 * 버킷을 굳이 input, output 으로 나눈 이유는 람다 함수의 무한 이벤트 재귀 호출을 방지하고자.
이미지 리사이징 작업을 백엔드에서 AWS Lambda 서버리스로 옮긴 뒤 이미지 업로드 속도 개선
→ 위와 동일한 조건에서 455ms → 246mm 로 단축. 이미지 용량이 커짐에 따라 더욱 격차가 벌어질 것으로 생각됨.
위의 개선을 통해서 AWS Lambda 함수의 장단점에 대해서 알아볼 수 있었음.
- 장점
- 서버리스로 실행되는 코드이기에 백엔드 서비스에 대한 코드를 별도의 관리 없이 사용 가능.
- AWS S3 의 CreateObject 이벤트를 트리거로 하여 발동하는 등, 다른 AWS 서비스들과 함께 연계하여 사용하기 용이하다
- 특정 트리거가 실행될때만 코드를 실행하고 싶은 경우 사용하기 편리하다
- 단점
- 람다는 메모리(최대 10GB), 처리시간 (최대 900초) 에 제한이 있다. 따라서 스펙의 측면에서는 직접 서버에서 돌리는 것보다는 성능이 부족할 수 있다.
- 따라서 람다 함수는 동시에 실행될 수 있는 횟수에 제한이 있다. 이 경우 동시성에 있어서 장애가 발생할 확률이 높아진다. 그러므로 Lambda 의 메모리를 늘려 스펙을 높이거나, AWS SQS(simple queue service) 를 활용, 보안 호스팅 대기열을 제공하여 동시성을 관리하여 해결할 수 있다.
- 이번 프로젝트의 경우 워크샵 등록은 계정을 ‘강사용 계정’ 으로 등록한 일부에 한해서 그리 많지 않은 횟수로 수행되는 이벤트 이기에, Lambda 함수를 활용하는 데에 있어서 가용 메모리나 동시성 적인 측면에서 큰 문제는 없을 것으로 예상되어 무리 없이 이미지 리사이징에 Lambda 함수를 도입하였다.
영상 시청 시 클라이언트 네트워크 상태에 따른 화질 조정
- 문제 상황: 유저가 접속하는 네트워크 환경이 원활하지 못할 경우 영상을 로딩하는 데 시간이 지체되어 영상을 재생할 수 없는 문제가 발생.
워크샵 상세보기에서 Cloud Front 로 영상을 불러 오더라도 로딩 시간이 지속되어 시청이 불가능 한 경우가 발생. 추가로 보다 더 쾌적한 영상 시청을 위해서 배속 조절, 해상도 조절 등의 기능이 추가적으로 필요함을 느끼게 됨.
- 해결 방안: 영상 파일을 S3에 업로드 시 AWS MedaiConvert 서비스를 사용하여 지정한 해상도 별로 영상 정보를 저장. 프론트로 보여줄 때에는 HLS(http live streaming) 스트리밍 프로토콜을 활용.
- 영상 업로드 시 파일을 input 용 S3 버킷에 저장.
try { // 랜덤한 이름 생성 const videoTypeName = video.originalname.substring( video.originalname.lastIndexOf('.'), video.originalname.length, ); const videoName = uuid() + videoTypeName; // s3 에 입력할 옵션 const s3OptionForReviewVideo = { Bucket: this.configService.get('AWS_S3_BUCKET_NAME_VIDEO_INPUT'), Key: `videos/workshops/${workshopData.workshop_id}/original/${videoName}`, Body: video.buffer, }; // 실제로 s3 버킷에 업로드 await this.s3Client.send(new PutObjectCommand(s3OptionForReviewVideo));
- input 용 버킷에 CreateObject 이벤트 발생 시 이를 트리거로 삼아 람다 함수 실행.
- 람다 함수는 AWS MediaConvert 서비스의 job create(작업 생성) 기능을 사용하여 영상을 HLS 스트리밍 프로토콜에 적합한 .m3u8 파일로 컨버팅 하여 output 용 S3 버킷에 저장
const AWS = require("aws-sdk"); const s3Client = new AWS.S3(); // region const aws_s3_region = "ap-northeast-2"; // s3 setup AWS.config.update({ region: aws_s3_region }); // media convert endpoint AWS.config.mediaconvert = { endpoint: "<AWS Mediaconvert 엔드포인트를 입력>", }; // create the client // const mediaConvert = new AWS.MediaConvert({ apiVersion: "2017-08-29" }); // output bucket const AWS_S3_BUCKET_NAME_VIDEO_OUTPUT = "workerbench-msdou-video-output"; // S3 와 API 게이트웨이에 접속하기 위한 권한. const media_convert_role = "arn:aws:iam::XXXXXXXXXXXX:role/role_mediaconvert"; exports.handler = async (event) => { const Bucket = event.Records[0].s3.bucket.name; // 업로드된 버킷명 const Key = event.Records[0].s3.object.key; // 업로드된 키명 (따라서 경로를 포함) const workshopFolderName = Key.split("/")[0]; // videos const videoCategory = Key.split("/")[1]; // workshops or reviews const content_id = Key.split("/")[Key.split("/").length - 3]; // workshop id or review id const filename = Key.split("/")[Key.split("/").length - 1]; // 파일명 // input 경로 const s3_video_input_source = `s3://${Bucket}/${Key}`; //output 경로 const s3_video_output_source = `s3://${AWS_S3_BUCKET_NAME_VIDEO_OUTPUT}/${workshopFolderName}/${videoCategory}/${content_id}/hls/`; try { // job 생성 ------------------------------------------ const job = await new AWS.MediaConvert({ apiVersion: "2017-08-29", }) .createJob({ Role: media_convert_role, Settings: { TimecodeConfig: { Source: "ZEROBASED", }, OutputGroups: [ { .....
→ 람다 함수를 활용할 경우 다른 AWS 서비스들과 연동하기 편리하며, AWS 서비스의 계정 내부에서 함수가 동작하기에 별도로 S3 에 권한 제약이 설정되어 있어도 크게 영향을 받지 않는다.
추가로 job create 설정을 바꿔야 할 필요성이 발생해도 백엔드 서버에서 추가적인 수정을 행할 필요가 없어 편리하다.
- output 용 S3 버킷에 컨버팅되어 저장된 결과는 다음과 같다.
- 추가로, 람다 함수에서 사용한 new AWS.MediaConvert().createJob() 안에 입력된 mediaConvert 옵션의 경우, 실제로 AWS MediaConvert 작업 템플릿을 만든 후 그 위에서 작업 생성을 하여 도출된 JSON 결과 코드를 기반으로 작성하였음.
→ 작업 템플릿 제작
→ 템플릿에서 job(작업)을 수동으로 생성하여 도출된 성공 결과값의 JSON 코드를 참조.
- .m3u8 영상은 AWS Cloud Front 를 통해서 URL 이 배포되고, 이를 프론트에서는 hls.js 를 통해 보여준다.
<!-- hls.js css, js import --> <link rel="stylesheet" href="https://cdn.plyr.io/3.7.2/plyr.css"> <script src="https://cdn.plyr.io/3.7.2/plyr.js"></script> <script src="//cdn.jsdelivr.net/npm/hls.js@latest"></script> ...
→ .m3u8 에 저장된 해상도 별 정보를 읽어들여 프론트에서 해상도를 직접 지정할 수 있도록 옵션 추가.
Share article
Subscribe to our newsletter