1. Transaction(๊ฑฐ๋) ์๊ฐ
- Transaction์ ์ฌ๋ฌ ์คํผ๋ ์ด์
์ ํ๋์ ๋
ผ๋ฆฌ์ ์ธ ์์
์ผ๋ก ์คํํ๋ ๊ธฐ๋ฅ์ด๋ค.
- ํธ๋์ญ์ (Transaction) : ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ํ๋ฅผ ๋ณํ์ํค๊ธฐ ํด์ ์ํํ๋ ์์ ์ ๋จ์
- e.g. Transaction(๊ฑฐ๋) ์์ : ์ํ ๊ฑฐ๋
- ๊ณ์ข A์์ 1000์์ ๊ณ์ข B๋ก ์ก๊ธํ๋ค.
- ์ด ๊ณผ์ ์
single transaction์ด๋ผ ํ๋ค.
- Transaction์ ์์ : Begin, Commit, Rollback
Begin: ํธ๋์ญ์ ์์Commit: ํธ๋์ญ์ ์ ์ฅRollback: ํธ๋์ญ์ ์ทจ์
- Transaction์ ๋ฌธ์ ์
Lost Reads: ๋ ํธ๋์ญ์ ์ด ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฐ์ดํธํด์ ํ๋์ ์ ๋ฐ์ดํธ๊ฐ ์์ค๋๋ ๊ฒฝ์ฐDirty Reads: ์์ง Commit ๋์ง ์์ ๊ฐ์ ๋ค๋ฅธ ํธ๋์ญ์ ์ด ์ฝ๋ ๊ฒฝ์ฐNon-Repeatable Reads: ํ ํธ๋์ญ์ ์์ ๋ฐ์ดํฐ๋ฅผ ๋๋ฒ ์ฝ์๋ ๋ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ ๋์ค๋ ๊ฒฝ์ฐPhantom Reads: ์ฒซ Read ์ดํ์ ๋ค๋ฅธ ํธ๋์ญ์ ์์ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ ๊ฒฝ์ฐ
1.1 Lost Update
1-- ์ด๊ธฐ์ํ2-- Table: account3-- | id | balance |4-- |----|---------|5-- | 1 | 1000 |67-- ํธ๋์ญ์ 18BEGIN TRANSACTION;9SELECT balance FROM account WHERE id = 1; -- 1000 ๋ฐํ10UPDATE account SET balance = 1000 - 100 WHERE id = 1; -- 900์ผ๋ก ์ ๋ฐ์ดํธ1112-- ํธ๋์ญ์ 2 (๋์์ ๋ฐ์)13BEGIN TRANSACTION;14SELECT balance FROM account WHERE id = 1; -- 1000 ๋ฐํ15UPDATE account SET balance = 1000 - 200 WHERE id = 1; -- 800์ผ๋ก ์ ๋ฐ์ดํธ1617-- ํธ๋์ญ์ 1 ์งํ18COMMIT; -- Balance: 9001920-- ํธ๋์ญ์ 2 ์งํ21COMMIT; -- Balance: 8002223-- ์ต์ข ๊ฒฐ๊ณผ24-- | id | balance |25-- |----|---------|26-- | 1 | 800 |
- ๋๊ฐ์ ํธ๋์ญ์ ์ด ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ๊ณ ์ ๋ฐ์ดํธํ๋ค.
- ๋์ค์ ์งํ๋ ํธ๋์ญ์ ์ด ๋จผ์ ์งํ๋ ํธ๋์ญ์ ์ ๊ฒฐ๊ณผ๋ฅผ ๋ฎ์ด์ด๋ค.
- ๋จผ์ ์งํ๋ ํธ๋์ญ์ ์ ์์ ์ ์ ์ค๋๋ค.
- Optimistic Lock ์ ๋ต์ผ๋ก ํด๊ฒฐ ๊ฐ๋ฅํ๋ค
1.2 Dirty Reads
1-- ์ด๊ธฐ์ํ2-- Table: account3-- | id | balance |4-- |----|---------|5-- | 1 | 1000 |67-- ํธ๋์ญ์ 18BEGIN TRANSACTION;9UPDATE account SET balance = balance - 100 WHERE id = 1; -- 900์ผ๋ก ์ ๋ฐ์ดํธ1011-- ํธ๋์ญ์ 212BEGIN TRANSACTION;13SELECT balance FROM account WHERE id = 1; -- 900 ๋ฐํ1415-- ํธ๋์ญ์ 1 ๋กค๋ฐฑ16ROLLBACK; -- Balance: 1000์ผ๋ก ๋๋๋ฆผ1718-- ํธ๋์ญ์ 2 ์งํ19-- ํธ๋์ญ์ 2์์ ์ฝ์ balance ๊ฐ์ ์๋ชป๋ ๊ฐ์ด๋ค.
- ์์ง ์ปค๋ฐ๋์ง ์์ ๋ค๋ฅธ ํธ๋์ญ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์์๋ ์๊ธฐ๋ ๋ฌธ์ ๋ค.
- ๋ณ๊ฒฝํ ๋ฐ์ดํฐ๋ฅผ ์ปค๋ฐํ์ง ์๊ณ ๋กค๋ฐฑํ ๊ฒฝ์ฐ ๋กค๋ฐฑ ์ ์ ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ๋ค๋ฅธ ํธ๋์ญ์ ์ ์๋ชป๋ ์ ๋ณด๋ก ๋ก์ง์ ์งํํ๋ค.
- Read Committed ํธ๋์ญ์ ์ผ๋ก ํด๊ฒฐ ๊ฐ๋ฅํ๋ค
1.3 Non-repeatable Reads
1-- ์ด๊ธฐ์ํ2-- Table: account3-- | id | balance |4-- |----|---------|5-- | 1 | 1000 |67-- ํธ๋์ญ์ 18BEGIN TRANSACTION;9SELECT balance FROM account WHERE id = 1; -- 1000 ๋ฐํ1011-- ํธ๋์ญ์ 212BEGIN TRANSACTION;13UPDATE account SET balance = balance - 100 WHERE id = 1; -- 900์ผ๋ก ์ ๋ฐ์ดํธ1415-- ํธ๋์ญ์ 1 ์งํ16SELECT balance FROM account WHERE id = 1; -- 900 ๋ฐํ (non-repeatable read)17COMMIT;
- ํธ๋์ญ์
์ด ๋ฐ์ดํฐ๋ฅผ ์ฝ์ ์ํ์์ ๋ค๋ฅธ ํธ๋์ญ์
์ด ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ ๊ฒฝ์ฐ,
- ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ์ฝ์์๋ ๊ธฐ์กด ์ฝ์๋ ๋ฐ์ดํฐ๊ฐ ์ฌ๊ตฌํ๋์ง ์๋ ํ์์ ์ด์ผ๊ธฐํ๋ค.
- Repeatable Read ํธ๋์ญ์ ์ผ๋ก ํด๊ฒฐ ๊ฐ๋ฅํ๋ค
1.4 Phantom Reads
1--- ์ด๊ธฐ์ํ2--- Table: account3--- | id | balance |4--- |----|---------|5--- | 1 | 1000 |6--- | 2 | 1500 |78--- ํธ๋์ญ์ 19BEGIN TRANSACTION;10SELECT * FROM account WHERE balance > 1000; -- 1500 ๋ฐํ1112--- ํธ๋์ญ์ 213BEGIN TRANSACTION;14INSERT INTO account (id, balance) VALUES (3, 1200);15COMMIT;1617-- ํธ๋์ญ์ 118SELECT * FROM account WHERE balance > 1000; -- 1500, 1200 ๋ฐํ (phantom read)19COMMIT;
- ํธ๋์ญ์
์ด ์ฌ๋ฌ Row๋ฅผ ๋ถ๋ฌ์ค๋ ํํฐ๋ง ์ฟผ๋ฆฌ๋ฅผ ์งํ ํ,
- ๋ค๋ฅธ ํธ๋์ญ์ ์์ ์ฟผ๋ฆฌ์ ์กฐ๊ฑด์ ๋ง๋ ์๋ก์ด ๋ฐ์ดํฐ๋ฅผ ์์ฑํ์๋ ๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋ค๋ฅธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋๊ฑธ ์ด์ผ๊ธฐํ๋ค.
- Serializable ํธ๋์ญ์ ์ผ๋ก ํด๊ฒฐ ๊ฐ๋ฅํ๋ค.
2. Transaction Level & Transaction Anomaly
| Dirty Read | Non-Repeatable Read | Phantom Read | |
|---|---|---|---|
| Read Uncommitted | ๊ฐ๋ฅ | ๊ฐ๋ฅ | ๊ฐ๋ฅ |
| Read Committed | ๋ถ๊ฐ๋ฅ | ๊ฐ๋ฅ | ๊ฐ๋ฅ |
| Repeatable Read | ๋ถ๊ฐ๋ฅ | ๋ถ๊ฐ๋ฅ | ๊ฐ๋ฅ |
| Serializable | ๋ถ๊ฐ๋ฅ | ๋ถ๊ฐ๋ฅ | ๋ถ๊ฐ๋ฅ |
2.1 Transaction ๋ฌธ๋ฒ
1SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;2BEGIN TRANSACTION;3--- SQL ์์4COMMIT;
3. Transaction ์ค์ต
1/*** 3) POST /posts2* post๋ฅผ ์์ฑํ๋ค3*4* A Model, B Model5* Post API -> A๋ชจ๋ธ์ ์ ์ฅํ๊ณ , B๋ชจ๋ธ์ ์ ์ฅํ๋ค.6* await repository.save(a)7* await repository.save(b)8*9* ๋ง์ฝ์ a๋ฅผ ์ ์ฅํ๋ค๊ฐ ์คํจํ๋ฉด b๋ฅผ ์ ์ฅํ๋ฉด ์๋ ๊ฒฝ์ฐ10* ์ด ๊ฒฝ์ฐ๋ฅผ ๋ง๊ธฐ ์ํด ๋ฑ์ฅํ ๊ฒ์ด Transaction11* all or nothing12*13* Transaction14* start -> ์์15* commit -> ์ ์ฅ16* rollback -> ์์๋ณต๊ตฌ17*/18@Post()19@UseGuards(AccessTokenGuard)20async postPosts(21// ์๋ต22) {23await this.postsService.createPostImage(body)24return this.postsService.createPost(userId, body)25}
4. ImageModel ๋ง๋ค๊ธฐ
์ง๊ธ์ 1๊ฐ์ ํฌ์คํธ์ 1๊ฐ์ ์ด๋ฏธ์ง์ด์ง๋ง, ์ฌ๋ฌ ๊ฐ์ ์ด๋ฏธ์ง๋ฅผ ์ฌ๋ฆด ์ ์๊ฒ ๋ณ๊ฒฝํ ๊ฒ์ด๋ค.
- ์ด๋ ํฌ์คํธ ์์ฑ, ์ด๋ฏธ์ง ์ ๋ก๋, ์ฎ๊ธฐ๊ธฐ๊น์ง Transaction์ผ๋ก ๋ฌถ์ด๋๋ฉด,
- ์ด๋ฏธ์ง์ ํฌ์คํธ ๋ ๋ค ์ ์์ฑ๋์ผ์ง๋ง ๋ก์ง์ด ์์ฑ๋๋ค.
posts.entity.ts์์ ์ด๋ฏธ์ง ํ๋๋ฅผ ๊ทธ๋ฅ ์ญ์ ํ๋ค.
- ํ๋์ ํฌ์คํธ๋ ์ฌ๋ฌ ๊ฐ์ ์ด๋ฏธ์ง๋ฅผ ๊ฐ์ง ์ ์์ผ๋,
- PostsModel์ ImageModel๊ณผ 1:M ๊ด๊ณ๋ก ๋ฌถ์ด์ฃผ๋ฉด ๋๋ค.
common/entities/image.entity.tsํ์ผ์ ๋ง๋ ๋ค.
1import { IsEnum, IsInt, IsOptional, IsString } from 'class-validator'2import { Transform } from 'class-transformer'34import { Column, Entity, ManyToOne } from 'typeorm'5import { join } from 'path'6import { BaseModel } from './base.entity'7import { PostsModel } from '../../posts/entities/posts.entity'8import { POST_PUBLIC_IMAGE_PATH } from '../const/path.const'910export enum ImageModelType {11POST_IMAGE,12}1314@Entity()15export class ImageModel extends BaseModel {16@Column({17default: 0,18})19@IsInt()20@IsOptional()21order: number2223/***24* UserModel -> ์ฌ์ฉ์ ํ๋กํ ์ด๋ฏธ์ง25* PostsModel -> ํฌ์คํธ ์ด๋ฏธ์ง26*/27@Column({28enum: ImageModelType,29})30@IsEnum(ImageModelType)31@IsString()32type: ImageModelType3334@Column()35@IsString()36@Transform(({ value, obj }) => {37// obj๋ ์ด๋ฏธ์ง ๋ชจ๋ธ์ด ์์ฑ๋์ ๋์, ํ์ฌ ๊ฐ์ฒด๋ฅผ ์๋ฏธ38if (obj.type === ImageModelType.POST_IMAGE) {39return `/${join(POST_PUBLIC_IMAGE_PATH, value)}`40} else {41return value42}43})44path: string4546@ManyToOne(type => PostsModel, post => post.images)47post?: PostsModel48}
posts ์ํฐํฐ์์ images ํ๋กํผํฐ๋ฅผ ์ถ๊ฐํ๋ค.
1// posts.entity.ts ์๋ต23@Entity()4export class PostsModel extends BaseModel {5// ์๋ต67@OneToMany(() => ImageModel, image => image.post)8images: ImageModel[]9}
app ๋ชจ๋์์ ImageModel์ ๋ฑ๋กํ๋ค.
1// app.module.ts ์๋ต2@Module({3imports: [4// ์๋ต5TypeOrmModule.forRoot({6// ์๋ต7// entitiesํด๋์ ์์ฑํ PostsModel ๊ฐ์ ธ์ค๊ธฐ8entities: [PostsModel, UsersModel, ImageModel],9synchronize: true,10}),11UsersModule,12AuthModule,13CommonModule,14],15// ์๋ต16})
- ๊ฐํน ์ด๋ ๊ฒ ํ์์๋ ์๋๋ ๊ฒฝ์ฐ๊ฐ ์๋๋ฐ,
- ๊ทธ๋๋ ์๋ฒ๋ฅผ ๊บผ์ฃผ๊ณ , dist(build๋๋ ์ค์ ๋ฐฐํฌํด๋)๋ฅผ ์ญ์ ํ๋ค๊ฐ ๋ค์ ์์ํ๋ฉด ๋๋ค.
5. ImageModel ์์ฑ ๋ก์ง
posts/const/default-post-find-options.const.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { FindManyOptions } from 'typeorm'2import { PostsModel } from '../entities/posts.entity'34export const DEFAULT_POST_FIND_OPTIONS: FindManyOptions<PostsModel> = {5relations: {6author: true,7images: true,8},9}
image/dto/create-image.dto.ts ํ์ผ์ ๋ง๋ ๋ค.
1import { PickType } from '@nestjs/mapped-types'2import { ImageModel } from 'src/common/entities/image.entity'34export class CreatePostImageDto extends PickType(ImageModel, [5'path', //6'post',7'order',8'type',9]) {}
posts/dto/create-post.dto.ts ํ์ผ์ ์์ ํ๋ค.
1import { IsOptional, IsString } from 'class-validator'23import { PickType } from '@nestjs/mapped-types'4import { PostsModel } from '../entities/posts.entity'56export class CreatePostDto extends PickType(PostsModel, ['title', 'content']) {7@IsString({8each: true, // ๋ฆฌ์คํธ ๊ฐ๋ณ ์์๋ง๋ค string์ผ๋ก ํ ์ง9})10@IsOptional()11images: string[] = []12}
posts ์ปจํธ๋กค๋ฌ์ postPosts()๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค.
1// posts.controller.ts ์๋ต2@Post()3@UseGuards(AccessTokenGuard)4async postPosts(5@User('id') userId: number,6@Body() body: CreatePostDto,7// @Body('title') title: string,8// @Body('content') content: string,9// ๊ธฐ๋ณธ๊ฐ์ true๋ก ์ค์ ํ๋ ํ์ดํ10// @Body('isPublic', new DefaultValuePipe(true)) isPublic: boolean,11) {12const post = await this.postsService.createPost(userId, body)13for (let i = 0; i < body.images.length; i++) {14await this.postsService.createPostImage({15post,16order: i,17path: body.images[i],18type: ImageModelType.POST_IMAGE,19})20}21return this.postsService.getPostById(post.id)22}
posts ์๋น์ค๋ฅผ ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค.
1// ์๋ต2@Injectable()3export class PostsService {4constructor(5@InjectRepository(PostsModel)6private readonly postsRepository: Repository<PostsModel>,7@InjectRepository(ImageModel)8private readonly imageRepository: Repository<ImageModel>,9private readonly commonService: CommonService,10private readonly configService: ConfigService,11) {}1213async getAllPosts() {14return this.postsRepository.find({ ...DEFAULT_POST_FIND_OPTIONS })15}1617async generatePosts(userId: number) {18for (let i = 0; i < 100; i++) {19await this.createPost(userId, {20title: `์์๋ก ์์ฑ๋ ํฌ์คํธ ์ ๋ชฉ ${i}`,21content: `์์๋ก ์์ฑ๋ ํฌ์คํธ ๋ด์ฉ ${i}`,22images: [],23})24}25}2627async paginatePosts(dto: PaginatePostDto) {28return this.commonService.paginate(29dto, //30this.postsRepository,31{ ...DEFAULT_POST_FIND_OPTIONS },32'posts',33)34}3536async getPostById(id: number) {37const post = await this.postsRepository.findOne({38...DEFAULT_POST_FIND_OPTIONS,39// PostsModel์ id๊ฐ ์ ๋ ฅ๋ฐ์ id์ ๊ฐ์์ง ํํฐ๋ง40where: {41id,42},43})44// ์๋ต45}4647async createPost(authorId: number, postDto: CreatePostDto) {48const post = this.postsRepository.create({49author: {50id: authorId,51},52...postDto,53images: [],54likeCount: 0,55commentCount: 0,56})57const newPost = await this.postsRepository.save(post)58return newPost59}6061async createPostImage(dto: CreatePostImageDto) {62// ์๋ต6364// save65const result = await this.imageRepository.save({66...dto,67})6869// ํ์ผ ์ฎ๊ธฐ๊ธฐ70await promises.rename(tempFilePath, publicFilePath)7172return result73}74// ์๋ต75}
posts ๋ชจ๋์ ์ถ๊ฐํ ImageModel์ ์ ์ฉ์ํฌ ์ ์๋๋ก importํ๋ค.
1// posts.module.ts ์๋ต2@Module({3imports: [4/*** ๋ชจ๋ธ์ ํด๋นํ๋ repostory๋ฅผ ์ฃผ์ ==> forFeature5* repository : ํด๋น ๋ชจ๋ธ์ ๋ค๋ฃฐ ์ ์๊ฒ ํด์ฃผ๋ ํด๋์ค6*/7TypeOrmModule.forFeature([8PostsModel, //9ImageModel,10]),11AuthModule,12UsersModule,13CommonModule,14],15// ์๋ต16})17export class PostsModule {}
6. Transaction ์์
์ด๋ฏธ์ง ์ ๋ก๋๊ฐ ์ ๋๋ก ์๋ฌ๊ฑฐ๋, ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด, ํฌ์คํธ๊ฐ ์์ฑ๋๋ฉด ์๋๋ค.
- ์ด๋ฏธ์ง ์ ๋ก๋ ์๋ฌ ๋ฑ์ ์๋ฌ ์ํฉ์ด ๋ฐ์ํ๋ฉด ํฌ์คํธ ์์ฑ๋ ์๋๊ฒ ๋ง๋ค์ด์ผ ํ๋ค.
- ์ด ๋ก์ง์ Transaction์ผ๋ก ๋ฌถ์ด์ค์ 1๊ฐ ์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด, ๋ชจ๋ ์๋๊ฒ ํด์ผ ํ๋ค.
posts ์ปจํธ๋กค๋ฌ์ postPosts()๋ฅผ ์์ ํ๋ค.
1// posts.controller.ts ์๋ต2@Post()3@UseGuards(AccessTokenGuard)4async postPosts(5@User('id') userId: number,6@Body() body: CreatePostDto,7// @Body('title') title: string,8// @Body('content') content: string,9// ๊ธฐ๋ณธ๊ฐ์ true๋ก ์ค์ ํ๋ ํ์ดํ10// @Body('isPublic', new DefaultValuePipe(true)) isPublic: boolean,11) {12// (1) ํธ๋์ญ์ ๊ณผ ๊ด๋ จ๋ ๋ชจ๋ ์ฟผ๋ฆฌ๋ฅผ ๋ด๋นํ ์ฟผ๋ฆฌ ๋ฌ๋(qr)๋ฅผ ์์ฑํ๋ค.13const qr = this.dataSource.createQueryRunner()1415// (2) ์ฟผ๋ฆฌ ๋ฌ๋์ ์ฐ๊ฒฐํ๋ค.16await qr.connect()17/*** ์ฟผ๋ฆฌ ๋ฌ๋์์ ํธ๋์ญ์ ์ ์์ํ๋ค.18* ์ด ์์ ๋ถํฐ ๊ฐ์ ์ฟผ๋ฆฌ ๋ฌ๋๋ฅผ ์ฌ์ฉํ๋ฉด19* ํธ๋์ญ์ ์์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ก์ ์ ์คํํ๋ค.20*/21await qr.startTransaction()2223// ๋ก์ง์คํ24try {25const post = await this.postsService.createPost(userId, body)26for (let i = 0; i < body.images.length; i++) {27await this.postsService.createPostImage({28post,29order: i,30path: body.images[i],31type: ImageModelType.POST_IMAGE,32})33}34await qr.commitTransaction()35await qr.release()36return this.postsService.getPostById(post.id)37} catch (e) {38// ์ด๋ค ์๋ฌ๋ ์๋ฌ๊ฐ ๋์ ธ์ง๋ฉด, ํธ๋์ญ์ ์ ์ข ๋ฃํ๊ณ ์๋ ์ํ๋ก ๋๋๋ฆฐ๋ค.39await qr.rollbackTransaction()40await qr.release()41}42}
posts ์๋น์ค๋ฅผ ์์ ํ๋ค.
1// posts.service.ts ์๋ต2getRepository(qr?: QueryRunner) {3return qr4? qr.manager.getRepository<PostsModel>(PostsModel) //5: this.postsRepository6}78async createPost(authorId: number, postDto: CreatePostDto, qr?: QueryRunner) {9const repository = this.getRepository(qr)1011const post = repository.create({12author: {13id: authorId,14},15...postDto,16images: [],17likeCount: 0,18commentCount: 0,19})20const newPost = await repository.save(post)21return newPost22}
7. Transaction ์ ์ฉ ๋ฐ ํ ์คํธ
posts/image/images.service.ts ํ์ผ์ ๋ง๋ ๋ค.
- posts ์๋น์ค์ createPostImage()๋ฅผ ์๋ผ๋ด์ images ์๋น์ค์ ๋ณต๋ถํ๋ค.
1import { BadRequestException, Injectable } from '@nestjs/common'2import { InjectRepository } from '@nestjs/typeorm'3import { QueryRunner, Repository } from 'typeorm'4import { basename, join } from 'path'56import { promises } from 'fs'7import { CreatePostImageDto } from './dto/create-image.dto'8import { ImageModel } from 'src/common/entities/image.entity'9import { POST_IMAGE_PATH, TEMP_FOLDER_PATH } from 'src/common/const/path.const'1011@Injectable()12export class PostsImagesService {13constructor(14@InjectRepository(ImageModel)15private readonly imageRepository: Repository<ImageModel>,16) {}1718getRepository(qr?: QueryRunner) {19return qr20? qr.manager.getRepository<ImageModel>(ImageModel) //21: this.imageRepository22}2324async createPostImage(dto: CreatePostImageDto, qr?: QueryRunner) {25const repository = qr.manager.getRepository<ImageModel>(ImageModel)2627// dto์ ์ด๋ฏธ์ง ์ด๋ฆ์ ๊ธฐ๋ฐ์ผ๋ก ํ์ผ ๊ฒฝ๋ก๋ฅผ ์์ฑํ๋ค28const tempFilePath = join(TEMP_FOLDER_PATH, dto.path)2930try {31/*** promises์ fs ๋ชจ๋์ import32* ํ์ผ์ด ์กด์ฌํ๋์ง ํ์ธ33* ๋ง์ฝ์ ์กด์ฌํ์ง ์๋๋ค๋ฉด ์๋ฌ๋ฅผ ๋์ง34*/35await promises.access(tempFilePath)36} catch (e) {37throw new BadRequestException('์กด์ฌํ์ง ์๋ ์์ ํ์ผ์ ๋๋ค!')38}3940/*** ํ์ผ์ ์ด๋ฆ๋ง ๊ฐ์ ธ์ค๊ธฐ41* /USers/aaa/bbb/ccc/asdf.jpg -> asdf.jpg42*/43const fileName = basename(tempFilePath)4445/*** ์๋ก ์ด๋ํ ํฌ์คํธ ํด๋์ ๊ฒฝ๋ก + ์ด๋ฏธ์ง์ ์ด๋ฆ46* {ํ๋ก์ ํธ๊ฒฝ๋ก}/public/posts/asdf.jpg47*/48const publicFilePath = join(POST_IMAGE_PATH, fileName)4950// save51const result = await repository.save({52...dto,53})5455// ํ์ผ ์ฎ๊ธฐ๊ธฐ56await promises.rename(tempFilePath, publicFilePath)5758return result59}60}
์ด์ ๋ posts ์ปจํธ๋กค๋ฌ๋ images ์๋น์ค์์ ๊ธฐ๋ฅ์ ๊ฐ์ ธ์จ๋ค.
1// ์๋ต23@Controller('posts')4export class PostsController {5constructor(6private readonly postsService: PostsService,7private readonly postsImagesService: PostsImagesService, // ์ถ๊ฐ8private readonly dataSource: DataSource,9) {}10// ์๋ต11@Post()12@UseGuards(AccessTokenGuard)13async postPosts(14// ์๋ต15try {16const post = await this.postsService.createPost(userId, body, qr)17// throw new InternalServerErrorException('์ผ๋ถ๋ฌ ์๋ฌ ๋ฐ์ ํ ์คํธ')18for (let i = 0; i < body.images.length; i++) {19await this.postsImagesService.createPostImage(20{21post,22order: i,23path: body.images[i],24type: ImageModelType.POST_IMAGE,25},26qr,27)28}29await qr.commitTransaction()30await qr.release()31return this.postsService.getPostById(post.id)32} catch (e) {33// ์ด๋ค ์๋ฌ๋ ์๋ฌ๊ฐ ๋์ ธ์ง๋ฉด, ํธ๋์ญ์ ์ ์ข ๋ฃํ๊ณ ์๋ ์ํ๋ก ๋๋๋ฆฐ๋ค.34await qr.rollbackTransaction()35await qr.release()36throw new InternalServerErrorException('์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค.')37}38}39}
posts ๋ชจ๋์ ์ปจํธ๋กค๋ฌ, ์๋น์ค์์ ์ฌ์ฉํ ์ ์๋๋ก PostsImagesService์ ์ถ๊ฐํ๋ค.
1// ์๋ต2@Module({3// ์๋ต4providers: [PostsService, PostsImagesService],5})6export class PostsModule {}