SMAIVNN
article thumbnail

조만간 운영할 사이트를 하나 만들며 이미지와 글이 혼합하여 보이는 블로그 형식의 게시글을 구현하고 있었습니다.

C(R)UD를 구현하며 느낀것, 어떻게 더 성능을 끌어 올릴까 등 다양한 방법과 후기를 남기고자 합니다.

 

우선 CRUD는 어떻게 보면 기본적인 플랫폼/서비스의 기본이 되는 작업이죠. 블로그도 sns도 모두 여기서부터 시작됩니다. 저는 이러한 CRUD에 있어서 가장 중요한 것은 데이터 정합성, 효율적인 데이터 처리라고 생각합니다.

 

문제

이번 고민 또한 이러한 데이터 정합성 문제에서 부터 시작되었습니다. 

 

제가 원하는 형식은 블로그처럼 글과 글 사이에 이미지가 첨부 가능한 게시글입니다. 이를 블로그형 게시글이라고 부르도록 하겠습니다. 

 

블로그형 게시글은 크게 보면 다음과 같은 순서의 작업이 필요합니다.

1. 이벤트 발생 > 2. 이미지 업로드 API > 3. 저장소 주소 반환 > 4. 게시글 내 이미지 주소 변환 > 5. 게시글 API

 

저는 아래와 같은 코드로 해당 방법을 구현하였습니다. (Next.js)

 const [uploadedImages, setUploadedImages] = useState<{ id: string; file: File }[]>([]);
	...
  const handleSubmit = async (data: z.infer<typeof FormSchema>) => {
	...
	// 본문과 일치하는 이미지 file들을 filtering합니다.
    const matchedImages = findMatchedImages(data.content, uploadedImages);

	// 이미지를 먼저 업로드합니다.
    if (matchedImages.length > 0) {
      const s3Response = await uploadImagesToS3( token, matchedImages, me.user.id! );

      if (s3Response.status !== 201) {
        console.log("Failed to upload images to S3.");
        return;
      }
      const imageUrl = await s3Response.json();

	// 본문 이미지태그들의 src를 대체합니다.
      data.content = updateImageSrc(data.content, imageUrl);
    }
	...
	게시글 저장
  };

 

구현 이후 제가 떠올린 문제점은 다음과 같았습니다. 

1. 해당 방법은 이미지가 업로드 된 이후 다시 한번 더 전송을 보내기에 네트워크 오버헤드가 생긴다. 그냥 한번에 전송하면 안될까?

2. 이미지를 따로 업로드 하고 게시글 저장이 실패했을 경우 혹은 그 외 상황들에서 데이터의 정합성을 어떻게 처리해야하지?

 

개선 과정

지금 부터는 제가 이런 생각들을 시작으로 개선을 진행한 과정입니다.

 

해결 방법을 구체화 하기 전, 이를 위한 다양한 방법을 먼저 찾아보았습니다. 그 중 대표적인 방법은 다음과 같습니다.

1. 이미지와 게시글을 한번에 보내기

2. 이미지를 미리 업로드 하기

 

이 둘에 대해서 조금 더 자세히 다뤄보도록 하겠습니다.

 

이미지와 게시글을 한번에 보내기

이 방법은 데이터를 한번에 서버로 전송하여 서버에서 모든 작업을 처리한 후 반환하는 방법입니다. 클라이언트 서버는 한번의 요청을 통해 게시글을 업로드 합니다. 

 

이미지 클라우드에 업로드한다고 생각했을 때 한번만 진행하면 되기에 중복 저장 비용이 발생하지 않습니다. 사용자가 이미지와 게시글을 한번에 전송하기에 구현이 상대적으로 간단합니다.

 

// Next.js
const handleSubmit = async (data: z.infer<typeof FormSchema>) => {
    ...
    const matchedImages = findMatchedImages(data.content, uploadedImages);
    const formData = new FormData();
    formData.append("content", data.content);

    if (matchedImages.length > 0) {
      matchedImages.forEach((image) => {
        formData.append("file", image.file);
      });
    }

    const response = await createPostWithImages(token, formData);
    if (response.status !== 201) {
      // s3 요소 삭제
      return;
    }
  };
// Nest.js
@UseInterceptors(
    FileFieldsInterceptor(
      [
        { name: 'thumbnail', maxCount: 1 },
        { name: 'file', maxCount: 20 },
      ],
      multerOptions(),
    ),
  )
  @HttpCode(201)
  @Post()
  async TxPostWithImages(
    @Req() request,
    @UploadedFiles()
    files: { thumbnail: Express.Multer.File; file: Express.Multer.File[] },
    @Body() dto: CreatePostDto,
  ) {
  	  ...
      // 한번에 비즈니스 로직으로 전달
      await this.PostService.TxCreateWithImages(request, files, dto);
      ..
  }

 

하지만 단점도 명확합니다. 대용량 이미지를 여러개 첨부할 경우 오버헤드가 증가합니다. 업로드 과정에서의 오류를 어떻게 처리하느냐에 따라 전체 요청의 실패로 처리할 수도 있으며 이러한 모든 과정은 결과적으로 사용자 경험에 영향을 미칩니다.

 

