TypeORM - boostcamp-2020/Project12-B-Slack-Web GitHub Wiki
TypeORM์ ๋์ ํ๊ฒ ๋ ์ด์
ORM(Object Relational Mapping)์ ๊ฐ์ฒด์ ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ๋ฐ์ดํฐ๋ฅผ ์๋์ผ๋ก ๋งคํํด์ฃผ๋ ๊ฒ์ผ๋ก, ๊ฐ์ฒด ์งํฅ์ ์ธ ์ฝ๋๋ก ์ธํด ์ง๊ด์ ์ด๊ณ ๋น์ฆ๋์ค ๋ก์ง์ ์ง์คํ ์ ์์ต๋๋ค.
Node์์ ์ง์ํ๋ ๋ค์ํ ORM ๋ชจ๋ ์ค์์ ์ด๋ค ORM์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข์์ง ๊ณ ๋ฏผํ๋ค๊ฐ ์ ํฌ๋ ๊ทธ์ค TypeORM์ ๋์ ํ๊ธฐ๋ก ํ์ต๋๋ค.
ํ๋ก์ ํธ์ ๊ธฐ๋ณธ ์ธ์ด๋ก TypeScript๋ฅผ ์ฌ์ฉํ๋๋ฐ, TypeORM์ TypeScript๋ฅผ ์ง์ํ๋ฉฐ, ๋ชจ๋ธ ์ ์๋ฅผ ์ ๋๋ก ํ๋ฉด ํ์ ์ ์ ํ๋ ์ฅ์ ์ ์ต๋ํ์ผ๋ก ์ป์ ์ ์์ต๋๋ค. ๋ํ, ๋ณต์กํ ๋ชจ๋ธ ๊ฐ์ ๊ด๊ณ๋ฅผ ํ์ฑํ ์ ์๊ณ , Validation์ด ๊ฐํธํด์ ์์ฐ์ฑ๊ณผ ์ ๋ขฐ์ฑ์ด ๋์์ง๋ค๋ ์ฅ์ ๋ ์์ต๋๋ค.
Active Record vs Data Mapper
TypeORM์ ์ด์ฉํ๋ฉด Active Record ํจํด๊ณผ Data Mapper ํจํด์ผ๋ก ๊ฐ๋ฐํ ์ ์์ต๋๋ค.
| Active Record ํจํด
Active Record ์ ๊ทผ ๋ฐฉ์์ ์ฌ์ฉํ๋ค๋ฉด ๋ชจ๋ธ ๋ด์ ๋ชจ๋ ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ์ ์ํ๊ณ , ๋ชจ๋ธ ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ์ฒด๋ฅผ save, remove ๋ฐ load ํฉ๋๋ค. ๊ฐ๋จํ ๋งํด์ Active Record ํจํด์ ๋ชจ๋ธ ๋ด์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์์ธ์คํ๋ ์ ๊ทผ ๋ฐฉ์์ ๋๋ค.
import {BaseEntity, Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
isActive: boolean;
static findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany();
}
}
| Data Mapper ํจํด
Data Mapper ์ ๊ทผ ๋ฐฉ์์ repository๋ผ๋ ๋ณ๋์ ํด๋์ค์ ๋ชจ๋ ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ์ ์ํ๊ณ repository๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐ์ฒด๋ฅผ save, remove ๋ฐ load ํฉ๋๋ค. ๊ฐ๋จํ ๋งํด Data Mapper๋ ๋ชจ๋ธ์ด ์๋ repository ๋ด์์ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๊ทผํ๋ ๋ฐฉ์์ ๋๋ค.
import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
isActive: boolean;
}
first name๊ณผ last name์ ์ด์ฉํด์ ์ฌ์ฉ์๋ฅผ ๋ฐํํ๋ ํจ์๋ฅผ ๋ง๋ค๊ณ ์ถ๋ค๋ฉด ์ด๋ฌํ ๊ธฐ๋ฅ์ custom repository์ ๋ง๋ค ์ ์์ต๋๋ค.
import {EntityRepository, Repository} from "typeorm";
import {User} from "../entity/User";
@EntityRepository()
export class UserRepository extends Repository<User> {
findByName(firstName: string, lastName: string) {
return this.createQueryBuilder("user")
.where("user.firstName = :firstName", { firstName })
.andWhere("user.lastName = :lastName", { lastName })
.getMany();
}
}
| Active Record vs Data Mapper
Active Record ์ ๊ทผ ๋ฐฉ์์ ๊ฐ๋จํ๊ฒ ์ฌ์ฉํ ์ ์์ผ๋ฉฐ, ๊ท๋ชจ๊ฐ ์์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ Data Mapper ์ ๊ทผ ๋ฐฉ์์ ๊ท๋ชจ๊ฐ ํฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ํฉํ๊ณ ์ ์ง๋ณด์ํ๋ ๋ฐ ํจ๊ณผ์ ์ ๋๋ค.
Slack Clone Project์ธ black์ ๊ตฌํํ๋ ๋ฐ ์์ด์ ๊ท๋ชจ๊ฐ ํฐ ํ๋ก์ ํธ์ด๊ธฐ ๋๋ฌธ์ ๋ชจ๋ ์ฟผ๋ฆฌ ๋ฉ์๋๊ฐ ๋ชจ๋ธ ๋ด์ ์ ์๋๋ค๋ฉด ๋ชจ๋ธ์ ํฌ๊ธฐ๊ฐ ๋๋ฌด ์ปค์ง ์๋ ์๊ฒ ๋ค๋ ์๊ฐ์ด ๋ค์์ต๋๋ค. ๋ํ, ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ๋ณ๋์ repository์์ ๊ด๋ฆฌํ๋ค๋ฉด ์ถํ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๊ฑฐ๋ ์ ์ง ๋ณด์ํ๊ธฐ ์ข๋ค๊ณ ํ๋จํด์ Data Mapper ํจํด์ ์ฌ์ฉํ๊ธฐ๋ก ํ์ต๋๋ค.
๋ฐ๋ผ์ Server ๋๋ ํฐ๋ฆฌ ๊ตฌ์กฐ๋ฅผ model
- repository
- service
- controller
- router
๋ก ์ ํ์ผ๋ฉฐ, model
๋๋ ํฐ๋ฆฌ์๋ TypeORM์ Model์ ์ ์ํ๊ณ , repository
๋๋ ํฐ๋ฆฌ์๋ service
์์ ์ฌ์ฉ๋ ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ์ ์ํ๋๋ก ํ์ต๋๋ค.
server
โโโ src
โโโ common
โ โโโ config
โ โโโ constants
โ โโโ error
โ โโโ middleware
โ โโโ utils
โโโ controller
โโโ model
โโโ repository
โโโ router
โโโ seeds
โโโ service
โโโ socket
โโโ event
โโโ handler
โโโ middleware
Relation
TypeORM์์ Relation์ ์ฌ์ฉํด์ ๊ด๋ จ๋ Entity์์ ์์
์ ์ฝ๊ฒ ํ ์ ์์ต๋๋ค.
โ one-to-one โ @OneToOne
โ many-to-one โ @ManyToOne
โ one-to-many โ @OneToMany
โ many-to-many โ @ManyToMany
| ex) User์ Message ์ฌ์ด์ ๊ด๊ณ ์ ์
User๋ ์ฌ๋ฌ Message๋ฅผ ๊ฐ์ง ์ ์์ต๋๋ค.
/* server\src\model\user.ts */
@Entity({ name: 'user' })
export default class User {
// ์๋ต
@OneToMany(() => Message, (message) => message.user)
messages: Message[];
// ์๋ต
}
User Model์์๋ Message Model๊ณผ์ ๊ด๊ณ๋ฅผ one-to-many๋ก ์ ์ํ๊ณ , messages์ ํ์
์ Message Model์ ๋ฐฐ์ด๋ก ์ ํ์ต๋๋ค.
/* server\src\model\message.ts */
@Entity({ name: 'message' })
export default class Message {
// ์๋ต
@ManyToOne(() => User, (user) => user.userId)
@JoinColumn({ name: 'userId' })
user: User;
// ์๋ต
}
Message Model์์๋ User Model๊ณผ์ ๊ด๊ณ๋ฅผ many-to-one์ผ๋ก ์ ์ํ๊ณ , user์ ํ์ ์ User Model๋ก ์ ํ์ต๋๋ค.
Transaction
Service Logic์ ์ํํ๋ค๊ฐ ์ค๊ฐ์ ์คํจ๋ฅผ ํ์ ๊ฒฝ์ฐ Rollback์ ํ๊ธฐ ์ํด์๋ Service Logic์ ํ๋์ ํธ๋์ญ์
์ผ๋ก ๊ฐ์ ธ๊ฐ์ผ ํ๋ค๊ณ ์๊ฐํ์ต๋๋ค. ๋ฐ๋ผ์ typeorm-transactional-cls-hooked
๋ชจ๋์ ์ฌ์ฉํด์ ํธ๋์ญ์
์ ์ ์ฉํ์ต๋๋ค.
typeorm-transactional-cls-hooked
๋ ์๋ก ๋ค๋ฅธ Repository ๋ฐ Service Method ๊ฐ์ ํธ๋์ญ์
์ ์ฒ๋ฆฌํ๊ณ ์ ํํ๋ TypeORM์ฉ ํธ๋์ญ์
๋ฉ์๋ ๋ฐ์ฝ๋ ์ดํฐ์
๋๋ค.
/* server\src\application.ts */
import { initializeTransactionalContext, patchTypeORMRepositoryWithBaseRepository } from 'typeorm-transactional-cls-hooked';
// ์๋ต
export default class Application {
async initDatabase() {
initializeTransactionalContext();
patchTypeORMRepositoryWithBaseRepository();
await createConnection();
}
// ์๋ต
`typeorm-transactional-cls-hooked` ๋ชจ๋์ installํ๊ณ , application.ts์ ํธ๋์ญ์
๊ด๋ จ ์ค์ ์ ์ถ๊ฐํด ํธ๋์ญ์
์ ์ ์ฉํ ์ ์๋๋ก ํ์ต๋๋ค.
/* server\src\service\chatroom-service.ts */
@Transactional()
async createChannel({ userId, title, description, isPrivate }) {
const user = await this.userRepository.findOne(userId);
const chatroom = await this.chatroomRepository.findByTitle(title);
if (chatroom || !user) {
throw new BadRequestError();
}
const newChatroom = await this.saveChatroom({ title, description, isPrivate, chatType: ChatType.Channel });
await this.saveUserChatroom({ sectionName: DefaultSectionName.Channels, user, chatroom: newChatroom });
return newChatroom.chatroomId;
}
Service Logic์์๋ ํจ์์ @Transactional()
๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ถ์ฌ์ ํธ๋์ญ์
์ค์ ์ ํ์ต๋๋ค.
Validation
์ฌ์ฉ์๊ฐ ๋ฐ์์ํฌ ์ ์๋ ์ค๋ฅ๋ฅผ ๋ฐฉ์งํ๊ณ , ์ฌ๋ฐ๋ฅธ ์ ๊ทผ ๋ฐฉ๋ฒ์ ์๋ ค์ฃผ๊ธฐ ์ํด ์ฌ์ฉ์๊ฐ ์ ๋ ฅํ ๋ฐ์ดํฐ์ ๋ํด์ ๊ฒ์ฆํ๋ ๊ณผ์ ์ ์ค์ํ๋ค๊ณ ์๊ฐํฉ๋๋ค.
TypeORM์์๋ Model์ ์ ์ํ ๋ class-validator
๋ชจ๋์ ํ์ฉํ์ฌ ๊ฐ Column์ด ๊ฐ์ ธ์ผํ ๋ฐ์ดํฐ์ ํํ๋ ๊ธธ์ด, ์กฐ๊ฑด๋ค์ ๋ฐ์ฝ๋ ์ดํฐ ํ์์ผ๋ก ์ถ๊ฐํ๋ฉด validate ํจ์๋ฅผ ์ฌ์ฉํด์ ๋ฐ์ดํฐ์ ๋ํ ๊ฒ์ฆ์ ํ ์ ์์ต๋๋ค.
/* server\src\model\reaction.ts */
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, OneToMany, DeleteDateColumn } from 'typeorm';
import MessageReaction from '@model/message-reaction';
import ReplyReaction from '@model/reply-reaction';
import { IsString } from 'class-validator';
@Entity({ name: 'reaction' })
export default class Reaction {
@PrimaryGeneratedColumn()
reactionId: number;
@Column({ length: 30, unique: true })
@IsString()
title: string;
@Column({ length: 100 })
@IsString()
emoji: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@DeleteDateColumn()
deletedAt: Date;
@OneToMany(() => MessageReaction, (messageReaction) => messageReaction.reaction)
messageReactions: MessageReaction[];
@OneToMany(() => ReplyReaction, (replyReaction) => replyReaction.reaction)
replyReactions: ReplyReaction[];
}
Reaction Model์ ๊ฒฝ์ฐ title๊ณผ emoji ์์ฑ์ด string ํ์์ด์ด์ผ ํ๊ธฐ ๋๋ฌธ์ @IsString()
๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋ถ์์ต๋๋ค.
/* server\src\common\utils\validator.ts */
import { validate } from 'class-validator';
import BadRequestError from '@error/bad-request-error';
const validator = async (reqType: object) => {
const errors = await validate(reqType);
if (errors.length > 0) {
throw new BadRequestError();
}
};
export default validator;
๋ํ, ๊ณตํต์ผ๋ก ์ฌ์ฉ๋๋ validator ํจ์๋ฅผ ๊ตฌํํด์ ์ค์ ํ ๋ฐ์ดํฐ ์กฐ๊ฑด์ ๋ง๋์ง ํ์ธํ๊ณ , ์ผ์นํ์ง ์๋๋ค๊ณ ํ๋ฉด ์ค๋ฅ๋ฅผ ๋ฐํํด Error Handler ์ชฝ์์ ํด๋น ์ค๋ฅ๋ฅผ ์ฒ๋ฆฌํ๋๋ก ๊ตฌํํ์ต๋๋ค.
๋๋ ์
TypeORM์ ํ์ฉํ๋ฉด์ ๊ฐ์ฅ ๋จผ์ ๋๊ผ๋ ์ ์ TypeScript์ ๊ถํฉ์ด ์ ๋ง๋๋ค๋ ๊ฒ์ ๋๋ค. ๋ชจ๋ธ์ ์ ์ํ ๋ ํ์ ์ ์ ํจ์ผ๋ก์จ ํด๋น ์์ฑ์ด ์ด๋ค ํ์ ์ ๊ฐ์ก๋์ง ์ฝ๊ฒ ํ์ธํ ์ ์๋ ๋ฑ TypeScript๋ฅผ ํ์ฉํ๋ ์ฅ์ ์ ์ต๋ํ ๊ฐ์ ธ๊ฐ ์ ์์์ต๋๋ค. ๋ํ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ด์ฉํด ๋ชจ๋ธ ์ฝ๋ ์์ฑํ๊ธฐ ๋๋ฌธ์ ๊ฐ๋ ์ฑ๋ ์ข๋ค๊ณ ์๊ฐํ์ต๋๋ค.
Validation ํ๋ ๋ถ๋ถ์์๋ ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ์ด์ฉํด ๊ฐ๋จํ๊ฒ ์ค์ ํ ์ ์์ด ์ ๋ขฐ์ฑ์ด ๋์์ง๊ณ ์์ฐ์ฑ์ด ์ข์์ก์ผ๋ฉฐ, ํ๋ก์ ํธ ์ค๊ฐ์ DB ์ค๊ณ๊ฐ ๋ณ๊ฒฝ๋ ๋ถ๋ถ์ด ์์๋๋ฐ, ๋ชจ๋ธ ์ค์ ์ ํตํด ๋ณ๊ฒฝ ์ ๋ณด๋ฅผ DB์ ์ฝ๊ฒ ๋ฐ์ํ ์ ์๋ค๋ ORM์ ์ฅ์ ๋ ๋๋ ์ ์์์ต๋๋ค.
๋ง์น๋ฉฐ
ํ๋ก์ ํธ์ TypeORM์ ๋์ ํด DB ์ค๊ณ๋ฅผ ๋ฐํ์ผ๋ก ๋ชจ๋ธ์ ์ ์ํ๊ณ ๊ฐ ๋ชจ๋ธ ์ฌ์ด์ ๊ด๊ณ๋ฅผ ์ค์ ํ๋ฉด์ ORM๊ณผ ์กฐ๊ธ ๋ ์นํด์ง ์ ์์๋ ์ข์ ๊ฒฝํ์ด์์ต๋๋ค.
ํ์ง๋ง ์ฟผ๋ฆฌ ๋ฉ์๋๋ฅผ ์์ฑํ๋ ๋ฐ ์์ด์ ์๋ง์ join์ ํ ํ์๊ฐ ์์๊ณ , TypeORM ์ด์ฉ์ด ์ต์ํ์ง ์์ ์ฟผ๋ฆฌ๋ฅผ ํ ๋ฒ์ด ์๋๋ผ ์ฌ๋ฌ ๋ฒ์ ๋๋ ์ ์ฒ๋ฆฌํ๋ ๊ฒฝ์ฐ๋ ๋ฐ์ํด์ ์ด์ ๊ดํ ๊ณต๋ถ๊ฐ ๋ ํ์ํ๋ค๊ณ ๋๊ผ์ต๋๋ค.
์ด๋ฒ์ TypeORM์ ์ฒ์ ์ฌ์ฉํด๋ดค๋๋ฐ, TypeORM์ ๋ ์ต์ํด์ง๋ฉด ๋ค๋ฅธ ORM๋ ์ฌ์ฉํด๋ณด๋ฉด์ ์ฅ๋จ์ ์ ์ง์ ๋น๊ตํด๋ณด๋ฉด ์ข๊ฒ ๋ค๊ณ ์๊ฐํ์ต๋๋ค.