개발
백엔드
S3 데이터 가져와서 테이블로 관리하기
목차
S3 서버는 아마존 웹 서비스에서 이용할 수 있는 스토리지 저장 서비스이다.
실제로 대용량의 파일을 저장해도 과금이 별로 안되며, 파일의 접근에 대한 비용과 트래픽 비용만이 요구된다.
물론 이 비용자체는 다른 웹하드에 비하면 합리적인 비용을 가지고있다.
단 아마존은 이런 스토리지 서비스인 S3를 대량 파일관리의 초점을 맞춰서 제공하기때문에 1000개 이상의 파일은 웹에서 검색도 안되고, API 요청으로도 천개 단위로만 파일 메타 데이터등을 가져올 수 있는 여러가지 제약이 존재한다.
이를 해결하기위해서 마찬가지로 AWS에서 제공하는 다른 서비스인 Amazon S3 Inventory 라던지, S3 Select 등을 통해서 이를 해결할 수 있긴한데, 여기서는 더욱 직접적으로 S3에 올리는 파일들을 직접 내 데이터베이스 테이블에서 관리해보려한다.
왜 굳이 직접 DB 구성을 하는지?
이미 온라인으로 지원되는 서비스를 직접 구축하는거는 그만큼 목적성이 명확하고 내가 필요한대로 손을 보기 위해서이다.
나같은 경우에는 에디터를 통해 업로드되는 '본문 내 이미지'의 경우에는 별도의 리스트업을 하지않고 데이터를 쌓아두기만 했었다.
하지만 그런식으로 오래동안 운영하고, 또 파일 자체에 여러가지 버전(최적화 파일, 동영상 변환 파일 등)이 존재하다보니 순식간에 파일량은 증가하게 되었다.
파일량이 천개를 기본적으로 넘어가다보니 S3에서 원하는 파일을 찾기 힘들기도하고, 매번 파일리스트를 가져올때마다 그 수많은 파일을 전부 가져와서 필터링 하는 방식의 작업을 해야만 했다.
근데 만약 이런 S3파일들을 내 테이블에서 정보로써 관리를 한다면 필요한 조건에 맞는 파일을 찾는것도 쉬울뿐더러 굳이 api 요청을 통해서 작업을 처리하지않고 내 데이터베이스에서 필요한 만큼 작업을 하고 S3에는 필요한 파일 한두개만 s3_key로써 작업을 할 수 있다.
직접 DB 관리하는것은 이러한 목적에 부합하기 때문에 노선을 변경해보았다.
어떻게 DB에 S3 파일을 리스트업하나?
이 부분은 생각보다 간단하다.
우리가 일반적으로 프레임워크나 각자의 코드에서 API를 통해 S3에 파일을 store, put 한 이후 그 정보를 그냥 테이블에 기록하면 그만일 것이다.
Schema::create('editor_files', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('name_origin');
$table->boolean('origin');
$table->string('version');
$table->unsignedBigInteger('size');
$table->string('s3_key');
$table->string('s3_url');
$table->string('cdn_url');
$table->timestamps();
});
Copy
내가 설계한 테이블의 스키마는 위와 같다.
나는 이미지 파일을 업로드하면 그 이미지의 용량 최적화 버전을 만드는 코드가 내부에 구현되어있기때문에 이를 레코드로 저장하고 나중에 하나의 원본파일명으로 관리하기위해 이를
name_origin
이라는 고유 파일명과 함께 원본을 체크하는 origin
플래그와 version
을 통해 어떤 파생 파일인지 기록한다.또 기본적으로 s3 에 접근할때 필요한 파일 키인
s3_key
와 함께 배포를위한 url
등을 저장하는 컬럼으로 구성해보았다.이 부분에 대한 구성은 필요한 대로 구현해 아래와 같은 느낌으로 내용을 채우면 될것이다.
public function uploadFile(Request $request)
{
$file = $request->file('file');
$uploadPath = 'editor';
$originalFilePath = $file->store($uploadPath, 's3');
if ($originalFilePath) {
$fileSize = $file->getSize(); // 파일 크기 (바이트 단위)
$imagePath = $file->getPathname();
$imageInfo = getimagesize($imagePath);
$mime = $imageInfo['mime'];
$width = $imageInfo[0]; // 이미지의 가로 길이 (픽셀 단위)
$originFile = EditorFile::create([
'name' => pathinfo($originalFilePath, PATHINFO_BASENAME),
'name_origin' => pathinfo($originalFilePath, PATHINFO_FILENAME),
'origin' => true,
'version' => null,
'size' => Storage::disk('s3')->size($originalFilePath),
's3_key' => $originalFilePath,
's3_url' => Storage::disk('s3')->url($originalFilePath),
'cdn_url' => cdn_url($originalFilePath),
]);
...
...
...
Copy
기존 데이터는 어떻게 넣어야하나?
처음부터 위와같은 로직을 구현해두면 이후에 업로드되는 파일들은 전부 관리가능한 파일로 테이블에 보관되겠지만, 지금의 나처럼 기존방식을 유지하던 사람이라면 새로 만드는거보다 더 골치아픈게 기존에 운용하던 정보를 새 시스템에 마이그레이션 하는것일 것이다.
이 부분에 대해서는 쉽게 현재 테이블 형식에 맞게 s3에 올라가있는 모든 파일을 가져와서 csv파일로 변환하는 코드를 아래처럼 짜볼 수 있을것이다.
S3Client 사용 버전
protected $s3Client;
protected $bucket;
public function __construct()
{
$this->middleware('auth');
// AWS S3Client 초기화
$s3Config = config('filesystems.disks.s3');
$this->s3Client = new S3Client([
'region' => $s3Config['region'],
'version' => 'latest',
'credentials' => [
'key' => $s3Config['key'],
'secret' => $s3Config['secret'],
],
'endpoint' => $s3Config['endpoint'] ?? null,
]);
$this->bucket = $s3Config['bucket'];
}
public function getClient()
{
return $this->s3Client;
}
public function getBucket()
{
return $this->bucket;
}
public function exportEditorFiles($root = 'editor/')
{
$allFiles = [];
$token = null;
do {
// S3 요청
$result = $this->s3Client->listObjectsV2([
'Bucket' => $this->bucket,
'Prefix' => $root,
'ContinuationToken' => $token,
]);
$contents = $result['Contents'] ?? [];
$allFiles = array_merge($allFiles, $contents);
$token = $result['NextContinuationToken'] ?? null;
} while ($result['IsTruncated']);
// 시간 순으로 정렬 (과거부터 시작)
usort($allFiles, function ($a, $b) {
$aTime = strtotime($a['LastModified']);
$bTime = strtotime($b['LastModified']);
return $aTime <=> $bTime; // 과거 순서대로
});
// CSV 파일 생성
$csvData = "name,name_origin,size,s3_key,s3_url,cdn_url,created_at,updated_at\n";
foreach ($allFiles as $file) {
if (empty($file['Key']) || substr($file['Key'], -1) === '/') {
continue; // 디렉토리는 제외
}
$s3Key = $file['Key'];
$size = $file['Size'] ?? 0;
$lastModified = $file['LastModified'] ?? null; // S3에서 수정 시간 가져오기
$name = pathinfo($s3Key, PATHINFO_FILENAME); // 확장자 및 디렉터리 제외
$nameOrigin = str_replace(['_optimized', '_censored'], '', $name); // 접미사 제거
$extension = pathinfo($s3Key, PATHINFO_EXTENSION);
$s3Url = "https://{$this->bucket}.s3.{$this->s3Client->getRegion()}.amazonaws.com/{$s3Key}";
$cdnUrl = cdn_url($s3Key); // CDN URL 생성 함수 사용 (필요 시 변경)
// KST(UTC+9)로 변환
$lastModifiedUtc = new DateTime($file['LastModified']);
$lastModifiedUtc->setTimezone(new DateTimeZone('Asia/Seoul')); // KST로 변환
$createdAt = $lastModifiedUtc->format('Y-m-d H:i:s'); // KST 형식
$updatedAt = $createdAt; // 업데이트된 정보가 없으므로 생성일과 동일하게 설정
// CSV 행 생성
$csvData .= "{$name}.{$extension},{$nameOrigin},{$size},{$s3Key},{$s3Url},{$cdnUrl},{$createdAt},{$createdAt}\n";
}
// CSV 데이터 출력
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="editor_files.csv"');
echo $csvData;
exit;
}
Copy
Laravel Storage 클래스 사용 버전
public function exportEditorFiles($root = 'editor/')
{
$allFiles = Storage::disk('s3')->listContents($root, true);
// 디렉토리 제거 및 유효 파일만 필터링
$allFiles = array_filter($allFiles, function ($file) {
return $file['type'] === 'file'; // 파일만 포함
});
// 시간 순으로 정렬 (과거부터 시작)
usort($allFiles, function ($a, $b) {
$aTime = $a['timestamp'] ?? 0; // timestamp가 없을 경우 기본값 0
$bTime = $b['timestamp'] ?? 0;
return $aTime <=> $bTime; // 과거 순서대로
});
// CSV 파일 생성
$csvData = "name,name_origin,size,s3_key,s3_url,cdn_url,created_at,updated_at\n";
foreach ($allFiles as $file) {
$s3Key = $file['path'];
$size = $file['size'] ?? 0;
$lastModified = $file['timestamp'] ?? null;
$name = pathinfo($s3Key, PATHINFO_FILENAME); // 확장자 및 디렉터리 제외
$nameOrigin = str_replace(['_optimized', '_censored'], '', $name); // 접미사 제거
$extension = pathinfo($s3Key, PATHINFO_EXTENSION);
$s3Url = Storage::disk('s3')->url($s3Key);
$cdnUrl = cdn_url($s3Key); // CDN URL 생성 함수 사용 (필요 시 변경)
// KST(UTC+9)로 변환
$createdAt = $lastModified
? Carbon::createFromTimestamp($lastModified)->timezone('Asia/Seoul')->format('Y-m-d H:i:s')
: 'N/A';
$updatedAt = $createdAt; // 업데이트된 정보가 없으므로 생성일과 동일하게 설정
// CSV 행 생성
$csvData .= "{$name}.{$extension},{$nameOrigin},{$size},{$s3Key},{$s3Url},{$cdnUrl},{$createdAt},{$updatedAt}\n";
}
// CSV 데이터 출력
header('Content-Type: text/csv');
header('Content-Disposition: attachment; filename="editor_files.csv"');
echo $csvData;
exit;
}
Copy
라라벨의 경우에는 S3Client 기능을 상속해 더 편하게 클래스를 구현해놔서 만약에 라라벨을 사용하는 환경이라면 아래 방식을 더 추천한다.
결국 S3Client는 한번에 1000레코드밖에 못가져오니까 그걸 반복적으로 돌려서 모든 리스트를 가져오는 부분이 더 추가된것이다.
결론
실제로 이렇게 관리하게된 이후로 에디터로 업로드된 파일을 좀더 빠르고 직관적이게 관리할 수 있었으며, 옛날에 업로드된 파일이 현재 어느 페이지에서도 쓰지않을때 삭제하는 기능, 현재 파일이 어느 페이지에서 사용하는지 확인하는 기능등을 내 마음대로 만들 수 있어서 이런 방식의 관리도 나쁘지 않다고 생각한다.

결론적으로 부담스럽게 모든 리스트를 요청하는 기존 api 방식보다는 내가 직접 테이블로 메타정보를 제어하고 그에따라 api는 작업이 필요한 몇몇 파일만 요청해 사용하는게 훨씬 이상적인 방향일 것이다.
#AWS #php
0
개의 댓글
백엔드 콜렉션의 다른 글
개발 카테고리의 다른 글

01/14
페이지네이션과 로드 모어(Load More)를 동시 구현해보자
우리가 일반적으로 여러 데이터 레코드를 보기좋게 구현하기 위해서는 테이블 구조를 이용하곤한다. 테이블 구조는 행과 열로 구...

01/07
Flex Box 사용시 자식 요소의 높이가 늘어나는 문제 해결 방법
문제퍼블리싱 코드를 이리저리 만지다가 특이한 현상을 발견했다. 크아악!! 내 badge의 높이 상태가? 위 스샷처럼 타이틀 영역에...

01/02
구글 애드센스 모바일 반응형 광고 높이 조정하기
본 글은 구글 애드센스의 반응형 광고를 달때 로딩되는 광고가 원하는 크기보다 너무 큰 광고가 로딩될때 이를 해결하기위한 내...