코딩 기록소
article thumbnail
반응형

시험 기간으로 밀려서 마저 올리지 못했던 프로젝트에서 발생한 궁금증과 그 해결 시간이다.

이번에 프로젝트하면서 단일 이미지 업로드, 다중 이미지 업로드를 처리하는데 많은 시간과 어려움을 겪었다. 그리고 내가 선택한 해결 방법과 그 이유에 대해서 알아보자.

 

이미지 업로드 방식에는 무엇이 있을까?

나는 이미지 업로드 방식에서 크게 5가지로 나눌 수 있었다.

 

blob을 사용하여 db에 저장 (권장 X)

설명

이것은 db에 데이터 타입 중 하나인 blob이라는 자료형을 사용하여 파일을 저장하는 방식이다.

Binary Large Object의 줄임말으로써, 말 그대로 대용량 바이너리 데이터 즉, 이진 데이터를 저장하는 방법이다.

MySQL에서는 다음과 같은 크기를 가지고 있다.

BLOB 타입 용량
TINYBLOB 2^8 - 1 (256 bytes)
BLOB 2^16 - 1 (64 KB)
MEDIUMBLOB 2^24 - 1 (8 MB)
LONGBLOB 2^32 - 1 (4 GB)

단점

  1. 이미지의 이진 데이터를 그대로 저장하는 방법이기 때문에 데이터베이스의 크기를 증가시키고 성능에 많은 영향을 끼친다.
  2. 처리 비용이 크다.

base64 형태로 db에 저장 (권장 X)

설명

base64란 그대로 직역하면 64진법이라는 뜻으로, 이미지의 이진 데이터 Text 형태로 바꾸는 Encoding 방법이라고 생각하면 쉽다. 이미지를 인코딩 하여 얻은 base64를 데이터베이스에 저장하는 기법이다.

단점

  1. 이진데이터를 base64로 인코딩하여 변환하면 2진 데이터가 64진법으로 바뀌기 때문에 원래의 이미지 용량보다 더 큰 용량을 가진다.
  2. 이미지를 base64로 변환하면 데이터의 값 자체가 엄청 크기 때문에 처리 비용이 상당하다.
  3. 이미지를 보여줄 때 Decoding의 과정이 필요하다.

백엔드에서 파일을 저장하고 경로를 db에 저장 (일반적인 방법)

설명

말 그대로 백엔드에서 이미지 업로드 요청을 받으면 서버 컴퓨터에 이미지를 저장하고 해당 경로를 db에 저장하는 방식이다.

장점

  1. 백엔드 단에서 파일을 저장하기 때문에 이미지의 업로드다운로드가 빠르다.
  2. 백업이 상대적으로 간단하다.
  3. 외부 서비스에 대한 의존성이 없다.
  4. base64, blob과는 다르게 경로만 저장하기 때문에 db에 부담이 가지 않는다.
  5. 중소규모 프로젝트에 적합하다.

단점

  1. 저장 공간의 제약을 받는다.
  2. 보안 및 백업을 따로 처리해야 한다.
  3. 서버에 요청이 많아지면 업로드 및 다운로드의 속도에 영향이 미친다.

Cloud Storage에 이미지를 업로드하고 경로를 db에 저장 (일반적인 방법)

설명

이미지를 백엔드 또는 DB 단에서 업로드하지 않고 AWS, GCP, Firebase Storage와 같은 제 3자 Cloud Storage에 업로드하는 방식이다. 이 때 Cloud Storage에 업로드된 경로는 DB에 저장해야 한다.

장점

  1. 이미지의 보안성을 보장한다.
  2. 복제, 백업 기능을 제공하기 떄문에 코드의 단순화가 이루어진다.
  3. 이미지 서버를 따로 둠으로써 안정성이 증가한다.
  4. 중대규모 프로젝트에 적합하다.

단점

  1. 클라우드 스토리지 상태에 따라 추가 대역폭시간이 소요될 수 있다.
  2. 사용량에 따른 비용이 발생한다.
