-
Notifications
You must be signed in to change notification settings - Fork 2
[재하] 1130(목) 개발기록
- PATCH /post/:id 트랜잭션 개선
- 계획
- 구현
- 동작 화면
- POST /post 트랜잭션 적용
- 구현
- 동작 화면
어제 트랜잭션 개선 당시 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()
는 항상 실행된다는 것.
이제 Image 삭제 및 새로 삽입하는 로직, 보드 수정 로직이 모두 하나의 트랜잭션에 들어간다. 성공!
앞서 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();
}
}
- before
- after
이제 Image와 Board 레코드의 생성 로직이 모두 한 트랜잭션에 들어간다. 성공!
© 2023 debussysanjang
- 🐙 [가은] Three.js와의 설레는 첫만남
- 🐙 [가은] JS로 자전과 공전을 구현할 수 있다고?
- ⚽️ [준섭] NestJS 강의 정리본
- 🐧 [동민] R3F Material 간단 정리
- 👾 [재하] 만들면서 배우는 NestJS 기초
- 👾 [재하] GitHub Actions을 이용한 자동 배포
- ⚽️ [준섭] 테스트 코드 작성 이유
- ⚽️ [준섭] TypeScript의 type? interface?
- 🐙 [가은] 우리 팀이 Zustand를 쓰는 이유
- 👾 [재하] NestJS, TDD로 개발하기
- 👾 [재하] AWS와 NCP의 주요 서비스
- 🐰 [백범] Emotion 선택시 고려사항
- 🐧 [동민] Yarn berry로 모노레포 구성하기
- 🐧 [동민] Vite, 왜 쓰는거지?
- ⚽️ [준섭] 동시성 제어
- 👾 [재하] NestJS에 Swagger 적용하기
- 🐙 [가은] 너와의 추억을 우주의 별로 띄울게
- 🐧 [동민] React로 멋진 3D 은하 만들기(feat. R3F)
- ⚽️ [준섭] NGINX 설정
- 👾 [재하] Transaction (트랜잭션)
- 👾 [재하] SSH 보안: Key Forwarding, Tunneling, 포트 변경
- ⚽️ [준섭] MySQL의 검색 - LIKE, FULLTEXT SEARCH(전문검색)
- 👾 [재하] Kubernetes 기초(minikube), docker image 최적화(멀티스테이징)
- 👾 [재하] NestJS, 유닛 테스트 각종 mocking, e2e 테스트 폼데이터 및 파일첨부
- 2주차(화) - git, monorepo, yarn berry, TDD
- 2주차(수) - TDD, e2e 테스트
- 2주차(목) - git merge, TDD
- 2주차(일) - NCP 배포환경 구성, MySQL, nginx, docker, docker-compose
- 3주차(화) - Redis, Multer 파일 업로드, Validation
- 3주차(수) - AES 암복호화, TypeORM Entity Relation
- 3주차(목) - NCP Object Storage, HTTPS, GitHub Actions
- 3주차(토) - Sharp(이미지 최적화)
- 3주차(일) - MongoDB
- 4주차(화) - 플랫폼 종속성 문제 해결(Sharp), 쿼리 최적화
- 4주차(수) - 코드 개선, 트랜잭션 제어
- 4주차(목) - 트랜잭션 제어
- 4주차(일) - docker 이미지 최적화
- 5주차(화) - 어드민 페이지(전체 글, 시스템 정보)
- 5주차(목) - 감정분석 API, e2e 테스트
- 5주차(토) - 유닛 테스트(+ mocking), e2e 테스트(+ 파일 첨부)
- 6주차(화) - ERD
- 2주차(화) - auth, board 모듈 생성 및 테스트 코드 환경 설정
- 2주차(목) - Board, Auth 테스트 코드 작성 및 API 완성
- 3주차(월) - Redis 연결 후 RedisRepository 작성
- 3주차(화) - SignUpUserDto에 ClassValidator 적용
- 3주차(화) - SignIn시 RefreshToken 발급 및 Redis에 저장
- 3주차(화) - 커스텀 AuthGuard 작성
- 3주차(수) - SignOut시 토큰 제거
- 3주차(수) - 깃헙 로그인 구현
- 3주차(토) - OAuth 코드 통합 및 재사용
- 4주차(수) - NestJS + TypeORM으로 MySQL 전문검색 구현
- 4주차(목) - NestJS Interceptor와 로거
- [전체] 10/12(목)
- [전체] 10/15(일)
- [전체] 10/30(월)
- [FE] 11/01(수)~11/03(금)
- [전체] 11/06(월)
- [전체] 11/07(화)
- [전체] 11/09(목)
- [전체] 11/11(토)
- [전체] 11/13(월)
- [BE] 11/14(화)
- [BE] 11/15(수)
- [FE] 11/16(목)
- [FE] 11/19(일)
- [BE] 11/19(일)
- [FE] 11/20(월)
- [BE] 11/20(월)
- [BE] 11/27(월)
- [FE] 12/04(월)
- [BE] 12/04(월)
- [FE] 12/09(금)
- [전체] 12/10(일)
- [FE] 12/11(월)
- [전체] 12/11(월)
- [전체] 12/12(화)