이미지를 미리 업로드 하기

해당 방법은 사용자가 이미지를 에디터에 혹은 이미지 첨부 버튼을 통해 업로드 했을 때, 해당 이미지를 바로 서버에 전송하는 것입니다. 서버에서는 이를 임시폴더(혹은 임시 s3 폴더)에 저장한 이후 사용자가 게시글 저장 이벤트를 통해 확정했을 때 본문에 해당하는 이미지들을 매인 폴더로 저장합니다. 자세한 설명은 아래와 같습니다.

 

 

이 방법은 파일을 개별적으로 업로드 하고 나중에 이동시키기에 확장성, 유연성이 좋습니다. 특히 이미지 처리에 필요한 데이터를 한번에 전송하지 않기 때문에 데이터가 분산되어 사용자 경험이 증가하는 것은 더불어 빠른 응답시간을 보장합니다.

 

하지만 많은 단점이 있습니다. 대표적으로, 중복 저장 비용이 중가합니다. 임시 폴더에 저장되기에 그리고 한번 더 메인폴더로 이동되며 만약 이미지 클라우드를 사용할 경우 중복 저장 비용이 발생할 수 있습니다. 또한 개별적인 업로드로 인해 더 많은 네트워크 호출이 생기게 됩니다. 

 

무엇을 선택해야 할까?

정합성 측면

앞서 말했듯이 어떤 방법을 선택하던 일단 결국 데이터가 일관적이어야 합니다. S3의 경우 데이터베이스 트랜잭션 처럼 원자성을 지원하지 않습니다. 따라서 서버에서 트랜잭션 작업을 묶어놔도 롤백이 발생했을 때 이미 업로드 된 file이 삭제되거나 다시 생기지 않습니다.

 

결국 게시물 저장에서도 오류가 발생하면 다시 한번 s3에 접근해서 작업을 진행해주어야 합니다.

 

데이터를 한번에 저장하는 방법은 이것에 대해서 글쓰기를 하나의 트랜잭션 단위로 접근합니다. 

하나의 트랜잭션 작업을 어떤식으로 분리한다면 여러 예외 상황에 대비하기 힘들고, 정합성을 맞춰주기 위해 다양한 작업이 필요할 수 있습니다. 즉 복잡도가 올라가고 코스트가 상승합니다. 글쓰기 뿐 아닌 다른 작업으로 생각하여도 그렇습니다. 일괄된 트랜잭션으로 데이터 일관성을 지킬 수 있으나 All or Nothing으로 부분 성공/실패가 존재하지 않습니다.

 

반면 이미지를 미리 업로드 하는 방식은 이미지가 먼저 업로드 되기에 게시글 작성 중 이미지 업로드 실패로 인한 손실 등을 방지할 수 있습니다. 또한 이미지 업로드가 실패하더라도 게시글 작성이 계속될 수 있으며 나중에 이미지를 다시 업로드 할 수도 있습니다. 하지만 트랜잭션의 단위를 명확하게 글쓰기가 아닌 이미지 업로드, 게시글 작성으로 분리해서 생각하기때문에 이 과정에서 동기화 문제가 발생할 수있습니다. 업로드와 게시글 전송이 분리되어 있기 때문에 각 단게에서의 오류를 독립적으로 처리할 수 있다는 부분도 있습니다.

 

따라서 정합성 문제는 서버에서의 처리이며, 결론은 행위를 더 세분화할 것인가 혹은 크게 가져갈 것인가에 따른 차이라고 판단이 되었습니다. 

 

효율(성능)적 측면

이미지와 게시글을 한번에 보내기

로직이 단순합니다. 이미지와 게시글을 한번에 저장하므로 관리가 간단합니다. 하지만 파일 크기에 따라 전체 응답이 길어질 수 있습니다. 아래는 KB단위의 이미지 파일을 최소 5장~10장 사이에서 서버에 일괄적으로 전달한 성능입니다.

 

그리 크지 않은 사진을 전송했음에도 평균 588ms의 응답 시간을 보입니다. 함께 전달하는 파일의 용량이 더 커질수록 이러한 응답 시간은 더 길어질 것이며 사용자 경험에도 영향을 미칠 확률이 큽니다.

미리 업로드 

사용자가 이미지를 업로드하는 동안 게시글을 작성할 수 있으므로, 응답 시간이 단축되어 사용자 경험이 향상됩니다 또한 이미지 업로드와 게시글 작성이 병렬로 진행될 수 있어 전체 작업이 더 빠르게 완료될 수 있습니다. 

 

 

이를 증명하듯, 일괄 업로드 방식과는 다르게 평균 79ms로 약 7배 더 빠른 응답 속도를 보여줍니다. 이미지는 벌써 처리 되었고 문서만을 저장하면 되기 때문입니다. 

 

물론 두 가지 방법의 세부적인 코드나 비즈니스 로직의 차이는 존재합니다만 큰 틀에서의 성능 차이는 이와 유사하다고 판단합니다.

 

경제적 측면

