Skip to content

Commit 7d4997f

Browse files
authored
Merge pull request #26 from seoki180/main
2 parents 82a4e80 + 01d8f79 commit 7d4997f

File tree

2 files changed

+324
-0
lines changed

2 files changed

+324
-0
lines changed

keyword/chapter06/README.md

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
* ORM
2+
ORM(Object-Relation Mapping)은 객체지향 프로그래밍에서 개발자가 SQL query를 직접 작성하지 않고 데이터베이스를 조작할 수 있게 하는 기술이다.
3+
ORM은 테이블을 코드내의 객체로 매핑시켜 테이블에 정보를 추가하거나 삭제할때 쿼리를 작성하지 않고 일반 JS class 객체에 접근하듯이 조작 할 수 있게 한다.
4+
ORM을 사용하면 쿼리를 직접 작성하지 않아도 되서 코드가 간결해지고, 스키마 변경에 유연하게 대응할 수 있다.
5+
대신 쿼리가 너무 복잡해지면 성능문제가 발생 할 수 있고, 디버깅이 복잡해질 수 있다.
6+
* Prisma 문서 살펴보기
7+
* Prisma Introduction
8+
9+
1. prisma client를 설정하기 위해선 데이터베이스 연결, 클라이언트 생성 및 적어도 하나 이상의 모델이 포함된 schema 파일이 필요하다.
10+
11+
```jsx
12+
datasource db {
13+
url = env("DATABASE_URL")
14+
provider = "postgresql"
15+
}
16+
17+
generator client {
18+
provider = "prisma-client-js"
19+
}
20+
21+
model User {
22+
id Int @id @default(autoincrement())
23+
createdAt DateTime @default(now())
24+
email String @unique
25+
name String?
26+
}
27+
```
28+
1. node 프로젝트에 primsa cli, client를 설치한다.
29+
30+
```jsx
31+
npm install prisma --save-dev
32+
npx prisma
33+
npm install @prisma/client
34+
```
35+
1. 프로젝트에서 실제로 prisma 클라이언트를 객체로 가져온다.
36+
37+
```jsx
38+
import { PrismaClient } from '@prisma/client'
39+
40+
const prisma = new PrismaClient()
41+
// use `prisma` in your application to read and write data in your DB
42+
```
43+
1. client를 사용해 데이터베이스에 쿼리를 보낸다.
44+
45+
```jsx
46+
// run inside `async` function
47+
const newUser = await prisma.user.create({
48+
data: {
49+
name: 'Alice',
50+
51+
},
52+
})
53+
54+
const users = await prisma.user.findMany()
55+
```
56+
![스키마를 이전하거나 스키마가 바뀔때마다 prisma generate를 실행시켜서 업데이트를 해야한다.](attachment:c1feb47c-012b-47e5-af62-a4596b03de3d:image.png)
57+
스키마를 이전하거나 스키마가 바뀔때마다 prisma generate를 실행시켜서 업데이트를 해야한다.
58+
* Full text search
59+
일반적인 DB를 통한 검색 LIKE %keyword%와 달리 “apple”, “apples”, “Apple’s” 같은 변형까지 매칭해줄 수 있게 하는 prisma 내장 기능이다
60+
FTS(full text search) 기능을 활성화 하면 데이터베이스 열 내의 텍스트를 검색하는 기능을 어플리케이션에 추가 할 수있다.
61+
62+
1. schema에 검색 기능을 추가하고 prisma generate를 해서 업데이트를 한다.
63+
64+
```jsx
65+
generator client {
66+
provider = "prisma-client-js"
67+
}
68+
69+
model post {
70+
id Int @unique
71+
content String
72+
title String
73+
74+
@@fulltext([content])
75+
@@fulltext([content, title]) //content, title을 검색에 사용하겠다는 의미
76+
}
77+
```
78+
1. prisma를 사용해 검색을 한다.
79+
80+
```jsx
81+
// All posts that contain the words 'cat' or 'dog'.
82+
const result = await prisma.posts.findMany({
83+
where: {
84+
body: {
85+
search: 'cat dog',
86+
},
87+
},
88+
})
89+
90+
// All posts that contain the words 'cat' and not 'dog'.
91+
const result = await prisma.posts.findMany({
92+
where: {
93+
body: {
94+
search: '+cat -dog',
95+
},
96+
},
97+
})
98+
99+
// All drafts that contain the words 'cat' and 'dog'.
100+
const result = await prisma.posts.findMany({
101+
where: {
102+
status: 'Draft',
103+
body: {
104+
search: '+cat +dog',
105+
},
106+
},
107+
})
108+
```
109+
![image.png](attachment:13d99ab8-f2db-452d-ba2e-0dfa54145ce4:image.png)
110+
장점
111+
112+
* 인덱스기반 검색으로 동작이 빠르고, 정확하다.
113+
* 의미 기반,키워드 조합이 가능함
114+
115+
단점
116+
117+
* MySQL, PostgreSQL의 DB 엔진에서만 사용 가능하다
118+
* 한국어 형태소 분석이 불가능하다.
119+
* ORM(Prisma)을 사용하여 좋은 점과 나쁜 점
120+
* 장점
121+
1. 타입기반 개발이 가능하여 안정성이 좋다( 특히 typescript를 기반으로 하는 프로젝트 할때 좋다)
122+
123+
```jsx
124+
const user = await prisma.user.findUnique({ where: { id: 'abc' } }); // ❌ 문자열 → 에러 발생
125+
```
126+
다음과 같은 코드가 존재한다고 할때 user.id가 number타입이면 primsa는 이 타입 정보를 바탕으로 컴파일 타임에 에러를 검출 할 수 있다.
127+
2. 자동 마이그레이션, 타입 생성이 가능하다.
128+
prisma migrate dev 명령어를 사용하면 DB에 변경된 모델을 자동으로 반영하고, 그에 맞는 **타입 정의**도 자동 생성된다.
129+
예시: User 모델에 새로운 필드 profileImage를 추가하면, 바로 타입이 반영되어 IDE 자동완성 기능까지 가능하다.
130+
DB 구조와 코드가 항상 **동기화**되며, 코드 변경 → 마이그레이션 → 타입 자동생성이 자연스럽게 연결됩니다
131+
3. 모델 중심 설계로 유지보수가 쉽다.
132+
4. 트랜잭션, 관계처리, 유효성 검사등이 추상화 되어 편리하다.
133+
* 단점
134+
1. 복잡한 쿼리(여러 JOIN, 집계 등)은 표현하기 어렵다
135+
2. 쿼리 튜닝과 성능 최적화에는 한계가 있다.
136+
3. Prisma 문법을 새로 배워야하기에 러닝커브가 존재한다.
137+
* 다양한 ORM 라이브러리 살펴보기
138+
* ex. Sequelize
139+
Nodejs 초기부터 사용된 대표적 ORM으로 자료가 많고 사용자가 많다.
140+
MYSQL, PostgreSQL, MSSQL등 여러 DB를 지원한다. 모델은 클래스로 정의하고 쿼리는 SQL-Like로 메서드 체이닝 방식을 사용한다.
141+
하지만 타입추론, 자동완성이 제한적이고, 쿼리문법이 추상화 되어 있지 않아 직접 쿼리를 짜는 느낌이 강하다.
142+
* ex. TypeORM
143+
데코레이터를 기반으로 엔티티를 정의하는 방식이다. NestJS에서 사용되며 공식문서에서도 TypeORM을 사용한다. 관계설정이 명시적이고 가독성이 우수한 벙밥이다.
144+
하지만 마이그레이션 안정성이 부족하고 버전간 호환 이슈가 발생할 수 있으며, DB구조가 복잡해질 경우 설정이 번거롭고 디버깅이 어렵다는 단점이 있다.
145+
* 페이지네이션을 사용하는 다른 API 찾아보기
146+
* [https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide](https://developers.kakao.com/docs/latest/ko/daum-search/dev-guide)
147+
카카오 Daum 검색 API
148+
149+
```jsx
150+
GET <https://dapi.kakao.com/v2/search/web?query=검색어&page=1&size=10>
151+
```
152+
* **주요 파라미터:**
153+
* page: 결과 페이지 번호 (1~50)
154+
* size: 페이지당 결과 수 (1~50)
155+
* **응답 구조:**
156+
* meta: 검색 결과에 대한 메타정보 (총 결과 수, 페이지 수 등)
157+
* documents: 검색 결과 목록
158+
* [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)
159+
쿠팡 상품 목록 조회 API
160+
161+
```jsx
162+
GET <https://api-gateway.coupang.com/v2/providers/seller_api/apis/api/v1/marketplace/seller-products?vendorId=VENDOR_ID&nextToken=NEXT_TOKEN&maxPerPage=10>
163+
```
164+
* **주요 파라미터:**
165+
* vendorId: 판매자 ID
166+
* nextToken: 다음 페이지를 조회하기 위한 토큰
167+
* maxPerPage: 페이지당 최대 결과 수
168+
* **응답 구조:**
169+
* nextToken: 다음 페이지 조회를 위한 토큰
170+
* data: 상품 목록 데이터

mission/chapter06/README.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
https://github.com/seoki180/9th_node_practice/tree/feature/chapter06
2+
3+
4+
5+
6+
* 한 번에 여러 번의 DB 작업을 연달아 처리할 때, 중간에 처리가 실패했는데 DB에는 중간까지만 값이 반영되어 있으면 문제가 있을 것 같습니다. 이를 방지하는 기술로는 Transaction이 있는데, Prisma를 이용해 Transaction을 관리하는 방법을 찾아 정리해주세요. 워크북의 실습 프로젝트에서도 적용할 수 있다면 적용해주세요.
7+
8+
Prisma에서 트랜잭션을 관리하는 법은 크게 두가지가 있다.
9+
10+
1. Batch(Sequential) 트랜잭션
11+
단순히 여러 prisma 쿼리를 묶어 ALL or NOTHING으로 처리한다.
12+
prisma.$transaction() 에 prisma 쿼리를 배열 형식으로 전달되면 모든 작업이 성공될 때만 커밋이 되고 하나라도 실패 하게 되면 롤백되는 방식으로 트랜잭션을 구현 할 수 있다.
13+
14+
```jsx
15+
await prisma.$transaction([
16+
prisma.user.create({ data: { name: 'Alice' } }),
17+
prisma.post.create({ data: { title: 'Hello', userId: 1 } }),
18+
]);
19+
```
20+
21+
이 방식은 매우 직관적이고 간단하여 독립된 쿼리 여러개를 동시에 실행할 때 유용하다.
22+
23+
하지만 이 방식에서는 모든 쿼리가 병렬로 실행되기 때문에 순차적 실행이 보장되지 않는다.
24+
25+
따라서 순서가 중요한 쿼리의 경우
26+
27+
예를들어 ) 유저가 가입을 해서 user.id가 생성되고 이후 쿼리에서 이user.id를 사용해야 할때 다음 방법을 사용할 수 있다.
28+
2. Interactive 트랜잭션
29+
트랙잭션 내부에서 조건분기, 외부API 호출, 흐름제어, 루프분기등 로직이 포함되는 경우 callback 인자로 받는 tx 객체를 통해 모든 쿼리를 실행한다. 이때 timeout, maxWait, isolationLevel등 옵션을 제어할 수 있다.
30+
31+
```jsx
32+
await prisma.$transaction(async (tx) => {
33+
const user = await tx.user.create({ data: { name: 'Bob' } });
34+
await tx.post.create({ data: { title: 'New Post', userId: user.id } });
35+
});
36+
```
37+
38+
```jsx
39+
type PrismaOrTx = PrismaClient | Prisma.TransactionClient;
40+
41+
async function createUserCore(db: PrismaOrTx, input: CreateUserDto) {
42+
const user = await db.user.create({ data: input.user });
43+
const profile = await db.profile.create({ data: { ...input.profile, userId: user.id } });
44+
return { user, profile };
45+
}
46+
47+
// 서비스: 트랜잭션 경계 설정
48+
async function registerUser(input: CreateUserDto) {
49+
return prisma.$transaction(async (tx) => {
50+
const result = await createUserCore(tx, input);
51+
// 필요시 추가 로직...
52+
return result;
53+
});
54+
}
55+
56+
//tx 객체 전달예제
57+
```
58+
59+
이 함수에서는 tx가 prisma 객체로 사용되고 위에서 아래로 코드의 순차적 실행이 보장된다. 또한 트랜잭션이므로 모든 작업이 성공적으로 수행되어야 커밋이 이루어질 수 있다.
60+
61+
* DB 성능은 서버 개발에서 가장 중요한 부분 중 하나입니다. Prisma를 이용해서 데이터베이스에 질의할 때, 각 SQL 쿼리가 얼마나 오래 소요되는지 로그를 남겨주세요. (쿼리를 실행하기 전의 시간을 측정하고, 쿼리를 실행한 이후의 시간을 측정하여 몇 ms가 소요되었는지 측정하여 `console.log`로 출력할 수 있습니다.) 가능하면 쿼리를 사용하는 부분마다 매번 `console.log`로 출력하기 보다는, 항상 공통으로 자동으로 적용될 수 있는 방법으로 구현해주세요.
62+
63+
* DB 성능은 서버 개발에서 가장 중요한 부분 중 하나입니다. Prisma를 이용해서 데이터베이스에 질의할 때, 각 SQL 쿼리가 얼마나 오래 소요되는지 로그를 남겨주세요. (쿼리를 실행하기 전의 시간을 측정하고, 쿼리를 실행한 이후의 시간을 측정하여 몇 ms가 소요되었는지 측정하여 `console.log`로 출력할 수 있습니다.) 가능하면 쿼리를 사용하는 부분마다 매번 `console.log`로 출력하기 보다는, 항상 공통으로 자동으로 적용될 수 있는 방법으로 구현해주세요.
64+
Prisma 공식문서에서는 두가지 방법으로 prisma에서 logging을 구현 할 수 있다고 한다.
65+
66+
1. Stdout에 로깅(기본값)
67+
2. 이벤트기반 로깅
68+
69+
1번 방식은 모든 로그레벨을 stdout으로 출력 가능하게 해준다. 가장 기본적이며 간단한 방법이고 logLevel객체에 이미 모든 로그레벨이 선언 되어 있어 간단하다.
70+
71+
```jsx
72+
const prisma = new PrismaClient({
73+
log: [
74+
{
75+
emit: 'stdout',
76+
level: 'query',
77+
},
78+
{
79+
emit: 'stdout',
80+
level: 'error',
81+
},
82+
{
83+
emit: 'stdout',
84+
level: 'info',
85+
},
86+
{
87+
emit: 'stdout',
88+
level: 'warn',
89+
},
90+
],
91+
})
92+
```
93+
2번방식은 prisma.$on() 메서드를 활용해 모든 이벤트를 감지하고 이벤트가 발생하면 직접 사용자가 로그를 출력할 수 있도록 한다.
94+
95+
```jsx
96+
prisma.$on('query', (e) => {
97+
console.log('Query: ' + e.query)
98+
console.log('Params: ' + e.params)
99+
console.log('Duration: ' + e.duration + 'ms')
100+
})
101+
```
102+
* 우리는 흔히 DB 쿼리에서 N+1 문제를 마주할 수 있습니다. 예를 들어, 게시글을 조회하면서 각 게시글에 해당하는 댓글들을 개별적으로 쿼리한다면, 댓글을 조회하는 쿼리가 각 게시글마다 추가로 발생하게 되어 N+1 문제가 발생합니다. 이를 Prisma에서 N+1문제를 해결할 수 있는 방법을 정리해주세요.
103+
104+
105+
N+1문제란 연관관계에서 발생하는 이슈로, 연관관계가 설정된 엔티티를 조회할때 조회된 데이터 개수만큼 조회 쿼리가 추가로 발생하게 되는데, 이를 N+1문제라 한다.
106+
107+
데이터가 많아질수록 쿼리 수가 기하급수적으로 증가하게 된다. 특히 ORM에서 많이 발생하게 된다.
108+
109+
해결 방법 두가지로 나눠서 생각 할 수 있다.
110+
111+
1. Eager Loading (즉시 로딩)
112+
데이터를 조회할 때 연관된 모든 객 체의 데이터까지 한 번에 불러 온다. 즉 위에서 에상된 문제인 게시글과 댓글을 한번에 조인해 불러오는 방식을 생각 할 수 있다.
113+
114+
```jsx
115+
const postsWithComments = await prisma.post.findMany({
116+
include: {
117+
comments: true, // 관계된 모든 댓글을 함께 가져옴
118+
},
119+
});
120+
```
121+
include방식으로 LEFT JOIN을 수행해 한번의 쿼리로 모든 데이터를 받아온다.
122+
123+
다만 너무 복잡한 관계인 경우에는 eager loading이 오히려 성능저하의 원인이 될 수 있다.
124+
2. Batch조회 이후 수동 매핑
125+
include로 처리할 수 없는 복잡한 관계일 경우, 수동 Batch로 해결 할 수 있다.
126+
127+
```jsx
128+
// 1. 게시글 먼저 가져오기
129+
const posts = await prisma.post.findMany();
130+
const postIds = posts.map(post => post.id);
131+
132+
// 2. 댓글을 postId 기준으로 한 번에 가져오기
133+
const comments = await prisma.comment.findMany({
134+
where: {
135+
postId: { in: postIds },
136+
},
137+
});
138+
139+
// 3. postId별로 댓글을 매핑
140+
const commentMap = new Map();
141+
comments.forEach(comment => {
142+
if (!commentMap.has(comment.postId)) {
143+
commentMap.set(comment.postId, []);
144+
}
145+
commentMap.get(comment.postId).push(comment);
146+
});
147+
148+
// 4. 게시글에 댓글 붙이기
149+
const postsWithComments = posts.map(post => ({
150+
...post,
151+
comments: commentMap.get(post.id) || [],
152+
}));
153+
```
154+
다음과 같은 쿼리로 조회 할 경우 쿼리가 2번만 실행된다.

0 commit comments

Comments
 (0)