클라우드 스토리지란?
클라우드 컴퓨팅 제공업체를 통해 비정형 데이터(이미지, 동영상 등)를 안전하게 물리적 서버에 저장하는 가상의 공간이다. 보안, 백업, 복제 등의 여러 가지 기능을 제공하며 서버와 파일 서버를 따로 둠으로써 최근에 많이 사용하는 방법 중 하나이다.

클라이언트가 Cloud Storage에 이미지를 업로드하고 경로를 db에 저장

설명

클라이언트가 Cloud Storage에 이미지를 업로드 하고, 해당 경로만 db에 저장하는 방식이다.

이미지를 저장하는 방식은 대부분 3번4번을 통해서 이미지 처리 방식이 이루어진다. 하지만, 대용량 파일 또는 다중 파일 업로드를 할 때에는 서버에 큰 부담을 줄 수 있다. 그렇기에 이러한 방법을 사용하기도 한다.

장점

  1. 서버의 메모리, 대역폭 등을 신경쓰지 않고 업로드할 수 있다.
  2. 서버에 부담이 가지 않기 때문에 대용량, 다중 파일 업로드에 유용하다.

단점

  1. 클라이언트 측에서 악의적인 이미지의 변조 등의 보안 문제가 발생할 수 있다.
  2. 클라이언트 - DB - 클라우드 스토리지 간의 일관성 문제가 발생할 수 있다.
  3. 클라우드 스토리지의 접근 권한권한 관리를 보다 신중하게 적용해야 한다.

내가 구현하고자 하는 기능과 그 방식 선택은?

내가 구현하고자 하는 이미지 관련 기능은 다음과 같다.

  • 유저의 프로필 사진, 고객센터 문의 이미지 등 단일 또는 적은 개수의 이미지 업로드 기능
  • 앨범에서 사용하는 다중 이미지 업로드 기능

앨범이라는 기능을 사용하기 위해서는 정말 많은 이미지를 다루어야 하기 때문에 위와 같은 처리 방식을 참고하면 5번이 제일 적합하다. 하지만 나는 다음과 같은 이유로 4번을 선택하였다.

만약, 클라이언트 - DB - 클라우드 스토리지 간의 일관성 보안을 유지할 수 있다면 클라이언트 단에서 이미지 업로드 하는 것이 정말 좋은 방법이라고도 생각한다.

  1. DB와 이미지의 정보의 일관성이 아주 중요하다고 생각
  2. 5번에서 이미지 관련 에러가 발생하면 일관성이 깨질 위험이 매우 증가
  3. Front 측 개발자가 Front에 대한 이해 및 React Native를 처음 사용함으로써, 복잡한 코드를 넣는다면 개발 시간이 많이 소요될 거라 생각
  4. 백엔드 측에서 이미지 업로드를 하나의 트랜잭션에 포함시켜 일관성을 유지

클라우드 스토리지 무엇을 쓸까?

클라우드 스토리지 관해서는 따로 깊게 서술하지는 않겠습니다. 해당 주소를 걸어둘테니 관심있는 사람들은 들어가서 확인하시면 됩니다.

AWS S3

Amazon에서 제공하는 SC3이다. 클라우드 스토리지로써 정말 많이 사용하는 제품이다.

프리티어라고 해서 무료 체험판을 1년동안 제공해준다. AWS S3는 언젠간 사용할 날이 올 거 같기 때문에 체험판을 잠깐 사용하는 건 아까울 거라 생각해서 패스한다.

특징

  1. 파일 설치는 불가하며, 파일 업로드, 삭제, 변경만 가능하다.
  2. 파일 단 하나의 용량은 5TB로 제한하지만, 업로드 개수는 제한이 없다.
  3. Bucket이라는 단위로 파일 집합을 구분한다. 쉽게 말해 디렉토리의 개념이다.
  4. 파일 관련 API (업로드, 삭제, 변경 등), Bucket API (조회, 생성, 삭제 등), Lifecycle 등을 제공한다.
  5. Region을 사용하여 파일 서버의 위치를 선택할 수 있다.

