Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions keyword/chapter06/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
* ORM
ORM(Object-Relation Mapping)은 객체지향 프로그래밍에서 개발자가 SQL query를 직접 작성하지 않고 데이터베이스를 조작할 수 있게 하는 기술이다.
ORM은 테이블을 코드내의 객체로 매핑시켜 테이블에 정보를 추가하거나 삭제할때 쿼리를 작성하지 않고 일반 JS class 객체에 접근하듯이 조작 할 수 있게 한다.
ORM을 사용하면 쿼리를 직접 작성하지 않아도 되서 코드가 간결해지고, 스키마 변경에 유연하게 대응할 수 있다.
대신 쿼리가 너무 복잡해지면 성능문제가 발생 할 수 있고, 디버깅이 복잡해질 수 있다.
* Prisma 문서 살펴보기
* Prisma Introduction

1. prisma client를 설정하기 위해선 데이터베이스 연결, 클라이언트 생성 및 적어도 하나 이상의 모델이 포함된 schema 파일이 필요하다.

```jsx
datasource db {
url = env("DATABASE_URL")
provider = "postgresql"
}

generator client {
provider = "prisma-client-js"
}

model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
email String @unique
name String?
}
```
1. node 프로젝트에 primsa cli, client를 설치한다.

```jsx
npm install prisma --save-dev
npx prisma
npm install @prisma/client
```
1. 프로젝트에서 실제로 prisma 클라이언트를 객체로 가져온다.

```jsx
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()
// use `prisma` in your application to read and write data in your DB
```
1. client를 사용해 데이터베이스에 쿼리를 보낸다.

```jsx
// run inside `async` function
const newUser = await prisma.user.create({
data: {
name: 'Alice',
email: '[email protected]',
},
})

const users = await prisma.user.findMany()
```
![스키마를 이전하거나 스키마가 바뀔때마다 prisma generate를 실행시켜서 업데이트를 해야한다.](attachment:c1feb47c-012b-47e5-af62-a4596b03de3d:image.png)
스키마를 이전하거나 스키마가 바뀔때마다 prisma generate를 실행시켜서 업데이트를 해야한다.
* Full text search
일반적인 DB를 통한 검색 LIKE %keyword%와 달리 “apple”, “apples”, “Apple’s” 같은 변형까지 매칭해줄 수 있게 하는 prisma 내장 기능이다
FTS(full text search) 기능을 활성화 하면 데이터베이스 열 내의 텍스트를 검색하는 기능을 어플리케이션에 추가 할 수있다.

1. schema에 검색 기능을 추가하고 prisma generate를 해서 업데이트를 한다.

```jsx
generator client {
provider = "prisma-client-js"
}

model post {
id Int @unique
content String
title String

@@fulltext([content])
@@fulltext([content, title]) //content, title을 검색에 사용하겠다는 의미
}
```
1. prisma를 사용해 검색을 한다.

```jsx
// All posts that contain the words 'cat' or 'dog'.
const result = await prisma.posts.findMany({
where: {
body: {
search: 'cat dog',
},
},
})

// All posts that contain the words 'cat' and not 'dog'.
const result = await prisma.posts.findMany({
where: {
body: {
search: '+cat -dog',
},
},
})

// All drafts that contain the words 'cat' and 'dog'.
const result = await prisma.posts.findMany({
where: {
status: 'Draft',
body: {
search: '+cat +dog',
},
},
})
```
![image.png](attachment:13d99ab8-f2db-452d-ba2e-0dfa54145ce4:image.png)
장점

* 인덱스기반 검색으로 동작이 빠르고, 정확하다.
* 의미 기반,키워드 조합이 가능함

단점

* MySQL, PostgreSQL의 DB 엔진에서만 사용 가능하다
* 한국어 형태소 분석이 불가능하다.
* ORM(Prisma)을 사용하여 좋은 점과 나쁜 점
* 장점
1. 타입기반 개발이 가능하여 안정성이 좋다( 특히 typescript를 기반으로 하는 프로젝트 할때 좋다)

