이미지와 첨부파일은 DB에 넣지 않는다
명함 사진, 결재 첨부, 프로필 이미지 — 파일은 생각보다 빨리 쌓입니다. 이걸 어디에 둘 것인가는 사소해 보이지만, “DB에 직접 넣느냐, 따로 보관하느냐”가 1년 뒤의 운영을 통째로 갈라놓습니다. 그리고 그보다 더 미묘한 함정이 하나 더 숨어 있습니다 — “파일 주소를 어디까지 저장하느냐.” 이 글은 그 두 결정을, 처음 설계하는 분을 위해 그림과 함께 풀어 쓴 기록입니다.
용어부터 — 객체 스토리지와 presign
섹션 제목: “용어부터 — 객체 스토리지와 presign”왜 파일을 DB에 넣으면 안 되나
섹션 제목: “왜 파일을 DB에 넣으면 안 되나”“파일도 데이터니까 DB에 같이 넣으면 편하지 않나?” 처음엔 그렇게 생각하기 쉽습니다. 한 군데서 관리되니 깔끔해 보입니다. 하지만 대형 바이너리(이미지·PDF 같은 큰 파일)를 DB에 직접 넣으면, 곧 네 방향에서 대가가 돌아옵니다.
| 증상 | 왜 생기나 |
|---|---|
| 백업이 거대해진다 | DB 백업이 모든 이미지·첨부를 통째로 끌고 다닌다 |
| 쿼리가 느려진다 | 큰 바이너리가 테이블에 끼어 데이터가 무거워진다 |
| 메모리·대역폭 낭비 | 파일 하나 내려받자고 백엔드 프로세스가 그 데이터를 통째로 들었다 놓는다 |
| 확장이 어렵다 | 여러 서버로 늘릴 때 DB가 파일까지 떠안아 병목이 된다 |
그래서 업계의 표준 답은 이미 정해져 있습니다. 파일 본체는 S3 호환 객체 스토리지에 두고, DB에는 그 파일을 가리키는 키만 저장합니다. SL.AIMS도 개발 환경에서는 MinIO를 객체 스토리지로 쓰고, 운영 단계로 가면 스토리지 백엔드를 바꿔도 되도록 설계했습니다. DB는 “어디에 무엇이 있다”는 색인만 들고, 무거운 짐은 전용 창고에 맡긴 셈입니다.
비유하자면 도서관과 같습니다. 도서관 카탈로그(DB)에는 “그 책은 3층 A-12 서가에 있다”는 위치 정보만 적혀 있지, 책의 본문 전체가 카탈로그에 옮겨 적혀 있지 않습니다. 책 본문은 서가(객체 스토리지)에 있습니다. 만약 카탈로그에 모든 책의 전문을 베껴 넣었다면, 카탈로그를 뒤지는 일(쿼리)부터 느려지고, 카탈로그를 복제(백업)하는 데도 어마어마한 시간이 걸릴 것입니다. 색인은 가볍게, 본체는 전용 창고에 — 이게 자연스러운 분업입니다.
presign — 파일이 백엔드를 거치지 않게
섹션 제목: “presign — 파일이 백엔드를 거치지 않게”업로드·다운로드는 presigned URL로 처리합니다. 핵심 아이디어는 이렇습니다 — 백엔드는 “허락”만 하고, 실제 무거운 파일 전송은 클라이언트와 스토리지가 직접 합니다. 백엔드는 그 사이에서 빠집니다.
이렇게 하면 파일이 아무리 커도 백엔드의 메모리·대역폭을 잡아먹지 않습니다. 동시에 여러 사람이 큰 파일을 올려도 API 서버가 출렁이지 않습니다. 백엔드는 “허가증 발급소”로만 남고, 짐 나르는 일은 전용 창고가 맡습니다.
진짜 함정 — DB에 절대 URL을 박지 마라
섹션 제목: “진짜 함정 — DB에 절대 URL을 박지 마라”여기까지는 비교적 잘 알려진 이야기입니다. 정작 사람을 무는 건 그다음의 미묘한 결정입니다 — 파일을 가리키는 주소를 DB에 어떻게 저장하느냐.
같은 파일, 두 가지 저장 방식의 운명
섹션 제목: “같은 파일, 두 가지 저장 방식의 운명”- 절대 URL 저장 — DB에
https://(스토리지 도메인)/cards/abc.jpg통째로. 지금은 편합니다. - 1년 뒤 스토리지 교체 — 개발 MinIO에서 운영 스토리지로 옮기거나 도메인을 바꿉니다.
- 절대 URL의 운명 — 저장된 모든 주소가 옛 도메인을 가리켜 전부 깨짐 → 전수 데이터 마이그레이션.
- 상대경로의 운명 — DB는
cards/abc.jpg만 들고 있었으므로 무변경. 도메인은 환경변수 한 줄만 바꾸면 URL이 자동으로 새로 조립됩니다.
”도메인을 데이터에 박지 않는다”는 결정
섹션 제목: “”도메인을 데이터에 박지 않는다”는 결정”이게 이 설계의 진짜 핵심입니다. 상대경로만 저장하면, 스토리지를 옮기든 도메인을 바꾸든 DB 데이터는 손대지 않아도 됩니다. 최종 URL은 그때그때 “환경변수(도메인) + 저장된 키(경로)“로 조립되므로, 환경이 바뀌면 조립 결과도 자동으로 따라 맞습니다.
| 절대 URL 저장 | 상대경로 저장 |
|---|---|
| 편의 URL을 바로 쓸 수 있다. 그러나 스토리지·도메인 교체 = 전수 데이터 마이그레이션. 이식성이 사실상 0. | 저장된 값만으론 바로 못 연다(조립 필요). 대신 백엔드·도메인을 갈아끼워도 DB 무변경. 이식성을 산다. |
트레이드오프는 분명합니다. “저장값을 그대로 붙여 넣으면 열리는 편의 URL”을 포기하는 대신, “백엔드를 자유롭게 갈아끼울 수 있는 이식성”을 얻습니다. 잠깐 쓰고 버릴 프로토타입이라면 편의가 이길 수도 있습니다. 하지만 운영을 길게 가져갈 시스템이라면, 이쪽이 거의 항상 옳습니다. 환경을 바꾸는 일은 반드시 오고, 그때 “전수 마이그레이션이냐 설정 한 줄이냐”의 차이는 어마어마합니다.
| DB에 저장하는 값 | 변하는가? | 어디에 둬야 |
|---|---|---|
| 객체 키 (cards/abc.jpg) | 안 변함 (그 파일의 고유 이름) | ✅ DB (데이터) |
| 스토리지 도메인 | 환경마다 다름·바뀜 | 환경변수 (설정) |
| 엔드포인트·접속 정보 | 환경마다 다름·바뀜 | 환경변수 (설정) |
이 결정에는 더 큰 원칙이 깔려 있습니다 — “환경에 따라 달라지는 값은 데이터가 아니라 설정에 둔다.” 도메인, 엔드포인트 주소, 스토리지 위치 같은 것들은 개발 환경과 운영 환경에서 다르고, 시간이 지나며 바뀝니다. 이렇게 “변하는 값”을 데이터에 직접 박아 넣으면, 환경이 바뀔 때마다 데이터를 통째로 손봐야 합니다. 반대로 변하는 값을 설정(환경변수)으로 빼 두면, 데이터는 “안 변하는 사실”(이 파일의 키는 무엇인가)만 들고 있게 됩니다. 이 분리를 처음에 해 두면, 나중에 “개발에서 운영으로”, “이 스토리지에서 저 스토리지로” 옮기는 일이 공포스러운 이벤트가 아니라 그냥 설정 교체가 됩니다. 같은 원칙이 도메인뿐 아니라 비밀 키·접속 정보 등 거의 모든 환경 의존 값에 적용됩니다.
관련 글: 〈디스크 비대화 회수 런북〉, 〈보안은 나중에 추가할 수 없다〉. 전체 그림은 〈사례연구: SL.AIMS〉에 있습니다.