Skip to content

Commit abdd3a2

Browse files
authored
Merge pull request #24 from rbdus0715/week6
[6주차/해피잭] 워크북 제출합니다.
2 parents d0d6428 + 2ed3917 commit abdd3a2

File tree

7 files changed

+354
-0
lines changed

7 files changed

+354
-0
lines changed

keyword/chapter06/1.png

703 KB
Loading

keyword/chapter06/2.png

396 KB
Loading

keyword/chapter06/3.png

567 KB
Loading

keyword/chapter06/4.png

237 KB
Loading

keyword/chapter06/README.md

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
- ORM
2+
ORM은 객체-관계 매핑으로, 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 테이블을 연결해주는 추상화 계층을 말한다.
3+
즉 SQL 쿼리를 직접 작성하지 않고, 코드 내에서 데이터베이스를 객체로 다루며 직관적으로 접근할 수 있다는 장점이 있다.
4+
### 장점
5+
- SQL을 직접 작성하는 부담이 줄고, 코드 가독성 및 일관성이 뛰어나다.
6+
- DB 스키마 변경에 대한 유지보수가 용이하다. ORM 프레임워크가 자동으로 변화에 대응해준다.
7+
- 개발 생산성이 향상되고, 비즈니스 로직에 집중할 수 있다.
8+
- DBMS에 대한 종속성이 줄며, 다양한 데이터베이스로 쉽게 이식 가능하다.
9+
- 객체 간의 관계를 코드에서 직관적으로 관리할 수 있다.
10+
### 단점
11+
- 대용량 트랜잭션이나 복잡한, 성능이 중요한 SQL은 ORM만으로 최적화하기 어렵다.
12+
- 모든 데이터베이스 기능을 지원하지 않아, 특수하거나 고도화된 쿼리는 직접 SQL로 처리해야 한다.
13+
- 프로젝트의 규모나 복잡성이 증가하면 ORM 구조가 설계 및 관리 측면에서 어려워질 수 있다.
14+
- DB와 객체 구조에 대한 높은 이해가 필요하다. 설계 실패 시 성능 저하나 데이터 불일치가 발생할 수 있다.
15+
- Prisma 문서 살펴보기
16+
- ex. Prisma의 Connection Pool 관리 방법
17+
### 쿼리 엔진이 커넥션 풀 이용하는 과정
18+
1. 쿼리 엔진이 connection pool size와 timeout 설정을 통해 pool을 인스턴스화한
19+
2. 쿼리 엔진은 하나의 connection을 생성해 연결 풀에 추가
20+
3. 쿼리가 들어오면 쿼리 엔진은 쿼리 처리하기 위해 연결을 예약
21+
4. 연결 풀에 사용 가능한 유휴 연결이 없으면 쿼리 엔진은 추가 데이터베이스 연결을 열고 데이터베이스 연결 수가 정의된 제한에 도달할 때까지 연결 풀에 추가 `connection_limit`
22+
5. 쿼리 엔진이 풀에서 연결 예약할 수 없을 때, 쿼리는 메모리의 FIFO 큐에 추가됨. (순서대로 처리하기 위해)
23+
6. 쿼리 엔진이 시간 제한 전에 큐에 있는 쿼리 처리할 수 없는 경우 해당 쿼리에 대한 오류 코드와 함계 예외 발생, 큐에있는 다음 쿼리로 넘어감
24+
### 연결 풀 크기
25+
default 크기: cpu 개수 \* 2 + 1
26+
혹은 connection_limit 매개변수를 통해 명시적으로 지정 가능
27+
```scheme
28+
datasource db {
29+
provider = "postgresql"
30+
url = "postgresql://johndoe:mypassword@localhost:5432/mydb?connection_limit=5"
31+
}
32+
```
33+
### 연결 풀 시간 초과
34+
기본 제한시간은 10초
35+
이 또한 명시적으로 설정 가능
36+
```jsx
37+
datasource db {
38+
provider = "postgresql"
39+
url = "postgresql://johndoe:mypassword@localhost:5432/mydb?connection_limit=5&pool_timeout=2"
40+
}
41+
// 혹은 0으로 설정하여 연결 풀 시간초과를 비활성화할 수 있다.
42+
```
43+
- ex. Prisma의 Migration 관리 방법
44+
### 마이그레이션 관리란?
45+
디비 스키마의 변경 이력을 체계적으로 추적하고 관리하며, 시간이 지남에 따라 디비 구조를 안정적으로 업데이트하고 동기화하는 프로세스
46+
### 마이그레이션 관리가 필요한 이유
47+
1. 버전 관리 및 추적: 특정 시점으로 롤백 가능
48+
2. 팀 협업: 여러 개발자가 동시에 디비 변경할 때 충돌 없이 변경사항 통합/적용 가능
49+
3. 환경 동기화: 개발, 테스트, 운영 환경 간의 디비 스키마를 일관성 있게 유지
50+
### Prisma에서 마이그레이션 관리하는 법
51+
schema.prisma 파일의 모델 정의를 기반으로 관리
52+
- prisma/migrations 디렉토리에 타임스탬프가 붙은 SQL 파일 형태로 저장
53+
- 디비 내부에 *prisma*migrations라는 특별 테이블 만들어, 어떤 마이그레이션 적용됐는지 기록
54+
- **프로세스 : 프리즈마 모델 수정 > prisma migrate dev > sql 파일 생성 및 디비 적용**
55+
```bash
56+
npx prisma migrate dev // 개발 환경에서 스키마 변경을 감지하고, 마이그레이션 파일 생성 및 즉시 적용.
57+
npx prisma migrate deploy // 배포 환경에서 이미 생성된 마이그레이션 파일을 안전하게 적용.
58+
npx prisma migrate reset // 데이터베이스를 초기화(모든 데이터 삭제)하고 마이그레이션 기록을 재설정.
59+
```
60+
- ORM(Prisma)을 사용하여 좋은 점과 나쁜 점
61+
## 장점
62+
- 생산성 향상 및 직관적인 코드: SQL 쿼리 대신 프로그래밍 언어의 메서드를 사용해 데이터를 조작할 수 있어 개발 속도가 빠르고 코드가 객체 지향적이며 직관적
63+
- 유지보수 용이성: 매핑 정보가 명확하고 코드가 독립적으로 작성되어 재사용성이 높고, SQL이 코드에 분산되지 않아 유지보수가 편리함
64+
- DBMS 종속성 감소: 대부분의 ORM은 여러 데이터베이스(DB)를 지원하여, DB를 교체할 때 코드 수정 범위를 최소화
65+
- 보안 강화: SQL 인젝션과 같은 보안 공격을 방지하는 Prepared Statement 사용
66+
## 단점
67+
- 성능 저하 가능성: 복잡한 쿼리(JOIN, 서브 쿼리 등)의 경우, ORM이 생성하는 SQL이 최적화되지 않아 직접 작성한 Raw SQL보다 성능이 떨어질 수 있음
68+
- 학습 곡선과 설계 난이도: ORM의 내부 동작 원리(N+1 문제 등)를 충분히 이해하지 못하면 오히려 성능 저하 및 데이터 일관성 문제 발생 위험
69+
- 완벽한 지원의 한계: 모든 상황을 ORM만으로 해결하기는 어려우며, 복잡하거나 극한의 성능이 필요한 쿼리는 결국 Raw SQL이나 저장 프로시저(Stored Procedure)를 사용해야 함
70+
## N+1 문제
71+
데이터베이스에서 연관된 데이터를 가져올 때, 하나의 쿼리로 전체 데이터를 가져오는 대신
72+
하나의 메인 쿼리 + 관련 데이터를 위한 N개의 추가 쿼리가 발생하는 문제
73+
### **include를 사용해서 해결하기**
74+
Prisma는 기본적으로 lazy loading이 아니라 eager loading을 지원
75+
include 옵션을 쓰면 관계 데이터를 한 번에 JOIN해서 가져올 수 있음
76+
```tsx
77+
const users = await prisma.user.findMany({
78+
include: {
79+
posts: true, // posts를 한 번에 가져옴
80+
},
81+
});
82+
```
83+
- 다양한 ORM 라이브러리 살펴보기
84+
## Prisma
85+
- TypeScript 완벽 지원, 타입 자동 생성
86+
- 선언적 스키마(shema.prisma)로 DB 구조 정의
87+
- 빠른 쿼리, 자동 마이그레이션, 직관적인 API
88+
- 장점: DX(개발자 경험) 최고, 타입 안정성 탁월
89+
- 단점: 복잡한 커스텀 SQL 쿼리에는 다소 제약
90+
## TypeORM
91+
- NestJS 공식 문서에서도 추천되는 전통적인 ORM
92+
- Active Record / Data Mapper 두 패턴 모두 지원
93+
- 데코레이터 기반 (@Entity(), @Column() 등)
94+
## Sequelize
95+
- 가장 오래되고 널리 사용된 Node.js ORM
96+
- Promise 기반 API, 트랜잭션/연관관계 잘 지원
97+
- 장점: 안정적, 문서 많음
98+
- 단점: 타입스크립트 지원이 부족함 (최근 개선 중)
99+
## Objection.js
100+
- Knex.js 기반 ORM (쿼리 빌더 + 모델 시스템)
101+
- SQL에 가까운 컨트롤을 제공하면서도 ORM의 장점 유지
102+
- 타입스크립트 호환성 좋음
103+
- 페이지네이션을 사용하는 다른 API 찾아보기
104+
- ex. https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28
105+
- ex. https://developers.notion.com/reference/intro#pagination
106+
## 링크 헤더 기반 페이지네이션
107+
- http 헤더에 다음, 이전, 첫번째, 마지막 페이지로이동할 수 있는 완전한 URL 링크 제공
108+
- 사용되는 매개변수
109+
- page, before/after, since : link 헤더에 포함된 url 자체를 사용해 요청
110+
- per_page: 한 페이지에 반환되는 결과 수 제어
111+
- 응답 시 주요 필드
112+
- link 헤더는 응답헤더에 포함되고, URL을 제공한다.
113+
- 작동 방식
114+
1. 요청
115+
2. 응답 헤더에서 Link 헤더 확인
116+
3. rel=”next”에 해당하는 URL을 사용해 다음 요청
117+
4. 반복

