Skip to content

Conversation

@asjasj3964
Copy link
Collaborator

@asjasj3964 asjasj3964 commented Feb 12, 2025

Sweepic Server PR List

close #199

⚒️develop의 최신 커밋을 pull 받았나요?

  • 최신 커밋 업데이트

🔍️ 이 PR을 통해 해결하려는 문제가 무엇인가요?

어떤 기능을 구현한건지, 이슈 대응이라면 어떤 이슈인지 PR이 열리게 된 계기와 목적을 Reviewer 들이 쉽게 이해할 수 있도록 적어 주세요
일감 백로그 링크나 다이어그램, 피그마를 첨부해도 좋아요

  • 로컬 환경에서는 에러 처리가 잘 되나 배포 환경에서는 존재하지 않은 폴더 ID를 입력하거나 중복되는 파일명을 입력하는 등의 경우에 에러 처리가 잘 되지 않고 CORS 서버 에러가 발생하였습니다. 또한 이러한 문제가 매 테스트마다 발생하는 것이 아닌 선택적으로 발생했습니다 (그러나 대부분의 경우에 발생하는 듯)
  • 로컬 환경에서는 정상적으로 작동이 잘 돼서 원인을 파악하기 어려우나 CORS 에러가 뜨기 전에 시간이 오래 걸렸던 것을 인지하여 사진 업로드 및 저장을 처리하는 multer 미들웨어(imageUploader)에서 Key 함수의 로직에 문제가 있었던 것이 아닌지 생각해보았습니다. (로컬 환경에서도 에러 처리를 연속적으로 실행 시 속도가 느려지기도 했습니다)

✨ 이 PR에서 핵심적으로 변경된 사항은 무엇일까요? (핵심 작업 내용)

문제를 해결하면서 주요하게 변경된 사항들을 적어 주세요