Firebase Storage

구글에서 제공하는 Firebase Storage이다. 어느정도 외부 서비스 API를 사용해보았다면 Firebase는 정말 많이 들어봤을 거라 생각한다.

Google Could를 기반으로 하는 오디오, 이미지, 동영상 등을 저장하는 객체 저장소이다.

특징

  1. Google Cloud Storage을 기반으로 만들어져있다.
  2. Bucket이라는 단위를 사용한다.
  3. Firebase 인증을 통하여 개별 파일이나 파일 그룹에 대해 액세스 권한을 설정이 가능하다.
  4. Google Cloud Storage API를 통해 GCS와 연동하여 사용이 가능하다.
  5. 무료이다.

GCS

구글에서 제공하는 Google Cloud Storage이며, Firebase Storage에 기반이 되는 저장소이다.

신규 계정당 3개월 동안 500크레딧을 제공한다. 여기서 500 크레딧은 엄청나게 크기 때문에 다 사용할 일은 없다.

특징

  1. Bucket이라는 단위를 사용한다.
  2. 구글 계정은 새로 만드는 게 쉬워서 사실상 필요할 때마다 무료로 사용할 수 있다.
  3. Region을 사용하여 파일 서버의 위치를 선택할 수 있다.
  4. 공개 URL을 사용할 수 있다.

그래서 내가 선택한 것은?

처음에는 무료를 사용하기 위해서 Firebase Storage를 사용하였다. S3나 GCS를 잘못 사용해서 요금 폭탄을 맞고싶지 않기 때문이다. 하지만 나는 Firebase Storage에 한계를 부딪히고 GCS로 넘어갔다.다음은 내가 크게 느낀 단점들이다.

  1. Firebase Storage에서 제공하는 기능이 적다.
    1. Firebase Storage에는 파일 이동 기능이 없지만 GCS는 move라는 함수를 사용하여 파일 이동을 할 수있다.
    2. Firebase Storage는 getDownloadURL를 통하여 이미지의 주소를 생성하고 사용자에게 보여주게 한다. 하지만 GCS는 인터넷 공개 URL을 사용하여 간단한 설정만으로도 사용자에게 URL 변환 과정 없이 보여줄 수 있다.
      단, 이미지 URL 호출할 수있는 접근 권한을 설정해주어야 한다. 아니면 요금 폭탄 맞는다.
  2. 좀 더 상세한 오류 및 그로 인한 오류 핸들링의 단순화 코드 단순화

이미지 처리 시뮬레이션

그래서 나는 어떻게 이미지를 업로드하는지에 대해서 많이 궁금하실 수도 있고, 이것을 보기위해 오신 분들도 있을 것이다. 그렇기에 나는 내가 처리한 방식을 공유하고자 한다.

이미지 업로드 흐름

  1. GCS에 저장할 이미지 Path를 생성
  2. DB 트랜잭션 시작
  3. DB에 Path 및 각종 관련 데이터를 저장
      1. 에러 발생 시 rollback
  4. 이미지를 클라우드 스토리지에 업로드
      1. 클라우드 스토리지  업로드 실패 시 rollback
  5. DB commit

이렇게 하면 앨범 기능은 몇 백개가 들어올 수 있는데 너무 느린 API가 되지 않냐고 생각할 수도 있다. 만약 그러한 부분을 신경 썼다면 나는 클라이언트 단에서 업로드를 하는 방법을 채택했을 것이다. 하지만, 나는 속도보다 일관성 측면을 좀 더 중요시하기 때문에 비록 시간이 오래 걸린다고 해도 안정성을 가져갈 것이다.

 

물론, 테스트 하였을 때 작은 용량의 이미지 100개를 업로드하는데 11초 정도 걸린 것 같다. 큰 이미지 용량과 이미지 압축 과정을 더하면 더 시간이 걸릴 것이다. 카카오톡에서 이미지 30개를 전송하는데 걸리는 시간이 30초 이상이었던 걸 생각하면 나쁘지 않다고 본다(?)