```jsx
const user = await prisma.user.findUnique({ where: { id: 'abc' } }); // ❌ 문자열 → 에러 발생
```
다음과 같은 코드가 존재한다고 할때 user.id가 number타입이면 primsa는 이 타입 정보를 바탕으로 컴파일 타임에 에러를 검출 할 수 있다.
2. 자동 마이그레이션, 타입 생성이 가능하다.
prisma migrate dev 명령어를 사용하면 DB에 변경된 모델을 자동으로 반영하고, 그에 맞는 **타입 정의**도 자동 생성된다.
예시: User 모델에 새로운 필드 profileImage를 추가하면, 바로 타입이 반영되어 IDE 자동완성 기능까지 가능하다.
DB 구조와 코드가 항상 **동기화**되며, 코드 변경 → 마이그레이션 → 타입 자동생성이 자연스럽게 연결됩니다
3. 모델 중심 설계로 유지보수가 쉽다.
4. 트랜잭션, 관계처리, 유효성 검사등이 추상화 되어 편리하다.
* 단점
1. 복잡한 쿼리(여러 JOIN, 집계 등)은 표현하기 어렵다
2. 쿼리 튜닝과 성능 최적화에는 한계가 있다.
3. Prisma 문법을 새로 배워야하기에 러닝커브가 존재한다.
* 다양한 ORM 라이브러리 살펴보기
* ex. Sequelize
Nodejs 초기부터 사용된 대표적 ORM으로 자료가 많고 사용자가 많다.
MYSQL, PostgreSQL, MSSQL등 여러 DB를 지원한다. 모델은 클래스로 정의하고 쿼리는 SQL-Like로 메서드 체이닝 방식을 사용한다.
하지만 타입추론, 자동완성이 제한적이고, 쿼리문법이 추상화 되어 있지 않아 직접 쿼리를 짜는 느낌이 강하다.
* ex. TypeORM
데코레이터를 기반으로 엔티티를 정의하는 방식이다. NestJS에서 사용되며 공식문서에서도 TypeORM을 사용한다. 관계설정이 명시적이고 가독성이 우수한 벙밥이다.
하지만 마이그레이션 안정성이 부족하고 버전간 호환 이슈가 발생할 수 있으며, DB구조가 복잡해질 경우 설정이 번거롭고 디버깅이 어렵다는 단점이 있다.
* 페이지네이션을 사용하는 다른 API 찾아보기
* [https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide](https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide)
카카오 Daum 검색 API