1차 시도

  • 기존의 imageUploader 미들웨어 코드
    ...
    export const imageUploader = multer({
      // 파일 업로드 미들웨어 설정
      storage: multerS3({
        ...
        key: async (req: Request, file, callback) => {
          // S3 버킷에 저장될 경로와 이름 정의
          const userId = req.user!.id; // 사용자 ID
    
          const uuid = uuidv4(); // UUID 생성
          const extension = path.extname(file.originalname); // 파일 이름(확장자) 추출
          if (!allowedExtensions.includes(extension)) {
            // 업로드 파일의 확장자가 허용 목록에 없을 경우
            return callback(new PhotoValidationError({extension: extension}));
          }
    
          // 디렉토리 path 설정 과정
          let uploadDirectory = null;
          console.log(req.body);
          if (req.body.folderName) {
            console.log(req.body.folderName);
            const createdMemoFolderId = await createMemoFolder(
              bodyToMemoFolder(req.body),
              userId,
            );
            if (createdMemoFolderId === null) {
              return callback(
                new FolderDuplicateError({folderName: req.body.folderName}),
              );
            }
            uploadDirectory = createdMemoFolderId;
            req.uploadDirectory = uploadDirectory; // 디렉토리 정보 저장
          } else {
            console.log(req.params.folderId);
            const folderId = req.params.folderId;
            const checkFolder = await getMemoFolder(BigInt(folderId));
            if (checkFolder === null || checkFolder.userId !== userId) {
              return callback(
                new FolderNotFoundError({folderId: BigInt(folderId)}),
              );
            }
            uploadDirectory = folderId;
          }
    
          callback(
            null,
            ${userId}/${uploadDirectory}/${uuid}_${file.originalname},
          ); // S3 버킷에서 파일이 저장될 key
        },
        acl: 'private', // 비공개 설정 (업로드 파일을 버킷 소유자만 접근 가능)
      }),
      limits: {fileSize: 5 * 1024 * 1024}, // 이미지 용량 제한 (5MB)
    });
    
    • 기존에는 async/await을 사용해 비동기 작업을 동기적으로 실행하여 callback() 실행 전에 모든 비동기 작업이 완료될 때까지 기다려야 했습니다. 또한 이로 인해 예외 처리 방식이 일관되지 않았을 수도 있습니다.
  • 개선된 imageUploader 미들웨어 코드
    ...
    export const imageUploader = multer({
      // 파일 업로드 미들웨어 설정
      storage: multerS3({
        ...
        key: (req: Request, file, callback) => {
          // S3 버킷에 저장될 경로와 이름 정의
          const userId = req.user!.id; // 사용자 ID
    
          const uuid = uuidv4(); // UUID 생성
          const extension = path.extname(file.originalname); // 파일 이름(확장자) 추출
          if (!allowedExtensions.includes(extension)) {
            // 업로드 파일의 확장자가 허용 목록에 없을 경우
            return callback(new PhotoValidationError({extension: extension}));
          }
    
          // 디렉토리 path 설정 과정
          let uploadDirectory = null;
          if (req.body.folderName) {
            Promise.all([createMemoFolder(bodyToMemoFolder(req.body), userId)])
              .then(([createdMemoFolderId]) => {
                if (createdMemoFolderId === null) {
                  return callback(
                    new FolderDuplicateError({folderName: req.body.folderName}),
                  );
                }
                uploadDirectory = createdMemoFolderId;
                req.uploadDirectory = uploadDirectory; // 디렉토리 정보 저장
                callback(
                  null,
                  `${userId}/${uploadDirectory}/${uuid}_${file.originalname}`,
                ); // S3 버킷에서 파일이 저장될 key
              })
              .catch(err => callback(err));
          } else {
            const folderId = req.params.folderId;
            Promise.all([getMemoFolder(BigInt(folderId))])
              .then(([checkFolder]) => {
                if (checkFolder === null || checkFolder.userId !== userId) {
                  return callback(
                    new FolderNotFoundError({folderId: BigInt(folderId)}),
                  );
                }
                uploadDirectory = folderId;
                callback(
                  null,
                  `${userId}/${uploadDirectory}/${uuid}_${file.originalname}`,
                ); // S3 버킷에서 파일이 저장될 key
              })
              .catch(err => callback(err));
          }
        },
        acl: 'private', // 비공개 설정 (업로드 파일을 버킷 소유자만 접근 가능)
      }),
      limits: {fileSize: 5 * 1024 * 1024}, // 이미지 용량 제한 (5MB)
    });
    
    • Promise.all([...])을 사용하여 비동기 작업을 병렬적으로 처리하고, .then()과 .catch()으로 콜백을 실행하는 방식으로 수정하였습니다. Promise.all([...])과 .then() 체인을 사용해 createMemoFolder()나 getMemoFolder()의 실행이 완료되면 callback()을 호출하도록 했습니다. 또한 콜백 내부에서 바로 에러를 처리하게 하여(.catch(err => callback(err))) 예외 처리가 일관되게 수행되도록 했습니다.
  • 그러나 연속적으로 에러 처리를 발생했을 때 속도가 점점 느려졌고 그것이 CORS 에러를 발생하는 원인이 아닐까 생각했습니다. 그래서 속도를 저하되는 원인을 찾아보았고 유효성 검사(폴더명이 중복인지, 존재하는 폴더인지) 시에 불필요하게 multer 미들웨어에서 S3에 먼저 요청을 하는 것을 발견하였습니다. 때문에 multer 미들웨어를 거치기 전에 유효성 검사를 하는 방법을 고민하였습니다.

2차 시도

// src/s3/image.uploader.ts
...
export const fileValidation = async (
  req: Request,
  file: Express.MulterS3File,
  next: NextFunction,
) => {
  try {
    const userId = req.user!.id;

    let uploadDirectory = null;
    if (req.body.folderName) {
      try {
        const createdMemoFolderId = await createMemoFolder(
          bodyToMemoFolder(req.body),
          userId,
        );
        if (createdMemoFolderId === null) {
          return next(
            new FolderDuplicateError({folderName: req.body.folderName}),
          );
        }
        uploadDirectory = createdMemoFolderId;
        req.uploadDirectory = uploadDirectory;

        next();
      } catch (error) {
        return next(error);
      }
    } else {
      try {
        const folderId = req.params.folderId;
        const checkFolder = await getMemoFolder(BigInt(folderId));
        if (!checkFolder || checkFolder.userId !== userId) {
          return next(new FolderNotFoundError({folderId: BigInt(folderId)}));
        }
        uploadDirectory = folderId;
        req.uploadDirectory = BigInt(uploadDirectory);

        next();
      } catch (error) {
        return next(error);
      }
    }
  } catch (error) {
    return next(error);
  }
};