한번에 저장하는 방식

  • 단순한 비용 구조: 이미지를 한 번에 업로드하므로, 별도의 임시 저장 비용이 발생하지 않습니다.
  • 높은 코스트: 이미지 업로드와 게시글 저장이 동시에 이루어지므로, 오버헤드가 상대적으로 높을 수 있습니다.

미리 업로드 방식

  • 효율적인 리소스 사용: 이미지를 업로드하는 동안 사용자가 게시글을 작성할 수 있으므로, 서버 리소스를 효율적으로 사용할 수 있습니다.
  • 임시 저장 비용: 이미지가 임시 폴더에 저장되므로, 추가적인 저장 비용이 발생할 수 있습니다. 예를 들어, S3를 사용하는 경우 임시 폴더의 저장 비용이 추가됩니다.
  • 삭제 비용: 주기적으로 임시 폴더를 정리해야 하며, 이 과정에서도 비용이 발생할 수 있습니다.

 

S3는 결국 접근하는 만큼 비용이 과금되기에 미리 업로드하는 방식이 더욱 과금 요소는 클 수 있습니다. 저도 아직 배포 전이라 어느정도의 차이가 날지는 모르겠지만 한번 비교하여 추후에 작성하도록 하겠습니다.

 

결론적으로

저는 두 가지 방법을 고려하여, 아래와 같은 사항을 점검하여야 한다고 생각합니다.

1. 구현 및 유지보수가 간단한가

2. 사용자수가 어느정도인가

 

초기 스타트업 또는 사이드 프로젝트의 경우 빠르고 간단한 구현과 시장 평가가 중요합니다. 일괄 업로드 방식의 구현은 비교적 간단합니다. 이미지와 게시글을 함께 하나의 요청으로 처리하기 때문에 별도의 업로드 및 저장 로직이 필요하지 않습니다. 이는 이후의 유지 보수작업이 더욱 쉽도록 할 수 있습니다.

 

또한 초기에는 사이트 이용자가 많지 않기 때문에 한 번에 큰 트래픽이 발생하지 않을 가능성이 큽니다. 사이트 트래픽이 낮기 때문에 네트워크 부하와 서버 처리 시간이 큰 문제가 되지 않을 수 있습니다.

 

따라서 사용자 수가 증가할 때까지는 일괄 업로드 방식으로 충분히 운영할 수 있습니다.

반면, 이미지를 미리 업로드하는 경우 이미지 업로드와 게시글 전송을 분리해야 하므로 구현이 더 복잡합니다. 또한 업로드된 이미지를 임시 저장하고 게시글과 연결하는 로직이 필요합니다. 하지만 업로드가 완료된 후 게시글을 작성하므로 전체적인 사용자 경험이 향상됩니다.

 

성능면에서도 이 방법이 더 우세합니다. 특히 인스타그램과 같은 수억명이 사용하는 SNS 어플도 이러한 방법을 채택하고 있는 것으로 알고 있습니다. 즉, 사이트가 성장하여 사용자 수가 증가하면, 이 방법이 더 효율적일 수 있습니다.

 

https://youtu.be/V27XkmVPqYQ?si=fROut30MBDSUIa8r

인스타그램 백엔드가 20억 유저를 감당하는 방법 - 노마드 코더

 

사용자 경험을 우선시하는 경우 이 방법이 더 좋습니다. 또한 향후 사이트가 성장할 것을 고려하여 미리 준비할 수 있습니다.

지금까지 내용을 표로 정리하자면 아래와 같습니다.

 

항목 한 번에 전송 미리 업로드
성능 요청 크기 커서 응답 시간 길어질 수 있음 요청 크기 작아져 응답 시간 단축됨
속도 업로드 시간이 길어질 수 있음 이미지 업로드 후 게시글 전송 시 속도 빠름
비용 한번에 접근 빈번한 접근
트래픽 트래픽 집중 트래픽 분산
복잡성 구현 간단 구현 복잡
사용자 경험 업로드 시간 동안 사용자 대기 필요 실시간 업로드 상태 확인 가능, 사용자 경험 향상


추가로, 저는 이제 취준을 하는 입장에서 현업을 아직 경험해보지 못했지만, 대부분의 현업에서는 미리 업로드 방식을 사용한다고 합니다. 그 이유는 비즈니스 단에서는 보통 파일서버를 별도로 두기 때문입니다. 

 

이러한 구현의 이유는, 파일이라는 대용량 트래픽을 서비스 백엔드에서 분리하여 서비스 메모리 및 트래픽을 제어하는데 의미가 있다고 합니다.

 

끝으로

결론적으로 저는 현재 저의 상황에서는 이미지를 게시글과 함께 한 번에 보내는 방법이 더 적합하다고 판단하였습니다.

 

하지만 이 두가지 방법의 성능 비교 및 구현을 해보고 싶었기에 각기 다른 두 가지 기능에 이 둘을 적용하여 운영을 해볼 생각입니다. 항상 느끼는 것이지만 영상 스트리밍, 실시간 서버 등도 어렵지만 단순한 CRUD도 다양하게 고려할 부분이 많은 것 같습니다. 다양한 의견 댓글 부탁드립니다. 읽어주셔 감사합니다.

profile

SMAIVNN

@SMAIVNN

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