keyword/chapter06/mission.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# 1. orm 사용해보기
2+
3+
## 1-1) npx prisma db pull
4+
5+
![image.png](1.png)
6+
7+
## 1-2) 기존 API 호환 확인 > 정상 작동
8+
9+
![image.png](2.png)
10+
11+
# 2. 마이그레이션
12+
13+
![image.png](3.png)
14+
15+
# 3. 목록 조회하기 (페이지네이션)
16+
17+
![image.png](4.png)
18+
19+
```tsx
20+
async findByRestaurantId(restaurant_id: string, cursor: string) {
21+
const reviews = await this.prisma.review.findMany({
22+
select: {
23+
id: true,
24+
content: true,
25+
restaurant: true,
26+
user: true,
27+
},
28+
where: { restaurant_id, id: { gt: cursor } },
29+
orderBy: { id: 'asc' },
30+
take: 5,
31+
});
32+
return responseFromReviews(reviews); // dto 적용하여 다음 커서 추가 후 반환
33+
}
34+
```

mission/chapter06/README.md

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
- 한 번에 여러 번의 DB 작업을 연달아 처리할 때, 중간에 처리가 실패했는데 DB에는 중간까지만 값이 반영되어 있으면 문제가 있을 것 같습니다. 이를 방지하는 기술로는 Transaction이 있는데, Prisma를 이용해 Transaction을 관리하는 방법을 찾아 정리해주세요. 워크북의 실습 프로젝트에서도 적용할 수 있다면 적용해주세요.
2+
3+
# 트랜잭션과 배치쿼리
4+
5+
총 3가지 시나리오에 따라 6가지 트랜잭션 처리 기법 제공
6+
7+
- 의존적 쓰기: Nested writes
8+
- 독립적 쓰기: Bluk 연산, $transaction([]) api
9+
- 읽기-수정-쓰기: Idempotent APIs, Optimistic concurrency control, Interactive transaction
10+
11+
## Nested Writes
12+
13+
여러 관련된 레코드를 한 번의 api 호출로 처리하는 방법
14+
15+
```tsx
16+
// 사용자와 게시글을 동시에 생성
17+
const newUser = await prisma.user.create({
18+
data: {
19+
20+
posts: {
21+
create: [
22+
{ title: 'Join the Prisma Discord' },
23+
{ title: 'Follow @prisma on Twitter' }
24+
]
25+
}
26+
}
27+
})
28+
```
29+
30+
## Bulk 연산
31+
32+
```tsx
33+
// 모든 읽지 않은 이메일을 읽음 처리
34+
await prisma.email.updateMany({
35+
where: {
36+
userId: 10,
37+
unread: true
38+
},
39+
data: {
40+
unread: false
41+
}
42+
})
43+
```
44+
45+
같은 타입의 여러 레코드를 한 번에 처리한다.
46+
47+
- createMany()
48+
- updateMany()
49+
- deleteMany()
50+
- createManyAndReturn()
51+
- updateManyAndReturn()
52+
53+
## $transaction([])
54+
55+
여러 프리즈마 쿼리를 배열로 묶어 순차적으로 실행한다.
56+
57+
```tsx
58+
const [posts, totalPosts] = await prisma.$transaction([
59+
prisma.post.findMany({where: {title: {contains: 'prisma'} } }}),
60+
prisma.post.count()
61+
]);
62+
```
63+
64+
## 대화형 트랜잭션
65+
66+
복잡한 로직을 포함한 트랜잭션 처리가 필요할 때 사용한다.
67+
68+
$transaction() 안에 비동기 콜백 함수가 들어가서 일련의 비즈니스 로직 과정을 처리한다.
69+
70+
```tsx
71+
async function transfer(from: string, to: string, amount: number) {
72+
return await prisma.$transaction(async (tx) => {
73+
// 1. 송신자 잔액 차감
74+
const sender = await tx.account.update({
75+
data: { balance: { decrement: amount } },
76+
where: { email: from }
77+
})
78+
79+
// 2. 잔액 검증
80+
if (sender.balance < 0) {
81+
throw new Error(`${from}의 잔액이 부족합니다`)
82+
}
83+
84+
// 3. 수신자 잔액 증가
85+
const recipient = await tx.account.update({
86+
data: { balance: { increment: amount } },
87+
where: { email: to }
88+
})
89+
90+
return recipient
91+
})
92+
}
93+
```
94+
95+
96+
- 우리는 흔히 DB 쿼리에서 N+1 문제를 마주할 수 있습니다. 예를 들어, 게시글을 조회하면서 각 게시글에 해당하는 댓글들을 개별적으로 쿼리한다면, 댓글을 조회하는 쿼리가 각 게시글마다 추가로 발생하게 되어 N+1 문제가 발생합니다. 이를 Prisma에서 N+1문제를 해결할 수 있는 방법을 정리해주세요.
97+
98+
## 1. include - eager loading(즉시 로딩)
99+
100+
```tsx
101+
const users = await prisma.user.findMany({
102+
include: {
103+
posts: true,
104+
},
105+
});
106+
```
107+
108+
장점
109+
110+
- 코드 간단
111+
- Prisma 내부에서 SQL join, in 쿼리로 최적화 수행
112+
- 대부분의 단순한 N+1 문제는 이걸로 해결 가능
113+
114+
단점
115+
116+
- 항상 데이터를 같이 불러오므로, 불필요한 데이터까지 가져올 수 있음
117+
- 중첩 관계가 깊을수록 쿼리 복잡도 증가 (JOIN 과다)
118+
119+
## 2. dataloader - batching & caching
120+
121+
GraphQL 서버나 NestJS 등에서 자주 사용되는 접근법
122+
123+
비슷한 쿼리들을 한 번에 묶어 Batch Query로 처리
124+
125+
```tsx
126+
import DataLoader from 'dataloader';
127+
import { prisma } from '../client';
128+
129+
// 1. 로더 생성
130+
const postLoader = new DataLoader(async (userIds: string[]) => {
131+
const posts = await prisma.post.findMany({
132+
where: { userId: { in: userIds } },
133+
});
134+
135+
// userId별로 그룹핑
136+
const postMap = userIds.map(
137+
(id) => posts.filter((p) => p.userId === id)
138+
);
139+
return postMap;
140+
});
141+
142+
// 2. 서비스/리졸버 내에서 사용
143+
const users = await prisma.user.findMany();
144+
const usersWithPosts = await Promise.all(
145+
users.map(async (u) => ({
146+
...u,
147+
posts: await postLoader.load(u.id),
148+
}))
149+
);
150+
151+
```
152+
153+
장점
154+
155+
- GraphQL과 궁합이 good (@nestjs/graphql)
156+
- 캐싱 및 배칭으로 동일 쿼리 중복 방지
157+
158+
단점: Prisma 단독 사용 시엔 약간 과한 구조일 수 있음
159+
160+
## 3. FluentAPI
161+
162+
Prisma 5에서 도입됨
163+
164+
쿼리를 체이닝 방식으로 최적화
165+
166+
```tsx
167+
const users = await prisma.user
168+
.fluent() // Fluent API 시작
169+
.include('posts')
170+
.where({ isActive: true })
171+
.orderBy({ createdAt: 'desc' })
172+
.take(10)
173+
.exec(); // 최종 실행
174+
```
175+
176+
또는 custom include 로직을 재사용 가능하게 구성
177+
178+
```tsx
179+
const withPosts = prisma.$extends({
180+
model: {
181+
user: {
182+
withPosts() {
183+
return this.findMany({
184+
include: { posts: true },
185+
});
186+
},
187+
},
188+
},
189+
});
190+
// 사용
191+
const users = await withPosts.user.withPosts();
192+
```
193+
194+
장점
195+
196+
- 중첩 include를 깔끔하게 재사용 가능
197+
- N+1 해결 로직을 하나의 Fluent Chain으로 관리 가능
198+
- Service 계층에서 중복 쿼리 방지
199+
200+
단점
201+
202+
- 아직 비교적 새로운 기능
203+
- 복잡한 관계의 자동 batching은 직접 제어해야 함 (DataLoader만큼 자동화되진 않음)

0 commit comments

Comments
 (0)