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๋„ ์‚ฌ์šฉํ•ด๋ณด๋ฉด์„œ ์žฅ๋‹จ์ ์„ ์ง์ ‘ ๋น„๊ตํ•ด๋ณด๋ฉด ์ข‹๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