```jsx
GET <https://dapi.kakao.com/v2/search/web?query=검색어&page=1&size=10>
```
* **주요 파라미터:**
* page: 결과 페이지 번호 (1~50)
* size: 페이지당 결과 수 (1~50)
* **응답 구조:**
* meta: 검색 결과에 대한 메타정보 (총 결과 수, 페이지 수 등)
* documents: 검색 결과 목록
* [https://developers.coupangcorp.com/hc/en-us/articles/360033645034-Product-list-paging-query](https://developers.coupangcorp.com/hc/en-us/articles/360033645034-Product-list-paging-query)
쿠팡 상품 목록 조회 API

```jsx
GET <https://api-gateway.coupang.com/v2/providers/seller_api/apis/api/v1/marketplace/seller-products?vendorId=VENDOR_ID&nextToken=NEXT_TOKEN&maxPerPage=10>
```
* **주요 파라미터:**
* vendorId: 판매자 ID
* nextToken: 다음 페이지를 조회하기 위한 토큰
* maxPerPage: 페이지당 최대 결과 수
* **응답 구조:**
* nextToken: 다음 페이지 조회를 위한 토큰
* data: 상품 목록 데이터
154 changes: 154 additions & 0 deletions mission/chapter06/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
https://github.com/seoki180/9th_node_practice/tree/feature/chapter06




* 한 번에 여러 번의 DB 작업을 연달아 처리할 때, 중간에 처리가 실패했는데 DB에는 중간까지만 값이 반영되어 있으면 문제가 있을 것 같습니다. 이를 방지하는 기술로는 Transaction이 있는데, Prisma를 이용해 Transaction을 관리하는 방법을 찾아 정리해주세요. 워크북의 실습 프로젝트에서도 적용할 수 있다면 적용해주세요.

Prisma에서 트랜잭션을 관리하는 법은 크게 두가지가 있다.

1. Batch(Sequential) 트랜잭션
단순히 여러 prisma 쿼리를 묶어 ALL or NOTHING으로 처리한다.
prisma.$transaction() 에 prisma 쿼리를 배열 형식으로 전달되면 모든 작업이 성공될 때만 커밋이 되고 하나라도 실패 하게 되면 롤백되는 방식으로 트랜잭션을 구현 할 수 있다.

```jsx
await prisma.$transaction([
prisma.user.create({ data: { name: 'Alice' } }),
prisma.post.create({ data: { title: 'Hello', userId: 1 } }),
]);
```

이 방식은 매우 직관적이고 간단하여 독립된 쿼리 여러개를 동시에 실행할 때 유용하다.

하지만 이 방식에서는 모든 쿼리가 병렬로 실행되기 때문에 순차적 실행이 보장되지 않는다.

따라서 순서가 중요한 쿼리의 경우

예를들어 ) 유저가 가입을 해서 user.id가 생성되고 이후 쿼리에서 이user.id를 사용해야 할때 다음 방법을 사용할 수 있다.
2. Interactive 트랜잭션
트랙잭션 내부에서 조건분기, 외부API 호출, 흐름제어, 루프분기등 로직이 포함되는 경우 callback 인자로 받는 tx 객체를 통해 모든 쿼리를 실행한다. 이때 timeout, maxWait, isolationLevel등 옵션을 제어할 수 있다.

```jsx
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({ data: { name: 'Bob' } });
await tx.post.create({ data: { title: 'New Post', userId: user.id } });
});
```

```jsx
type PrismaOrTx = PrismaClient | Prisma.TransactionClient;

async function createUserCore(db: PrismaOrTx, input: CreateUserDto) {
const user = await db.user.create({ data: input.user });
const profile = await db.profile.create({ data: { ...input.profile, userId: user.id } });
return { user, profile };
}

// 서비스: 트랜잭션 경계 설정
async function registerUser(input: CreateUserDto) {
return prisma.$transaction(async (tx) => {
const result = await createUserCore(tx, input);
// 필요시 추가 로직...
return result;
});
}

//tx 객체 전달예제
```

이 함수에서는 tx가 prisma 객체로 사용되고 위에서 아래로 코드의 순차적 실행이 보장된다. 또한 트랜잭션이므로 모든 작업이 성공적으로 수행되어야 커밋이 이루어질 수 있다.

* DB 성능은 서버 개발에서 가장 중요한 부분 중 하나입니다. Prisma를 이용해서 데이터베이스에 질의할 때, 각 SQL 쿼리가 얼마나 오래 소요되는지 로그를 남겨주세요. (쿼리를 실행하기 전의 시간을 측정하고, 쿼리를 실행한 이후의 시간을 측정하여 몇 ms가 소요되었는지 측정하여 `console.log`로 출력할 수 있습니다.) 가능하면 쿼리를 사용하는 부분마다 매번 `console.log`로 출력하기 보다는, 항상 공통으로 자동으로 적용될 수 있는 방법으로 구현해주세요.