export const imageUploader = multer({
  // 파일 업로드 미들웨어 설정
  storage: multerS3({
    // multerS3 저장소 설정
   ...
    key: (req: Request, file, callback) => {
      // S3 버킷에 저장될 경로와 이름 정의
      const extension = path.extname(file.originalname);
      if (!allowedExtensions.includes(extension)) {
        throw new PhotoValidationError({extension: extension});
      }
      const userId = req.user!.id; // 사용자 ID
      const folderId = req.uploadDirectory;
      const uuid = uuidv4(); // UUID 생성
      const s3Key = `${userId}/${folderId}/${uuid}_${file.originalname}`;

      callback(null, s3Key);
    },
    acl: 'private', // 비공개 설정 (업로드 파일을 버킷 소유자만 접근 가능)
  }),
  limits: {fileSize: 5 * 1024 * 1024}, // 이미지 용량 제한 (5MB)
});
// src/controllers/memo-image.controller.ts

@Route('memo')
export class MemoImageController extends Controller {
  ...
  @Post('/image-format/folders/:folderId')
  **@Middlewares([fileValidation, ImageUploadMiddleware])**
  @Tags('memo-image-controller')
  ...
  public async handlerMemoImageAdd(
    @Request() req: ExpressRequest,
    @Path('folderId') targetFolderId: string,
  ): Promise<ITsoaSuccessResponse<MemoFolderImageResponseDto>> {
    try {
      ...
    }
  }
  ...
}
  • 파일을 S3에 접근해 업로드하기 전에 저장하려는 폴더가 유효한지 검사하는 fileValidation 미들웨어를 만들고 미들웨어 적용 시 @middlewares([fileValidation, ImageUploadMiddleware])로 fileValidation이 imageUploadMiddleware보다 먼저 실행하도록 하였습니다. 이는 사진 저장 API에서 연속적인 유효 검사 에러를 빠르게 처리해주었지만 폴더 생성 및 사진 저장 API에서는 req.body가 undefined로 나와 서버 에러를 맞았습니다. 이는 multer 미들웨어가 파일 업로드를 처리할 때 multipart/form-data 형식의 요청에서만 req.body 값을 채울 수 있어 multer 실행 전에 req.body는 undefined가 되는 것이었습니다.
  • 따라서 폴더 생성 및 사진 저장 API는 폴더 생성 API와 사진 저장 API로 구현하는 것이 효율적일 것이라는 결론을 내렸습니다.

최종 코드

// src/s3/image.upload.ts

export const imageUploader = multer({
  // 파일 업로드 미들웨어 설정
  storage: multerS3({
    // multerS3 저장소 설정
    s3: s3, // AWS S3 객체 설정
    bucket: process.env.AWS_S3_BUCKET_NAME, // 업로드할 S3 버킷 이름
    contentType: multerS3.AUTO_CONTENT_TYPE, // 업로드 파일의 MIME 타입 자동 설정
    key: (req: Request, file, callback) => {
      // S3 버킷에 저장될 경로와 이름 정의
      const extension = path.extname(file.originalname);
      if (!allowedExtensions.includes(extension)) {
        throw new PhotoValidationError({extension: extension});
      }

      const userId = req.user!.id; // 사용자 ID
      const folderId = req.uploadDirectory;
      const uuid = uuidv4(); // UUID 생성
      const s3Key = `${userId}/${folderId}/${uuid}_${file.originalname}`;

      callback(null, s3Key);
    },
    acl: 'private', // 비공개 설정 (업로드 파일을 버킷 소유자만 접근 가능)
  }),
  limits: {fileSize: 5 * 1024 * 1024}, // 이미지 용량 제한 (5MB)
});
// src/s3/image.uploader.middleware.ts
export const ImageUploadMiddleware = async (
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  const userId = BigInt(req.user!.id);
  if (req.params === null) {
    return next(
      new DataValidationError({reason: 'folderId가 유효하지 않습니다.'}),
    );
  }
  try {
    const folderId = req.params.folderId;
    console.log(req.params);
    const checkFolder = await getMemoFolder(BigInt(folderId));

    if (!checkFolder || checkFolder.userId !== userId) {
      return next(new FolderNotFoundError({folderId: BigInt(folderId)}));
    }
    const uploadDirectory = folderId;
    req.uploadDirectory = BigInt(uploadDirectory);
  } catch (error) {
    return next(error);
  }
  imageUploader.single('image')(req, res, err => {
    if (err) {
      next(err);
    }
    next();
  });
};
// src/controllers/memo-image.controller.ts

