Skip to content

[재하] 1130(목) 개발기록

박재하 edited this page Nov 30, 2023 · 2 revisions

목표

  • PATCH /post/:id 트랜잭션 개선
    • 계획
    • 구현
    • 동작 화면
  • POST /post 트랜잭션 적용
    • 구현
    • 동작 화면

PATCH /post/:id 트랜잭션 개선

계획

어제 트랜잭션 개선 당시 image 테이블에 새 레코드를 생성하는 부분은 트랜잭션이 따로 도는 문제가 있었다. uploadFile() 함수에서 이 로직을 밖으로 빼내서 문제를 해결해보자.

구현

async uploadFile(file: Express.Multer.File): Promise<any> {
  if (!file.mimetype.includes('image')) {
    throw new BadRequestException('Only image files are allowed');
  }

  const { buffer } = file;

  const resized_buffer = await sharp(buffer)
    .resize(500, 500, { fit: 'cover' })
    .toFormat('png', { quality: 100 })
    .toBuffer();

  const filename = uuid();

  // NCP Object Storage 업로드
  AWS.config.update(awsConfig);
  const result = await new AWS.S3()
    .putObject({
      Bucket: bucketName,
      Key: filename,
      Body: resized_buffer,
      ACL: 'public-read',
    })
    .promise();
  // eTag 없으면 에러 리턴
  if (!result.ETag) {
    throw new InternalServerErrorException('Failed to upload file');
  }
  Logger.log(`uploadFile result: ${result.ETag}`);

  const eTag = result.ETag;
  return {
    mimetype: 'image/png',
    filename,
    size: resized_buffer.length,
    eTag,
  };
}
  • NCP Object Storage 업로드 로직만 살려두고, 리턴에 Image 객체 대신 image 객체 생성에 필요한 정보를 Object로 리턴한다.

  • 더불어 정상적으로 업로드되었는지를 AWS-SDK가 ETag를 리턴해 주는지로 확인하고, Image Entity에 eTag도 not null로 추가해준다.

  • 더불어, 기존의 mimetype, filename도 업로드 요청한 클라이언트측의 정보가 아닌, Sharp로 resize 및 convert한 정보를 넣어준다.

...
export class Image extends BaseEntity {
	...
	@Column({ type: 'varchar', length: 50, nullable: false })
	eTag: string;
  ...
}

Image 엔티티에 컬럼 추가

async updateBoard(
  id: number,
  updateBoardDto: UpdateBoardDto,
  userData: UserDataDto,
  files: Express.Multer.File[],
) {
  // transaction 생성하여 board, image, star, like 테이블 동시에 수정
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();
  ...
  // transaction 시작
  await queryRunner.startTransaction();
  try {
    if (files.length > 0) {
      const images: Image[] = [];
      for (const file of files) {
        const imageInfo = await this.uploadFile(file);

        const image = queryRunner.manager.create(Image, {
          ...imageInfo,
        });

        const updatedImage = await queryRunner.manager.save(image);
        images.push(updatedImage);
      }
      // 기존 이미지 삭제
      for (const image of board.images) {
        // 이미지 리포지토리에서 삭제
        // await this.imageRepository.delete({ id: image.id });
        await queryRunner.manager.delete(Image, { id: image.id });
        // NCP Object Storage에서 삭제
        await this.deleteFile(image.filename);
      }
      // 새로운 이미지로 교체
      board.images = images;
    }
    ...

    // commit Transaction
    await queryRunner.commitTransaction();

    delete updatedBoard.user.password; // password 제거하여 반환

    return updatedBoard;
  } catch (err) {
    Logger.error(err);
    await queryRunner.rollbackTransaction();

    throw new InternalServerErrorException('Failed to update board');
  } finally {
    await queryRunner.release();
  }
}

수정된 로직은 위와 같다. image 대신 imageInfo를 받고, this.ImageRepository가 아닌 queryRunner.manager에서 create와 save를 실행

참고로 학습메모 1을 참고하면, try의 return updatedBoard;나 catch의 throw new InternalServerErrorException('Failed to update board');이 있어도, try-catch-finally문은 finally의 실행을 보장해준다.

즉 위와 같은 경우라도 queryRunner.release()는 항상 실행된다는 것.

동작 화면

스크린샷 2023-11-30 오후 2 15 39 스크린샷 2023-11-30 오후 2 16 15

이제 Image 삭제 및 새로 삽입하는 로직, 보드 수정 로직이 모두 하나의 트랜잭션에 들어간다. 성공!

POST /post 트랜잭션 적용

구현

앞서 uploadFile 개선까지 완료했으므로, updateBoard 메소드 로직을 참고하여 createBoard도 트랜잭션 방식으로 개선해보자.

async createBoard(
  createBoardDto: CreateBoardDto,
  userData: UserDataDto,
  files: Express.Multer.File[],
): Promise<Board> {
  const { title, content, star } = createBoardDto;

  // transaction 생성하여 board, image, star, like 레코드 동시에 생성
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();

  // const user = await this.userRepository.findOneBy({ id: userData.userId });
  const user = await queryRunner.manager.findOneBy(User, {
    id: userData.userId,
  });

  // transaction 시작
  await queryRunner.startTransaction();
  try {
    const images: Image[] = [];
    for (const file of files) {
      // Object Storage에 업로드
      const imageInfo = await this.uploadFile(file);

      // 이미지 리포지토리에 저장
      const image = queryRunner.manager.create(Image, {
        ...imageInfo,
      });

      const createdImage = await queryRunner.manager.save(image);

      images.push(createdImage);
    }

    // 별 스타일이 존재하면 MongoDB에 저장
    let star_id: string;
    if (star) {
      const starDoc = new this.starModel({
        ...JSON.parse(star),
      });
      await starDoc.save();
      star_id = starDoc._id.toString();
    }

    // const board = this.boardRepository.create({
    // 	title,
    // 	content: encryptAes(content), // AES 암호화하여 저장
    // 	user,
    // 	images,
    // 	star: star_id,
    // });
    const board = queryRunner.manager.create(Board, {
      title,
      content: encryptAes(content), // AES 암호화하여 저장
      user,
      images,
      star: star_id,
    });
    // const createdBoard: Board = await this.boardRepository.save(board);
    const createdBoard: Board = await queryRunner.manager.save(board);

    // commit transacton
    await queryRunner.commitTransaction();

    createdBoard.user.password = undefined; // password 제거하여 반환
    return createdBoard;
  } catch (error) {
    Logger.error(error);
    await queryRunner.rollbackTransaction();

    throw new InternalServerErrorException('Failed to update board');
  } finally {
    await queryRunner.release();
  }
}

동작 화면

스크린샷 2023-11-30 오후 2 14 36
  • before
스크린샷 2023-11-30 오후 1 23 21
  • after
스크린샷 2023-11-30 오후 1 59 31

이제 Image와 Board 레코드의 생성 로직이 모두 한 트랜잭션에 들어간다. 성공!

학습 메모

  1. finally 블록은 try, catch에 return이나 throw가 있어도 실행됨

소개

규칙

학습 기록

[공통] 개발 기록

[재하] 개발 기록

[준섭] 개발 기록

회의록

스크럼 기록

팀 회고

개인 회고

멘토링 일지

Clone this wiki locally