* DB 성능은 서버 개발에서 가장 중요한 부분 중 하나입니다. Prisma를 이용해서 데이터베이스에 질의할 때, 각 SQL 쿼리가 얼마나 오래 소요되는지 로그를 남겨주세요. (쿼리를 실행하기 전의 시간을 측정하고, 쿼리를 실행한 이후의 시간을 측정하여 몇 ms가 소요되었는지 측정하여 `console.log`로 출력할 수 있습니다.) 가능하면 쿼리를 사용하는 부분마다 매번 `console.log`로 출력하기 보다는, 항상 공통으로 자동으로 적용될 수 있는 방법으로 구현해주세요.
Prisma 공식문서에서는 두가지 방법으로 prisma에서 logging을 구현 할 수 있다고 한다.

1. Stdout에 로깅(기본값)
2. 이벤트기반 로깅

1번 방식은 모든 로그레벨을 stdout으로 출력 가능하게 해준다. 가장 기본적이며 간단한 방법이고 logLevel객체에 이미 모든 로그레벨이 선언 되어 있어 간단하다.

```jsx
const prisma = new PrismaClient({
log: [
{
emit: 'stdout',
level: 'query',
},
{
emit: 'stdout',
level: 'error',
},
{
emit: 'stdout',
level: 'info',
},
{
emit: 'stdout',
level: 'warn',
},
],
})
```
2번방식은 prisma.$on() 메서드를 활용해 모든 이벤트를 감지하고 이벤트가 발생하면 직접 사용자가 로그를 출력할 수 있도록 한다.

```jsx
prisma.$on('query', (e) => {
console.log('Query: ' + e.query)
console.log('Params: ' + e.params)
console.log('Duration: ' + e.duration + 'ms')
})
```
* 우리는 흔히 DB 쿼리에서 N+1 문제를 마주할 수 있습니다. 예를 들어, 게시글을 조회하면서 각 게시글에 해당하는 댓글들을 개별적으로 쿼리한다면, 댓글을 조회하는 쿼리가 각 게시글마다 추가로 발생하게 되어 N+1 문제가 발생합니다. 이를 Prisma에서 N+1문제를 해결할 수 있는 방법을 정리해주세요.


N+1문제란 연관관계에서 발생하는 이슈로, 연관관계가 설정된 엔티티를 조회할때 조회된 데이터 개수만큼 조회 쿼리가 추가로 발생하게 되는데, 이를 N+1문제라 한다.

데이터가 많아질수록 쿼리 수가 기하급수적으로 증가하게 된다. 특히 ORM에서 많이 발생하게 된다.

해결 방법 두가지로 나눠서 생각 할 수 있다.

1. Eager Loading (즉시 로딩)
데이터를 조회할 때 연관된 모든 객 체의 데이터까지 한 번에 불러 온다. 즉 위에서 에상된 문제인 게시글과 댓글을 한번에 조인해 불러오는 방식을 생각 할 수 있다.

```jsx
const postsWithComments = await prisma.post.findMany({
include: {
comments: true, // 관계된 모든 댓글을 함께 가져옴
},
});
```
include방식으로 LEFT JOIN을 수행해 한번의 쿼리로 모든 데이터를 받아온다.

다만 너무 복잡한 관계인 경우에는 eager loading이 오히려 성능저하의 원인이 될 수 있다.
2. Batch조회 이후 수동 매핑
include로 처리할 수 없는 복잡한 관계일 경우, 수동 Batch로 해결 할 수 있다.

```jsx
// 1. 게시글 먼저 가져오기
const posts = await prisma.post.findMany();
const postIds = posts.map(post => post.id);

// 2. 댓글을 postId 기준으로 한 번에 가져오기
const comments = await prisma.comment.findMany({
where: {
postId: { in: postIds },
},
});

// 3. postId별로 댓글을 매핑
const commentMap = new Map();
comments.forEach(comment => {
if (!commentMap.has(comment.postId)) {
commentMap.set(comment.postId, []);
}
commentMap.get(comment.postId).push(comment);
});

// 4. 게시글에 댓글 붙이기
const postsWithComments = posts.map(post => ({
...post,
comments: commentMap.get(post.id) || [],
}));
```
다음과 같은 쿼리로 조회 할 경우 쿼리가 2번만 실행된다.