Cloud Storage에 저장될 경로란?
Cloud Storage는 Bucket이라는 단위로 묶여있습니다. 쉽게 말해 루트 디렉토리라고 볼 수 있습니다. 각 버킷마다 프로젝트 별로 나눌 수도 있고, 기능 별로 나눌 수도 있습니다.
우리는 프로젝트 별로 Bucket을 나누었으며 버킷 안에서 이미지가 저장될 경로는 임의로 선택할 수가 있습니다.

간단한 예로는 A라는 프로젝트에서 유저의 프로필 사진을 저장한다고 하면 A라는 Bucket에서 /users/[user_id]/profile 이라는 경로로 설정할 수 있습니다.

이미지 삭제 흐름

이 부분에서 좀 많은 시간이 소요됐었다. 처음에는 다음과 같이 적용했었다.

  1. DB 이미지 정보 삭제
  2. Cloud Storage에 있는 이미지 삭제
    1. 만약 삭제 실패할 시 rollback
  3. DB commit

하지만, 사용자 입장에서는 삭제를 하는데 오류로 인해 삭제하지 못한다고 뜨면 사용자 친화적이지 못하다라고 생각했고, 삭제 API 소요 시간이 너무 길다고 생각하였다. 업로드는 앨범이라는 특성상 일관성을 유지를 위해 어쩔 수 없이 동기 코드로 작성되어 시간이 좀 걸리는 API가 되었지만 삭제는 추후에 일관성 처리할 수 있는 방법이 많다고 생각하였다.

 

그렇기에, 나는 다음과 같이 변경하였다.

  1. DB 트랜잭션 시작
  2. DB 이미지 정보 삭제
  3. DB commit
  4. GCS에 있는 이미지를 삭제
    1. 이 때부터는 비동기 코드이므로, 백그라운드 단에서 작동된다.
    2. 만약 삭제 실패할 시 error_image라는 테이블에 이미지 정보를 저장한다.
    3. error_image에 저장도 못했다면 log를 남겨 실패한 이미지의 정보를 남긴다.

사용자 친화적으로 만들기 위해 삭제를 실패하였을 때는 추후에 처리하는 과정을 만들어냈다.

삭제 실패한 이미지들은 error_image에 저장하게 되는데 매월 1일마다 서버에서 배치를 돌려 일괄적으로 삭제처리 하게 하였다. 만약, error_image에 넣지도 못하였다면 log를 확인해 해당 이미지의 정보를 확인할 수 있다.

그로인해, 이미지 삭제는 비동기로 처리되기 때문에 매우 빠른 속도의 API가 되었다.

트랜잭션, 커밋(commit), 롤백(rollback)이란?
트랜잭션은 데이터를 변화시키기 위해 수행하는 하나의 작업 단위를 말한다.
커밋은 트랜잭션 내에서 데이터를 변화시켰다면 그것을 영구적으로 데이터베이스에 반영하기 위한 명령어이다.
롤백은 트랜잭션 내에서 데이터를 변화시킨 것을 트랜잭션 걸기 전의 상태로 돌려보내고 트랜잭션을 종료하는 명령어이다.

쉽게 말해, 한 번에 데이터를 여러 번 처리해야할 때는 트랜잭션을 사용하여 하나의 작업 단위로 묶고 성공하였을 때는 커밋을 사용하여 데이터의 변화를 적용하거나, 실패하였을 때는 롤백을 사용하여 데이터가 변화하기 전의 상태로 되돌리는 것이다.
배치(Batch)란?
실시간으로 처리하지 않아도 되며, 대용량 데이터를 특정 시간마다 처리하고 싶을 때 사용할 수 있는 방법이다.
나 같은 경우는 매월 1일마다 error_image에 저장된 이미지를 삭제하는 코드를 돌리게 한다.
또한, Tour API를 사용하여 관광지의 정보를 매월 2일마다 요청하여 가져온다.
반응형
profile

코딩 기록소

@seungyong20

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!