@Route('memo')
export class MemoImageController extends Controller {
  ...
  @Post('/image-format/folders/:folderId')
  @Middlewares(ImageUploadMiddleware)
  @Tags('memo-image-controller')
  ...
  public async handlerMemoImageAdd(
    @Request() req: ExpressRequest,
    @Path('folderId') targetFolderId: string,
  ): Promise<ITsoaSuccessResponse<MemoFolderImageResponseDto>> {
    try {
      ...
    }
  }
  ...
}
  • ImageUploadMiddleware 안에서 imageUploader multer를 거치기 전에 사진을 저장할 폴더가 존재하는지 검사 수행
  • 폴더 생성 및 사진 저장 API(handlerMemoFolderImageAdd)는 주석처리

🤚 동작 확인

기능을 실행했을 때 정상 동작하는지 여부를 확인하고 스크린 샷을 올려주세요

🔖 핵심 변경 사항 외에 추가적으로 변경된 부분이 있나요?

없으면 "없음" 이라고 기재해 주세요

  • 없음

🙏 Reviewer 분들이 이런 부분을 신경써서 봐 주시면 좋겠어요

개발 과정에서 다른 분들의 의견은 어떠한지 궁금했거나 크로스 체크가 필요하다고 느껴진 코드가 있다면 남겨주세요

  • imageUploader 미들웨어 로직을 자세히 살펴봐주시면 감사하겠습니다.

🩺 이 PR에서 테스트 혹은 검증이 필요한 부분이 있을까요?

테스트가 필요한 항목이나 테스트 코드가 추가되었다면 함께 적어주세요

  • 없음

📌 PR 진행 시 이러한 점들을 참고해 주세요

  • Reviewer 분들은 코드 리뷰 시 좋은 코드의 방향을 제시하되, 코드 수정을 강제하지 말아 주세요.
  • Reviewer 분들은 좋은 코드를 발견한 경우, 칭찬과 격려를 아끼지 말아 주세요.
  • Review는 특수한 케이스가 아니면 Reviewer로 지정된 시점 기준으로 2일 이내에 진행해 주세요.
  • Comment 작성 시 Prefix로 P1, P2, P3 를 적어 주시면 Assignee가 보다 명확하게 Comment에 대해 대응할 수 있어요
    • P1 : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
    • P2 : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
    • P3 : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)


📝 Assignee를 위한 CheckList

  • To-Do Item

@asjasj3964 asjasj3964 added ♻️ REFACTOR 기능 향상 및 리팩토링 🐛 FIX 버그 수정 labels Feb 12, 2025
@asjasj3964 asjasj3964 self-assigned this Feb 12, 2025
@asjasj3964 asjasj3964 linked an issue Feb 12, 2025 that may be closed by this pull request
* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선

* [SWEP-96] 폴더 생성 및 사진 저장 API 주석처리 + multer 미들웨어 실행 전에 폴더 ID 검사
* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선

* [SWEP-96] 폴더 생성 및 사진 저장 API 주석처리 + multer 미들웨어 실행 전에 폴더 ID 검사

