Skip to content

Commit 01d8f79

Browse files
committed
mission : 미션수행
1 parent ede9ff8 commit 01d8f79

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed

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)