๐ก 08. ์ค์ ๊ฐ์ด๋ โ ๋ฐฐ์ด ๊ฒ๋ค์ ๋ชจ๋ ์ฎ์ด '์ง์ง' API ๋ง๋ค๊ธฐ
๐ ๊ฐ์
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ชจ๋, ์ปจํธ๋กค๋ฌ, ์๋น์ค, Drizzle, Zod๋ฅผ ์ด๋์ํ์ฌ ์ค์ ์๋ํ๋ '์ฃผ์ ํฌํธํด๋ฆฌ์ค ๊ด๋ฆฌ API'๋ฅผ ๋ฐ๋ฐ๋ฅ๋ถํฐ ์์ฑํด ๋ด ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ 1๋จ๊ณ: ๋ผ๋ ์ค๊ณ (DB ์คํค๋ง) ๐ก
- ๐งฉ 2๋จ๊ณ: ์๋ฒฝํ DTO ์์ฑ (SSOT) ๐ข
- ๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ์ปจํธ๋กค๋ฌ์ ์๋น์ค ์ฎ์ด์ CRUD ์์ฑํ๊ธฐ
- ๐ผ ๋ฒ ์คํธ ํ๋ํฐ์ค์ ์ค๋ฌด ํ
- ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐๏ธ ์นํธ์ํธ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 25๋ถ (์ง์ ์ฝ๋ฉํ๋ฉฐ ๋ฐ๋ผ์ค๋ฉด 1์๊ฐ)
๐งณ ์ ์ ์ง์ ์ฒดํฌ๋ฆฌ์คํธ
- 02์ฅ์ ์ปจํธ๋กค๋ฌ, 03์ฅ์ ์๋น์ค ๊ธฐ๋ณธ ํํ๋ฅผ ๊ธฐ์ตํ๋ค.
- 04์ฅ์ DTO ๊ฐ๋ ๊ณผ 05์ฅ์ Drizzle ORM ๊ธฐ๋ณธ ์ฟผ๋ฆฌ ๋ฌธ๋ฒ์ ํ์ด๋ดค๋ค.
- 06์ฅ์ ๊ฐ๋(
JwtAuthGuard)๊ฐ ์ด๋ค ์ญํ ์ ํ๋์ง ์๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
๋ชฉํ ๊ธฐ๋ฅ ์ ์ โ DB ํ
์ด๋ธ ์ค๊ณ โ DTO ์ฐ์ด๋ด๊ธฐ โ ๋น์ฆ๋์ค ๋ก์ง(์๋น์ค) ์์ฑ โ ์ปจํธ๋กค๋ฌ๋ก ๊ธธ ๋ซ๊ธฐ(์ต์ข
์กฐ๋ฆฝ)
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
- ๋จ์ผ ์ง์ค ๊ณต๊ธ์(SSOT) ํจํด์ด ์ค๋ฌด์์ ์ผ๋ง๋ ์ฝ๋๋ฅผ ์ค์ฌ์ฃผ๋์ง ์ฒด๊ฐํ ์ ์๋ค.
- ๋ด ํฌํธํด๋ฆฌ์ค(๊ฐ์ธ ์์ฐ)๋ฅผ ๋ณดํธํ๋ ๋ผ์ฐํ ๊ธฐ๋ฒ์ ์ค๊ณํ ์ ์๋ค.
- ํผ์์ ์๋ก์ด ๊ธฐ๋ฅ(์: ๋๊ธ ๊ธฐ๋ฅ, ์ข์์ ๊ธฐ๋ฅ)์ ์ค๊ณ๋ถํฐ API ๊ตฌํ๊น์ง ์ฒ์๋ถํฐ ๋๊น์ง ํผ์ ํด๋ผ ์ ์๋ค.
๐ค ์ ์์์ผ ํ๋๊ฐ
์ง๊ธ๊น์ง ์ฐ๋ฆฌ๋ ๋ ๊ณ ๋ธ๋ก์ '๋ถํ'๋ค์ด ๊ฐ๊ฐ ์ด๋ค ์ญํ ์ ํ๋์ง ๋ฐ๋ก๋ฐ๋ก ๋ผ์ด๋๊ณ ๋ฐฐ์ ์ด.
์์ง(์๋น์ค), ๋ฐํด(DB), ์ด์ ๋(์ปจํธ๋กค๋ฌ) ์๋ ์๋ฆฌ๋ฅผ ์๋ ๊ฒ๊ณผ, ๋ง์ ๋ถํ์ ์น ๋ค ๋ชจ์๋๊ณ "์, ์ด์ ๊ตด๋ฌ๊ฐ๋ ๋นํ๊ธฐ๋ฅผ ์ง์ ์กฐ๋ฆฝํด ๋ด!" ๋ผ๊ณ ํ๋ ๊ฑด ์์ ํ ๋ค๋ฅธ ์์ญ์ด์ผ.
์ด ์ค์ ๊ฐ์ด๋๋ฅผ ๋ฐ๋ผ๊ฐ๋ฉด์ ํํธํ๋ ๋จธ๋ฆฟ์ ์ง์๋ค์ ํ๋์ ๋งค๋๋ฌ์ด ์
๋ฌด ํ๋ฆ(Workflow) ์ผ๋ก ์์ถ(Compile) ์ง์ผ์ผ ๋น๋ก์ ๋ด ๊ฒ์ด ๋ผ.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ง 5์ด์๊ฒ ์ค๋ช ํ๋ค๋ฉด?
์๋์ฐจ ๊ณต์ฅ์์ ๋ฉ์ง ์๋์ฐจ๋ฅผ ์กฐ๋ฆฝํ๋ ๊ณผ์ ์ ์์ํด ๋ณผ๊น?
- ๋ผ๋ ๋ง๋ค๊ธฐ (์คํค๋ง): ์๋์ฐจ์ ์ฒ ์ ํ๋ ์ ๋์์ธ์ ๊ทธ๋ฆฐ๋ค.
- ์ฌ์ฉ ์ค๋ช ์ ์ฐ์ด๋ด๊ธฐ (SSOT DTO): ํ๋ ์ ๋์์ธ๋ง ๋ณด๊ณ ์ปดํจํฐ(Zod)๊ฐ ์์์ "์ด ์ฐจ์ ์ต๋ ์ ์์ 4๋ช " ์ด๋ผ๋ ์ฌ์ฉ ์ค๋ช ์๋ฅผ ์๋์ผ๋ก ์ฐ์ด๋ธ๋ค.
- ์์ง ๋ฌ๊ธฐ (์๋น์ค): ํธ๋ค์ ๊บพ์ผ๋ฉด ๋ฐํด๊ฐ ๋์๊ฐ๊ฒ ์ฒ ์ฌ๋ฅผ ์ฐ๊ฒฐํ๋ค.
- ๋ฒํผ ๋ซ๊ธฐ (์ปจํธ๋กค๋ฌ): ์ด์ ์๊ฐ ๋ฐ์์ ๋๋ฅผ ์ ์๊ฒ ํธ๋ค๊ณผ ํ๋ฌ์ ์์๊ฒ ๋ฐฐ์นํ๊ณ , ์ด์ ๋ฉดํ(๊ฐ๋)๊ฐ ์๋ ์ฌ๋๋ง ํ ์ ์๊ฒ ์ด์ ๊ตฌ๋ฉ์ ํ๋ค.
๐งฉ 1๋จ๊ณ: ๋ผ๋ ์ค๊ณ (DB ์คํค๋ง) ๐ก
๐ฏ **์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:**Drizzle ์คํค๋ง๋ฅผ ์ ์ธํ๊ณ ํ์ ์ ๋นผ๋ผ ์ ์๋ค.
์ฐ๋ฆฌ๊ฐ ๋ง๋ค ๊ฑด "์ฃผ์ ํฌํธํด๋ฆฌ์ค ๊ด๋ฆฌ" ๊ธฐ๋ฅ์ด์ผ.
- ๋ชฉํ ๊ธฐ๋ฅ: ์ด๋ฆ๊ณผ ์ค๋ช ์ผ๋ก ํฌํธํด๋ฆฌ์ค๋ฅผ ๋ง๋ค๊ณ , ๋ด ๋ชฉ๋ก์ ๋ณด๊ณ , ์์ /์ญ์ ํ๋ค.
// packages/schema/src/schema.ts
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
// 1. ํ
์ด๋ธ ์ ์ (๋ฌผ๋ฆฌ์ DB ๊ตฌ์กฐ)
export const portfolios = pgTable('portfolios', {
id: serial('id').primaryKey(),
userId: text('user_id').notNull(), // ์์ ์ ID
name: text('name').notNull(), // ์: "๋ด ๋
ธํ ๋ ๋ ์กฐํฉ"
description: text('description'), // ์ค๋ช
(Optional)
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
// 2. ํ์
์ถ๋ก (์ฌ๊ธฐ๊ฐ Drizzle์ ๊ฟ์ผ ํฌ์ธํธ)
export type Portfolio = typeof portfolios.$inferSelect; // SELECT ํ ๋ ๋์ค๋ ๊ฐ์ฒด์ ํ์
export type NewPortfolio = typeof portfolios.$inferInsert; // INSERT ํ ๋ ๋ฃ์ด์ผ ํ๋ ๊ฐ์ฒด์ ํ์
์ด์ ํฐ๋ฏธ๋์ ์ด๊ณ NestJS CLI๋ก ๋น ๊นกํต ๋ชจ๋์ ์์ฑํด์ค. (์์ผ๋ฉด ์ง์ ํด๋/ํ์ผ ๋ง๋ค์ด๋ ๋ผ)
nest g resource portfolios
# ๋ฌผ์ด๋ณด๋ฉด REST API๋ฅผ ์ ํํด!๐งฉ 2๋จ๊ณ: ์๋ฒฝํ DTO ์์ฑ (SSOT) ๐ข
๋ฐฑ์๋ ํํธ๋ Zod ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก DTO๋ฅผ ๋ฝ์๋ผ ๊ฑฐ์ผ. nestjs-zod ์ drizzle-zod๋ฅผ ์ด๋ค!
๋ฐฉ๊ธ 1๋จ๊ณ์์ ์ง portfolios DB ์คํค๋ง ๋จ ํ๋๋ง ์์ ํ๋ฉด, ์ ํจ์ฑ ๊ฒ์ฌ ์๋ฌ ๋ก์ง(DTO)๊น์ง ์์์ ์ฐ๋๋์ด ์น ๋ค ๋ฐ๋๋ ๊ถ๊ทน์ ๋จ์ผ ์ง์ค ๊ณต๊ธ์(SSOT) ๊ตฌ์กฐ์ผ.
// src/portfolios/dto/create-portfolio.dto.ts
import { createZodDto } from 'nestjs-zod';
import { createInsertSchema } from 'drizzle-zod';
import { portfolios } from '@repo/schema'; // ๋ฐฉ๊ธ ์ง ์คํค๋ง ๊ฐ์ ธ์ค๊ธฐ
// 1. Zod ์คํค๋ง๋ก ์๋ ๋ฒ์ญ!
const CreateSchema = createInsertSchema(portfolios, {
// DB ๋ชจ๋ธ์๋ ์๋ Zod๋ง์ ๊น๊นํ ๊ท์น์ ์ถ๊ฐ!
name: (s) => s.min(2, 'ํฌํธํด๋ฆฌ์ค ์ด๋ฆ์ 2๊ธ์ ์ด์ ์ ์ด์ฃผ์ธ์!').max(50),
description: (s) => s.max(200).optional(),
}).pick({
name: true, // ํด๋ผ์ด์ธํธ๊ฐ ์
๋ ฅํด์ผ ํ ๊ฑด ์ด๋ฆ๊ณผ ์ค๋ช
๋ฟ์ด์ผ.
description: true, // userId ๊ฐ์ ๊ฑด ํค๋์ ํ ํฐ์์ ๋นผ๋ผ ๊ฑฐ๋๊น DTO์์ ์ง์!
});
// 2. ์๋ฒฝํ DTO ํด๋์ค ํ์
export class CreatePortfolioDto extends createZodDto(CreateSchema) {}์์ ์ฉ DTO๋ 1์ด ์ปท์ด์ผ.
// src/portfolios/dto/update-portfolio.dto.ts
import { createZodDto } from 'nestjs-zod';
import { CreatePortfolioDto } from './create-portfolio.dto';
// Create DTO์ ๊ตฌ์ฑ ์์๋ฅผ ๋ชจ์กฐ๋ฆฌ Optional(?)๋ก ๋ฐ๊พผ๋ค.
export class UpdatePortfolioDto extends createZodDto(
CreatePortfolioDto.schema.partial()
) {}๐งช ๋ฐ๋ผํด๋ณด๊ธฐ: ์ปจํธ๋กค๋ฌ์ ์๋น์ค ์ฎ์ด์ CRUD ์์ฑํ๊ธฐ
๋ถํ์ด ๋ค ๋ชจ์์ด. ์ด์ ์์ง(์๋น์ค)์ ์กฐ๋ฆฝํ๊ณ , ํธ๋ค(์ปจํธ๋กค๋ฌ)๊ณผ ์ฐ๊ฒฐ๋ง ํ๋ฉด ์์ฑ๋ผ!
Step 1. ๋น์ฆ๋์ค ๋ก์ง(์๋น์ค) ์์ฑ
// src/portfolios/portfolios.service.ts
import { Inject, Injectable, NotFoundException } from '@nestjs/common';
import { DATABASE_TOKEN, Database } from '../database/database.module';
import { portfolios, NewPortfolio } from '@repo/schema';
import { eq, and } from 'drizzle-orm';
import { CreatePortfolioDto } from './dto/create-portfolio.dto';
import { UpdatePortfolioDto } from './dto/update-portfolio.dto';
@Injectable()
export class PortfoliosService {
// DB ์ ์ฉ ํซ๋ผ์ธ ์ฃผ์
!
constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {}
// 1. ์์ฑ ๋ก์ง
async create(userId: string, dto: CreatePortfolioDto) {
// DTO(์ด๋ฆ, ์ค๋ช
)์ ์์คํ
๋ณ์(์ ์ ID)๋ฅผ ๊ฒฐํฉ
const newPortfolio: NewPortfolio = { ...dto, userId };
const [created] = await this.db
.insert(portfolios)
.values(newPortfolio)
.returning(); // Postgres ๋ง๋ฒ: INSERT ํ ๋ฐ์ดํฐ๋ฅผ ๊บผ๋ด์ค
return created;
}
// 2. ๋ค๊ฑด ์กฐํ ๋ก์ง (๋ฐ๋์ ๋ด ๊ฒ๋ง ๋์์ผ ํจ!!)
async findAll(userId: string) {
return this.db.query.portfolios.findMany({
where: eq(portfolios.userId, userId),
orderBy: (p, { desc }) => [desc(p.createdAt)], // ์ต์ ์
});
}
// 3. ๋จ์ผ ์กฐํ (๋ณด์ ์ฒดํฌ ํฌํจ)
async findOne(userId: string, id: number) {
const portfolio = await this.db.query.portfolios.findFirst({
// id๋ ๋ง๊ณ , userId๋ ๋ง์์ผ ํจ (๋จ์ ํฌํธํด๋ฆฌ์ค๋ ๋ชป ๋ณด๊ฒ ๋ฐฉ์ด)
where: and(eq(portfolios.id, id), eq(portfolios.userId, userId)),
});
if (!portfolio) {
throw new NotFoundException(`ํฌํธํด๋ฆฌ์ค #${id}๋ฅผ ์ฐพ์ ์ ์๋น.`);
}
return portfolio;
}
}Step 2. API ์๋ํฌ์ธํธ(์ปจํธ๋กค๋ฌ) ๋ซ๊ธฐ
์ด์ 100% ๋ซ์ด์ฃผ๋ฉด ๋ผ. ๋จ, ๋ชจ๋ API๋ ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์๊ฒ ๊ฒฝ๋น์(Guard)์ ์ธ์ฐ์.
// src/portfolios/portfolios.controller.ts
import { Controller, Get, Post, Body, Param, UseGuards, ParseIntPipe } from '@nestjs/common';
import { PortfoliosService } from './portfolios.service';
import { CreatePortfolioDto } from './dto/create-portfolio.dto';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { User } from '../auth/types'; // (๋ด๊ฐ ๋ง๋ ์ ์ ์ธํฐํ์ด์ค)
// 1. ๋ผ์ฐํฐ ์ ๋์ฌ์ ์ ์ญ ๊ฒฝ๋น์ ๋ฐฐ์น
@Controller('portfolios')
@UseGuards(JwtAuthGuard)
export class PortfoliosController {
constructor(private readonly portfoliosService: PortfoliosService) {}
// 2. ์์ฑ API (POST /portfolios)
@Post()
create(
@CurrentUser() user: User, // ๐ ๋๊ฐ ์์ฒญํ๋์ง ์ฌ์์ฆ์์ ๋ฝ๊ธฐ
@Body() dto: CreatePortfolioDto // ๐ ์
๊ตฌ์์ Zod๊ฐ ๋ฐ์ดํฐ ๊ฒ์ฌ ๋ค ํด์ค
) {
// ์ปจํธ๋กค๋ฌ๋ ๊ฒ๋ ์ฟจํ๊ฒ ํ๋ผ๋ฏธํฐ๋ง ๋๊ฒจ์ฃผ๊ณ ํด๊ทผ!
return this.portfoliosService.create(user.id, dto);
}
// 3. ๋ค๊ฑด ์กฐํ API (GET /portfolios)
@Get()
findAll(@CurrentUser() user: User) {
return this.portfoliosService.findAll(user.id);
}
// 4. ๋จ์ผ ์กฐํ API (GET /portfolios/:id)
@Get(':id')
findOne(
@CurrentUser() user: User,
@Param('id', ParseIntPipe) id: number // ๐ ๋ฌธ์๋ฅผ ์ซ์๋ก ๊ฐ์ ๋ณํ
) {
return this.portfoliosService.findOne(user.id, id);
}
}๐ผ ๋ฒ ์คํธ ํ๋ํฐ์ค์ ์ค๋ฌด ํ ๐ก
1. ๋ณด์์ ํต์ฌ: ์์ ๊ถ ์ฆ๋ช (Ownership Check)
findAll ์ด๋ findOne์ ์งค ๋ ์ต๊ด์ ์ผ๋ก where: eq(portfolios.id, id) ๋ก๋ง ๋๋ด๋ฉด ์ต์
์ ๋ณด์ ์ฌ๊ณ (IDOR ์ทจ์ฝ์ ) ๊ฐ ํฐ์ง ์ ์์ด.
์
์์ ์ธ ํด์ปค๊ฐ GET /portfolios/999 ๋ผ๊ณ ์๋ฌด ์์ด๋๋ ์ฐ์ด ๋ฃ์ผ๋ฉด, ์ํ ๋จ์ ๊ณ์ข ์ ๋ณด๊ฐ ๋ค ๋ ๋ฒ๋ฆฌ๊ฑฐ๋ .
์ฝ๋์์ ๋ณธ ๊ฒ์ฒ๋ผ ๋ฐ๋์ and(eq(id), eq(userId)) ์กฐ๊ฑด์ ๊ฑธ์ด์ ๋ฌด์กฐ๊ฑด '๋ด ์์ฐ'์ธ์ง ๊ต์ฐจ ๊ฒ์ฆ ํด์ผ ํด!
2. Drizzle $infer... ํ์
์ ์ฌ์ฌ์ฉ
์ค๋ฌด์์ User๋ Portfolio ๊ฐ์ ํ์
์ interfaces ํด๋์ ์ผ์ผ์ด ์น์ง ๋ง.
Drizzle ์คํค๋ง ํ์ผ์์ typeof ํ
์ด๋ธ๋ช
.$inferSelect ๋ก ๋ด๋ณด๋ธ(export) ํ์
์ ์ฑ ์ด๋์๋ importํด์ ์ฌํ์ฉํด. ์ด๋์ผ ๋์ค์ DB ์ปฌ๋ผ์ด ์ถ๊ฐ๋ผ๋ TypeScript ์๋ฌ ๋ ์ด๋๋ง์ ๋ค ๊ฑธ๋ ค์ ์ ์ง๋ณด์๊ฐ ๋งค์ฐ ์ฌ์์ ธ.
๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
์๋ฌ ๋ฉ์์ง๊ฐ ๋จ๋ฉด Ctrl+F๋ก ๊ฒ์ํด๋ด.
โ TypeError: Cannot read properties of undefined (reading 'insert')
**ํ์:**POST ์์ฒญ์ ๋ ๋ ธ๋๋ 500 ์๋ฌ๋ฅผ ๋ฑ์ผ๋ฉฐ ํฐ๋ฏธ๋์ ์ ๋ฐ ๋ก๊ทธ๊ฐ ๋ธ.
์์ธ: PortfoliosService ํด๋์ค ์๋จ์ constructor(@Inject(DATABASE_TOKEN) private readonly db: Database) {} ๋ถ๋ถ์ @Inject(...) ๋ฐ์ฝ๋ ์ดํฐ๋ฅผ ๋นผ๋จน์ ๊ฑฐ์ผ. NestJS๋ DB ๊ฐ์ฒด๋ฅผ ๊ฝ์์ฃผ์ง ์์์ผ๋ this.db๊ฐ undefined ์ธ ์ํ๋ก insert๋ฅผ ๋ ๋ฆฐ ๊ฑฐ์ง!
ํด๊ฒฐ์ฑ
: @Inject๋ฅผ ์ ๋๋ก ๋ฌ์๋์ง ํ์ธํด.
โ 400 Bad Request: Expected string, received object
์์ธ: ํฌํธํด๋ฆฌ์ค๋ฅผ ๋ง๋ค ๋, nestjs-zod ํ์ดํ๋ผ์ธ์์ ์
๊ตฌ์ปท ๋นํ๋ ์ค. Postman์ด๋ ํ๋ก ํธ์๋์์ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ผ ๋ ๊ฐ์ฒด ๋งคํ์ด ์ด๊ธ๋ ๊ฑฐ์ผ.
ํด๊ฒฐ์ฑ
: ์๋ฒ ์ฝ๋ ์๋ชป์ด ์๋! CreatePortfolioDto์ ์ ํ ์คํ ๊ทธ๋๋ก { "name": "๋ด ํฌ์์กฐํฉ" } ๊ฐ์ ์ฌ๋ฐ๋ฅธ JSON ํ์ด๋ก๋๋ฅผ ๋ ๋ ธ๋์ง ์์ฒญ ์ชฝ์ ์ ๊ฒํด ๋ณผ ๊ฒ.
๐๏ธ ์นํธ์ํธ โ ์ค๋ฌด ์์ฝ ์นด๋
[๊ฐ๋ฐ ํ๋ฆ(Workflow) ํ๋์ ๋ณด๊ธฐ]
- DB ๋ ๋ฒจ:
schema.ts์pgTable๋ผ๋ ์ ์ธ - ์ฝ๋ ๋ ๋ฒจ:
nest g resource๋ก ๋ชจ๋/์ปจํธ๋กค๋ฌ/์๋น์ค ์์ฑ - DTO ํต์ :
drizzle-zod๋ก ์คํค๋ง์์ DTO ๋๊ธฐํ - ์๋น์ค ๋๋:
constructor(@Inject(DATABASE_TOKEN))๋ก ๋ถ๋ฌ์ ๋ก์ง ์์ฑ - ๋ผ์ฐํฐ ์
๊ตฌ: ์ปจํธ๋กค๋ฌ์ ๊ฐ๋ ์น๊ณ (
@UseGuards), ๋ผ์ฐํฐ ์ด๊ธฐ(@Get)
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. NestJS ํ๋ ์์ํฌ์ Zod ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฐ๋ ์, '๋จ์ผ ์ง์ค ๊ณต๊ธ์(SSOT)' ์ด๋ผ๋ ์ฉ์ด์ ์ฅ์ ์ ๊ฐ์ฅ ์ ์ค๋ช ํ ๊ฒ์ ๋ฌด์์ธ๊ฐ?
- A) ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ํ ๊ตฐ๋ฐ์๋ง ์๋ฒ๋ฅผ ๋์ด ๋ณด์์ด ์๋ฒฝํด์ง๋ค.
- B) DB ์คํค๋ง ๊ตฌ์กฐ ์ฝ๋๋ฅผ ํ ๋ฒ๋ง ๋ฐ๊พธ๋ฉด, ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ตฌ์กฐ๋ ๋ฌผ๋ก ํด๋ผ์ด์ธํธ๋ฅผ ๋ง์์ฃผ๋ ๋ฐ์ดํฐ ์ ํจ์ฑ ๊ฒ์ฆ(DTO) ๊ท์น๊น์ง ์์์ ํจ๊ป ๋ณํด์ ์ค์๋ฅผ ์์ ์ค๋ค.
- C) ๋ชจ๋ ์ปดํฌ๋ํธ(Controller, Service)๋ฅผ ํ๋์ ๋ชจ๋ ์์์ ์ ๋ถ ๊ด๋ฆฌํ๋ ์ํคํ ์ฒ ํจํด์ ๋งํ๋ค.
โ ์ ๋ต: B
๐ก **์ค๋ช :**DB ์คํค๋ง ๋ฐ๋ก, ์ธํฐํ์ด์ค(Interface) ํ์ ๋ฐ๋ก, ์ ํจ์ฑ ๊ฒ์ฌ ํด๋์ค ๋ฐ๋ก ๋ง๋ค๋ค๊ฐ ์ด๋ฆ(name) ์ต๋ ๊ธธ์ด๋ฅผ 50์์์ 100์๋ก ๋๋ฆฌ๋ ค๋ฉด ์ธ ๊ตฐ๋ฐ ์ฝ๋๋ฅผ ๋ค ๊ณ ์ณ์ผ ํด. SSOT ํจํด์ ์ ์ผ ๊ทผ์(Source)์ธ DB ์คํค๋ง ํ๋๋ง ๊ณ ์น๋ฉด ์ ๋ถ ์ฐ๋๋ผ์ ๋ฐ๋๋ ๋ง๋ฒ์ด์ผ!
Q2. ๋ค์ ์ค ์ปจํธ๋กค๋ฌ์์ @CurrentUser() ๊ฐ ์๋ฌด๋ฆฌ ์ฝ์ด๋ ๋ฌด์กฐ๊ฑด ๊ฐ์ ์ฑ์ ์ฌ ์ ์๋๋ก ์คํจํ๊ฒ ๋ง๋๋ ์์ธ์?
- A) ์ปจํธ๋กค๋ฌ ํด๋์ค ์๋จ์
@UseGuards(JwtAuthGuard)๋ฅผ ๋ถ์ด์ง ์์๋ค. - B) ์ ์ ๊ฐ ํค๋์ ์ฌ๋ฐ๋ฅธ
Authorization: Bearer <token>๊ฐ์ ๋ฃ์ด์ ๋ณด๋๋ค. - C) DTO์
@IsString()์์ฑ์ ๋นผ๋จน์๋ค.
โ ์ ๋ต: A
๐ก ์ค๋ช
: ์
๊ตฌ์ ๊ฒฝ๋น์(๊ฐ๋)๋ฅผ ์ธ์ฐ์ง ์์ผ๋ฉด, ๋น์ฐํ ์ ๋ถ์ฆ(ํ ํฐ) ๋ ๊ฒ์ฌํ์ง ์๊ณ , ๋ฐ๋ผ์ ์์ฒญ(request) ๊ฐ์ฒด ์์ ์ ์ ์ ๋ณด๋ ๊ฝํ์ง ์์!
Q3. findAll (ํ์ ํฌํธํด๋ฆฌ์ค ๋ชฉ๋ก ์ ์ฒด ์กฐํ) ๊ธฐ๋ฅ์ ์งค ๋, ๋ณด์์ ์ํด where: eq(portfolios.userId, userId) ์ฝ๋๋ฅผ ๋ฐ๋์ ๋ฃ์ด์ผ ํ๋ ์ด์ ๋ ๋ฌด์์ ๋ง๊ธฐ ์ํจ์ธ๊ฐ?
โ
์ ๋ต: ๊ถํ ์๋ ๋ค๋ฅธ ์ฌ์ฉ์๊ฐ ๋จ์ ํฌํธํด๋ฆฌ์ค ์ ๋ณด๋ฅผ ๋ฌด๋จ์ผ๋ก ์ด๋(IDOR ๋ณด์ ์ทจ์ฝ์ )ํ๋ ๊ฒ์ ๋ง๊ธฐ ์ํจ!
์ค๋ช
: ๋ฌด์กฐ๊ฑด "DB ์์ ์๋ ๋ชจ๋ ํฌํธํด๋ฆฌ์ค ๋ค ๋ด๋!" ๊ฐ ์๋๋ผ, "๋ก๊ทธ์ธํ ๋น์ (userId)์ ์์ ๊ถ์ด ํ์ธ๋ ํฌํธํด๋ฆฌ์ค๋ง ๋ด๋!" ๋ก ํํฐ๋ฅผ ๊ฑธ์ด์ผ ํด.