회사 프로젝트를 서비스하는 중, 특정 시간에 활성 유저 수가 늘었을 때 성능이 나빠지는 것을 확인했습니다.
이를 해결하기 위해 DB 슬로우 쿼리 모니터링, API 부하 테스트(clinic 라이브러리 등 이용)를 진행했지만 별다른 특이점을 발견할 수 없었습니다.
클라이언트가 이미지 리소스를 동시다발적으로 요청하게 될 때 성능 저하(네트워크 대역폭이나 서버 사양 문제가 아닐까 합니다.)가 발생하는 것이 잠정적인 원인이라고 보고 이미지 리소스를 S3를 통해 받아올 수 있도록 조치를 취하기로 했습니다.
운영체제 : macOS Monterey 12.4
IDE : VSCode
프레임워크 : Node.js (express)
프로그래밍 언어 : Javascript
패키지 매니저 : npm
기존에는 Node.js 서버가 클라이언트의 이미지 조회 요청과 업로드 요청 둘 다 처리하도록 되어있었습니다. 이미지 업로드 요청은 하루에 10건 가량 처리되기 때문에 큰 영향을 끼치지 않을 거라 생각하고 클라이언트 → 서버 → S3
와 같은 흐름으로 처리되도록 하고, 이미지 조회 요청은 클라이언트 → S3
와 같은 흐름으로 처리되도록 변경하기로 했습니다.
이제 위의 그림과 같이 요청이 처리될 수 있도록 개발을 해야합니다. 회사 프로젝트의 소스 코드를 공유할 수는 없으므로 따로 실습 프로젝트를 생성해 개발을 하겠습니다. 깃허브 주소
실습 프로젝트를 생성해 간단하게 이미지를 업로드하는 페이지와 API를 작성했습니다.
// index.js
// ... 생략
APP.use('/upload', express.static(__dirname + '/upload'));
// ... 생략
APP.post('/upload', (req, res) => {
let file = req.files.upload;
// 사용자가 업로드한 이미지를 서버 디렉토리에 저장
file.mv(`${__dirname}/upload/${file.name}`, (error) => {
if (error) {
return res.status(500).send(error);
}
// 업로드 경로를 클라이언트에게 반환
res.json({ success: true, path: `/upload/${file.name}` });
});
});
S3를 도입하기 이전의 코드는 서버의 upload 디렉토리에 저장되는 것을 볼 수 있습니다. 또한, upload 디렉토리를 static 디렉토리로 지정해서 접근할 수 있도록 했습니다.
AWS 계정이 이미 없다고 가정하고 시나리오를 작성하겠습니다.
로그인 하게 되면 콘솔 메인 페이지에 접속하게 됩니다.
상단 검색창에 'S3' 검색 후 S3(혹은 Simple Storage Service) 클릭
우측의 버킷 만들기 클릭
버킷 사전 설정
AWS 리전 : 버킷이 서비스 될 지역을 의미합니다.
아시다시피 AWS는 높은 응답 속도를 제공하기 위해 특정 국가 혹은 지역마다 호스팅 서버가 있기 때문에 데이터를 주고 받는 지역과 가까운 리전을 사용하는 것이 좋습니다.
저는 한국 사람이기 때문에 아시아 태평양(서울) 을 선택했습니다.
버킷 이름 : 버킷의 고유한 이름입니다. unique scope가 글로벌이기 때문에 다른 사용자들의 버킷 이름과도 중복이 되면 안됩니다.
저는 kyu0-test로 지정했습니다.
객체 소유권 : 페이지의 설명을 잘 읽으시면 ACL이 뭔지 아시리라 믿습니다. 제 생각엔 웬만한 규모의 팀이 아니라면 ACL을 사용할 일은 없을 것 같습니다.
이 버킷의 퍼블릭 액세스 차단 설정 : 신규 혹은 기존의 버킷 정책, ACL이 이 버킷에 영향을 끼치도록 할 것인가 아닌가에 대한 설정인 것 같습니다.
자세한 설명은 이 링크에 있습니다.
저는 모두 체크 해제 했습니다.
그 외의 설정은 건드리지 않고 버킷을 생성했습니다.
파란색으로 표시되는 버킷 이름을 클릭합니다.
버킷 정보를 보여주는 페이지가 출력됩니다. 메뉴바에서 권한 탭을 클릭합니다.
스크롤을 살짝 내리면 버킷 정책 란이 보입니다. 우측의 편집 버튼을 클릭합니다.
정책란에 하단의 json을 참고해서 수정한 텍스트를 입력한 뒤, 변경사항 저장 버튼을 클릭합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AddPerm",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::버킷 이름을 입력해주세요/*"
}
]
}
서버로부터 우리가 생성한 S3 버킷에 업로드 요청을 보내기 위해서는 별도로 권한이 필요합니다. 이를 위해 IAM 서비스에서 사용자를 생성하도록 하겠습니다.
AWS 콘솔 메인 페이지의 상단 검색창에 'IAM' 검색 후 클릭합니다.
왼쪽 사이드바의 액세스 관리 - 사용자 탭을 클릭합니다.
우측의 사용자 생성 버튼을 클릭합니다.
4. 사용자 이름을 입력한 뒤 다음 버튼을 클릭합니다.
권한 옵션 - 직접 정책 연결, 권한 정책 - AmazonS3FullAccess 항목에 체크해줍니다.
사용자 생성 버튼을 클릭하면 하단의 이미지와 같이 페이지가 출력됩니다. 파란색으로 표시된 사용자 이름을 클릭합니다.
액세스 키 만들기 버튼을 클릭합니다.
사용 사례를 선택합니다. 아무거나 선택해도 상관없습니다. 선택한 사례에 따라서 액세스키가 아닌 다른 방법으로 서비스를 이용할 수 있는 대안을 알려줄 뿐입니다...
다음 단계에서 액세스 키 만들기 버튼을 클릭하면, 액세스 키가 생성됩니다.
해당 키들은 되도록이면 노출이 되면 안되며, 특히 비밀 액세스 키는 생성 직후 한 번만 조회할 수 있습니다.
이제 사전 작업은 끝났습니다... 다시 코드를 작성하러 가봅니다.
npm을 이용해 aws-sdk 패키지를 프로젝트에 설치하겠습니다.
npm install aws-sdk
그 후 모듈을 임포트하고 적절한 위치에 S3 객체를 생성해줍니다. 저는 실습을 위한 프로젝트이므로 간단하게 entry point인 index.js 파일에 작성했습니다. 실제 프로젝트에서는 파일 업로드를 담당하는 JS 파일에 작성하시면 좋을 것 같습니다.
// index.js
import aws from 'aws-sdk';
// ... 생략
const S3 = new aws.S3({
credentials: {
accessKeyId: '', // 사용자를 생성하고 발급받은 액세스 키
secretAccessKey: '' // 사용자를 생성하고 발급받은 비밀 액세스 키
}
});
이렇게 작성하고 나면 node 터미널에 다음과 같은 경고가 출력됩니다.
2025년 8월 8일에 지원이 종료되니 AWS SDK for Javascript v3 로 마이그레이션하라는 안내문입니다.
AWS SDK for Javascript v3에서는 모듈식 패키지를 채택해 애플리케이션의 번들 사이즈를 줄여주고, 타입을 지원하는 등의 개발 편의성을 늘렸다고 합니다. 자세히 알고 싶으신 분들은 링크를 참고하시면 좋을 것 같습니다.
AWS 공식문서를 보면서 작업하도록 하겠습니다.
npm uninstall aws-sdk
npm install @aws/client-s3
기존의 aws-sdk 패키지를 삭제하고 @aws/client-s3 패키지를 설치해줍니다.
// index.js
// import 'aws' from 'aws-sdk'; 대체
import { S3Client } from '@aws/client-s3';
// ... 생략
const S3 = new S3Client({
region: 'ap-northeast-2', // 버킷의 aws 리전
credentials: {
accessKeyId: '', // 사용자 생성 시 발급받은 액세스 키
secretAccessKey: '' // 사용자 생성 시 발급받은 비밀 액세스 키
}
});
위와 같이 S3 객체를 생성해줍니다. 이 다음은 사용자가 업로드한 이미지 파일을 S3 버킷에 업로드하는 코드를 작성하겠습니다.
// index.js
import { S3Client, PutObjectCommand } from '@aws/client-s3';
// ... 생략
APP.post('/upload', async (req, res) => {
let file = req.files.upload;
const command = new PutObjectCommand({
Bucket: 'kyu0-test',
Key: 'public/image/sample.jpg', // 경로+파일명
Body: file.data
});
try {
const response = await S3.send(command);
if (response.$metadata.httpStatusCode == 200) {
res.send({success: true, path: getSavedPath('public/image/sample.jpg')});
}
} catch (error) {
console.error(error);
res.status(500).send({ success: false, data: error});
}
});
// S3에 업로드된 이미지 파일의 URL을 구해주는 함수
function getSavedPath(filePath) {
const bucketname = 'kyu0-test';
const region = 'ap-northeast-2';
return `https://${bucketname}.s3.${region}.amazonaws.com/${filePath}`;
}
// ... 생략
업로드할 파일이 특정 디렉토리의 하위에 위치하도록 하고 싶다면 PutObjectCommand
의 Key의 값을 디렉토리/파일명
으로 지정해주시면 됩니다. 주의할 점은 /디렉토리/파일명
과 같이 지정하게 되면 버킷에는 /
, 디렉토리
와 같이 두 개의 디렉토리가 생성된다는 점입니다.
따라서, 의도하시는 게 아니라면 맨 앞의 /
문자는 빼주셔야 됩니다.
S3 버킷에 업로드된 파일에 접근할 수 있는 URL은 아래와 같은 특정한 규칙에 따라 생성됩니다.
https://{버킷이름}.s3.{리전}.amazonaws.com/{파일경로}
이 규칙을 활용하면 간단하게 파일에 접근할 수 있는 URL을 클라이언트 혹은 DB에 제공할 수 있습니다. (S3와 통신해서 얻는 방법은 못 찾았습니다 ㅠ 아시는 분은 댓글 달아주시면 감사드리겠습니다 ...)
본문과 같이 AWS S3와 직접 통신하여 업로드/다운로드를 할 수 있지만 트래픽이 많을 경우 비용이 많이 청구될 수 있습니다. 이 때 AWS CloudFront라는 서비스를 이용하시면 살짝이나마 더 저렴한 과금 기준, 캐싱 도입 등을 이용하셔서 비용을 더 절감하실 수 있습니다!
이 게시글을 작성하게 된 계기는 국내 블로그 중 Node.js로 S3와 통신하는 예제가 대부분 javascript v2로 되어 있어서... 혹시나 다른 분들께 도움이 되면 좋겠다는 마음이 들어서입니다.
이 글을 보신 분들께 도움이 됐길 바라며 글을 마치겠습니다. 긴 글 봐주셔서 감사합니다.
잘못된 내용이나 오타 지적 언제나 환영입니다.
저도 비슷한 상황인데 도움이 많이 됐습니다ㅎㅎ 블로그 글들이 다 좋네요