* [SWEP-96] tsoa.json 폴더 생성 및 사진 저장 spec requestBody 삭제
@jjiinaaa jjiinaaa merged commit c8e89a6 into develop Feb 13, 2025
1 check passed
@jjiinaaa jjiinaaa deleted the fix/SWEP-96 branch February 13, 2025 09:18
jjiinaaa added a commit that referenced this pull request Feb 14, 2025
* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선 (#200) (#201)

* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선 (#200)

* fix: 폴더 생성 및 사진 저장 API 주석처리 + multer 미들웨어 실행 전에 폴더 ID 검사 (#202)

* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선

* [SWEP-96] 폴더 생성 및 사진 저장 API 주석처리 + multer 미들웨어 실행 전에 폴더 ID 검사

* fix: tsoa.json 폴더 생성 및 사진 저장 spec requestBody 삭제 (#203)

* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선

* [SWEP-96] 폴더 생성 및 사진 저장 API 주석처리 + multer 미들웨어 실행 전에 폴더 ID 검사

* [SWEP-96] tsoa.json 폴더 생성 및 사진 저장 spec requestBody 삭제

* [SWEP-98] 챌린지 API tsoa 변환 (#205)

* [SWEP-86] 히스토리 API 임시 저장

* [SWEP-87] 챌린지 API 개선

* [SWEP-58] 스웨거 문서 작성 (#187)

* [SWEP-58] 스웨거 문서 작성

* [SWEP-58] 사진삭제 스웨거 주석 작성

* [SWEP-58] 스웨거 문서 변경

* [SWEP-89] 메모 텍스트 저장 API tsoa 적용 및 여러 multer 객체 사용할 수 있도록 구현 (#189) (#191)

Co-authored-by: JinHa Park <160022452+jjiinaaa@users.noreply.github.com>

* [SWEP-98] tsoa 변환

* [SWEP-98] 임시저장 14:31

* [SWEP-98] app.ts 수정

---------

Co-authored-by: Socializedistp <159607145+Socializedistp@users.noreply.github.com>
Co-authored-by: asjasj3964 <84120715+asjasj3964@users.noreply.github.com>
Co-authored-by: JinHa Park <160022452+jjiinaaa@users.noreply.github.com>

---------

Co-authored-by: asjasj3964 <84120715+asjasj3964@users.noreply.github.com>
Co-authored-by: codie0226 <80187674+codie0226@users.noreply.github.com>
Co-authored-by: Socializedistp <159607145+Socializedistp@users.noreply.github.com>
jjiinaaa added a commit that referenced this pull request Feb 15, 2025
* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선 (#200) (#201)

* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선 (#200)

* fix: 폴더 생성 및 사진 저장 API 주석처리 + multer 미들웨어 실행 전에 폴더 ID 검사 (#202)

* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선

* [SWEP-96] 폴더 생성 및 사진 저장 API 주석처리 + multer 미들웨어 실행 전에 폴더 ID 검사

* fix: tsoa.json 폴더 생성 및 사진 저장 spec requestBody 삭제 (#203)

* [SWEP-96] 메모장 사진 업로드 및 저장 로직 개선

* [SWEP-96] 폴더 생성 및 사진 저장 API 주석처리 + multer 미들웨어 실행 전에 폴더 ID 검사

* [SWEP-96] tsoa.json 폴더 생성 및 사진 저장 spec requestBody 삭제

* [SWEP-98] 챌린지 API tsoa 변환 (#205)

* [SWEP-86] 히스토리 API 임시 저장

* [SWEP-87] 챌린지 API 개선

* [SWEP-58] 스웨거 문서 작성 (#187)

* [SWEP-58] 스웨거 문서 작성

* [SWEP-58] 사진삭제 스웨거 주석 작성

* [SWEP-58] 스웨거 문서 변경

* [SWEP-89] 메모 텍스트 저장 API tsoa 적용 및 여러 multer 객체 사용할 수 있도록 구현 (#189) (#191)

Co-authored-by: JinHa Park <160022452+jjiinaaa@users.noreply.github.com>

* [SWEP-98] tsoa 변환

* [SWEP-98] 임시저장 14:31

* [SWEP-98] app.ts 수정

---------

Co-authored-by: Socializedistp <159607145+Socializedistp@users.noreply.github.com>
Co-authored-by: asjasj3964 <84120715+asjasj3964@users.noreply.github.com>
Co-authored-by: JinHa Park <160022452+jjiinaaa@users.noreply.github.com>

* [SWEP-58] 휴지통 API 변경사항 (#207)

* [SWEP-101] 히스토리 API 수정 (#213)

* [SWEP-101] 히스토리 API 수정

* [SWEP-101] 어워드 조회 수정

---------

Co-authored-by: asjasj3964 <84120715+asjasj3964@users.noreply.github.com>
Co-authored-by: codie0226 <80187674+codie0226@users.noreply.github.com>
Co-authored-by: Socializedistp <159607145+Socializedistp@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 FIX 버그 수정 ♻️ REFACTOR 기능 향상 및 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SWEP-96] 메모장 사진 업로드 및 저장 로직 개선

2 participants