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
Binary file added keyword/chapter06/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added keyword/chapter06/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added keyword/chapter06/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added keyword/chapter06/4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
117 changes: 117 additions & 0 deletions keyword/chapter06/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
- ORM
ORM은 객체-관계 매핑으로, 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스의 테이블을 연결해주는 추상화 계층을 말한다.
즉 SQL 쿼리를 직접 작성하지 않고, 코드 내에서 데이터베이스를 객체로 다루며 직관적으로 접근할 수 있다는 장점이 있다.
### 장점
- SQL을 직접 작성하는 부담이 줄고, 코드 가독성 및 일관성이 뛰어나다.
- DB 스키마 변경에 대한 유지보수가 용이하다. ORM 프레임워크가 자동으로 변화에 대응해준다.
- 개발 생산성이 향상되고, 비즈니스 로직에 집중할 수 있다.
- DBMS에 대한 종속성이 줄며, 다양한 데이터베이스로 쉽게 이식 가능하다.
- 객체 간의 관계를 코드에서 직관적으로 관리할 수 있다.
### 단점
- 대용량 트랜잭션이나 복잡한, 성능이 중요한 SQL은 ORM만으로 최적화하기 어렵다.
- 모든 데이터베이스 기능을 지원하지 않아, 특수하거나 고도화된 쿼리는 직접 SQL로 처리해야 한다.
- 프로젝트의 규모나 복잡성이 증가하면 ORM 구조가 설계 및 관리 측면에서 어려워질 수 있다.
- DB와 객체 구조에 대한 높은 이해가 필요하다. 설계 실패 시 성능 저하나 데이터 불일치가 발생할 수 있다.
- Prisma 문서 살펴보기
- ex. Prisma의 Connection Pool 관리 방법
### 쿼리 엔진이 커넥션 풀 이용하는 과정
1. 쿼리 엔진이 connection pool size와 timeout 설정을 통해 pool을 인스턴스화한
2. 쿼리 엔진은 하나의 connection을 생성해 연결 풀에 추가
3. 쿼리가 들어오면 쿼리 엔진은 쿼리 처리하기 위해 연결을 예약
4. 연결 풀에 사용 가능한 유휴 연결이 없으면 쿼리 엔진은 추가 데이터베이스 연결을 열고 데이터베이스 연결 수가 정의된 제한에 도달할 때까지 연결 풀에 추가 `connection_limit`
5. 쿼리 엔진이 풀에서 연결 예약할 수 없을 때, 쿼리는 메모리의 FIFO 큐에 추가됨. (순서대로 처리하기 위해)
6. 쿼리 엔진이 시간 제한 전에 큐에 있는 쿼리 처리할 수 없는 경우 해당 쿼리에 대한 오류 코드와 함계 예외 발생, 큐에있는 다음 쿼리로 넘어감
### 연결 풀 크기
default 크기: cpu 개수 \* 2 + 1
혹은 connection_limit 매개변수를 통해 명시적으로 지정 가능
```scheme
datasource db {
provider = "postgresql"
url = "postgresql://johndoe:mypassword@localhost:5432/mydb?connection_limit=5"
}
```
### 연결 풀 시간 초과
기본 제한시간은 10초
이 또한 명시적으로 설정 가능
```jsx
datasource db {
provider = "postgresql"
url = "postgresql://johndoe:mypassword@localhost:5432/mydb?connection_limit=5&pool_timeout=2"
}
// 혹은 0으로 설정하여 연결 풀 시간초과를 비활성화할 수 있다.
```
- ex. Prisma의 Migration 관리 방법
### 마이그레이션 관리란?
디비 스키마의 변경 이력을 체계적으로 추적하고 관리하며, 시간이 지남에 따라 디비 구조를 안정적으로 업데이트하고 동기화하는 프로세스
### 마이그레이션 관리가 필요한 이유
1. 버전 관리 및 추적: 특정 시점으로 롤백 가능
2. 팀 협업: 여러 개발자가 동시에 디비 변경할 때 충돌 없이 변경사항 통합/적용 가능
3. 환경 동기화: 개발, 테스트, 운영 환경 간의 디비 스키마를 일관성 있게 유지
### Prisma에서 마이그레이션 관리하는 법
schema.prisma 파일의 모델 정의를 기반으로 관리
- prisma/migrations 디렉토리에 타임스탬프가 붙은 SQL 파일 형태로 저장
- 디비 내부에 *prisma*migrations라는 특별 테이블 만들어, 어떤 마이그레이션 적용됐는지 기록
- **프로세스 : 프리즈마 모델 수정 > prisma migrate dev > sql 파일 생성 및 디비 적용**
```bash
npx prisma migrate dev // 개발 환경에서 스키마 변경을 감지하고, 마이그레이션 파일 생성 및 즉시 적용.
npx prisma migrate deploy // 배포 환경에서 이미 생성된 마이그레이션 파일을 안전하게 적용.
npx prisma migrate reset // 데이터베이스를 초기화(모든 데이터 삭제)하고 마이그레이션 기록을 재설정.
```
- ORM(Prisma)을 사용하여 좋은 점과 나쁜 점
## 장점
- 생산성 향상 및 직관적인 코드: SQL 쿼리 대신 프로그래밍 언어의 메서드를 사용해 데이터를 조작할 수 있어 개발 속도가 빠르고 코드가 객체 지향적이며 직관적
- 유지보수 용이성: 매핑 정보가 명확하고 코드가 독립적으로 작성되어 재사용성이 높고, SQL이 코드에 분산되지 않아 유지보수가 편리함
- DBMS 종속성 감소: 대부분의 ORM은 여러 데이터베이스(DB)를 지원하여, DB를 교체할 때 코드 수정 범위를 최소화
- 보안 강화: SQL 인젝션과 같은 보안 공격을 방지하는 Prepared Statement 사용
## 단점
- 성능 저하 가능성: 복잡한 쿼리(JOIN, 서브 쿼리 등)의 경우, ORM이 생성하는 SQL이 최적화되지 않아 직접 작성한 Raw SQL보다 성능이 떨어질 수 있음
- 학습 곡선과 설계 난이도: ORM의 내부 동작 원리(N+1 문제 등)를 충분히 이해하지 못하면 오히려 성능 저하 및 데이터 일관성 문제 발생 위험
- 완벽한 지원의 한계: 모든 상황을 ORM만으로 해결하기는 어려우며, 복잡하거나 극한의 성능이 필요한 쿼리는 결국 Raw SQL이나 저장 프로시저(Stored Procedure)를 사용해야 함
## N+1 문제
데이터베이스에서 연관된 데이터를 가져올 때, 하나의 쿼리로 전체 데이터를 가져오는 대신
하나의 메인 쿼리 + 관련 데이터를 위한 N개의 추가 쿼리가 발생하는 문제
### **include를 사용해서 해결하기**
Prisma는 기본적으로 lazy loading이 아니라 eager loading을 지원
include 옵션을 쓰면 관계 데이터를 한 번에 JOIN해서 가져올 수 있음
```tsx
const users = await prisma.user.findMany({
include: {
posts: true, // posts를 한 번에 가져옴
},
});
```
- 다양한 ORM 라이브러리 살펴보기
## Prisma
- TypeScript 완벽 지원, 타입 자동 생성
- 선언적 스키마(shema.prisma)로 DB 구조 정의
- 빠른 쿼리, 자동 마이그레이션, 직관적인 API
- 장점: DX(개발자 경험) 최고, 타입 안정성 탁월
- 단점: 복잡한 커스텀 SQL 쿼리에는 다소 제약
## TypeORM
- NestJS 공식 문서에서도 추천되는 전통적인 ORM
- Active Record / Data Mapper 두 패턴 모두 지원
- 데코레이터 기반 (@Entity(), @Column() 등)
## Sequelize
- 가장 오래되고 널리 사용된 Node.js ORM
- Promise 기반 API, 트랜잭션/연관관계 잘 지원
- 장점: 안정적, 문서 많음
- 단점: 타입스크립트 지원이 부족함 (최근 개선 중)
## Objection.js
- Knex.js 기반 ORM (쿼리 빌더 + 모델 시스템)
- SQL에 가까운 컨트롤을 제공하면서도 ORM의 장점 유지
- 타입스크립트 호환성 좋음
- 페이지네이션을 사용하는 다른 API 찾아보기
- ex. https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api?apiVersion=2022-11-28
- ex. https://developers.notion.com/reference/intro#pagination
## 링크 헤더 기반 페이지네이션
- http 헤더에 다음, 이전, 첫번째, 마지막 페이지로이동할 수 있는 완전한 URL 링크 제공
- 사용되는 매개변수
- page, before/after, since : link 헤더에 포함된 url 자체를 사용해 요청
- per_page: 한 페이지에 반환되는 결과 수 제어
- 응답 시 주요 필드
- link 헤더는 응답헤더에 포함되고, URL을 제공한다.
- 작동 방식
1. 요청
2. 응답 헤더에서 Link 헤더 확인
3. rel=”next”에 해당하는 URL을 사용해 다음 요청
4. 반복
34 changes: 34 additions & 0 deletions keyword/chapter06/mission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 1. orm 사용해보기

## 1-1) npx prisma db pull

![image.png](1.png)

## 1-2) 기존 API 호환 확인 > 정상 작동

![image.png](2.png)

# 2. 마이그레이션

![image.png](3.png)

# 3. 목록 조회하기 (페이지네이션)

![image.png](4.png)

```tsx
async findByRestaurantId(restaurant_id: string, cursor: string) {
const reviews = await this.prisma.review.findMany({
select: {
id: true,
content: true,
restaurant: true,
user: true,
},
where: { restaurant_id, id: { gt: cursor } },
orderBy: { id: 'asc' },
take: 5,
});
return responseFromReviews(reviews); // dto 적용하여 다음 커서 추가 후 반환
}
```
203 changes: 203 additions & 0 deletions mission/chapter06/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
- 한 번에 여러 번의 DB 작업을 연달아 처리할 때, 중간에 처리가 실패했는데 DB에는 중간까지만 값이 반영되어 있으면 문제가 있을 것 같습니다. 이를 방지하는 기술로는 Transaction이 있는데, Prisma를 이용해 Transaction을 관리하는 방법을 찾아 정리해주세요. 워크북의 실습 프로젝트에서도 적용할 수 있다면 적용해주세요.

# 트랜잭션과 배치쿼리

총 3가지 시나리오에 따라 6가지 트랜잭션 처리 기법 제공

- 의존적 쓰기: Nested writes
- 독립적 쓰기: Bluk 연산, $transaction([]) api
- 읽기-수정-쓰기: Idempotent APIs, Optimistic concurrency control, Interactive transaction

## Nested Writes

여러 관련된 레코드를 한 번의 api 호출로 처리하는 방법

```tsx
// 사용자와 게시글을 동시에 생성
const newUser = await prisma.user.create({
data: {
email: 'alice@prisma.io',
posts: {
create: [
{ title: 'Join the Prisma Discord' },
{ title: 'Follow @prisma on Twitter' }
]
}
}
})
```

## Bulk 연산

```tsx
// 모든 읽지 않은 이메일을 읽음 처리
await prisma.email.updateMany({
where: {
userId: 10,
unread: true
},
data: {
unread: false
}
})
```

같은 타입의 여러 레코드를 한 번에 처리한다.

- createMany()
- updateMany()
- deleteMany()
- createManyAndReturn()
- updateManyAndReturn()

## $transaction([])

여러 프리즈마 쿼리를 배열로 묶어 순차적으로 실행한다.

```tsx
const [posts, totalPosts] = await prisma.$transaction([
prisma.post.findMany({where: {title: {contains: 'prisma'} } }}),
prisma.post.count()
]);
```

## 대화형 트랜잭션

복잡한 로직을 포함한 트랜잭션 처리가 필요할 때 사용한다.

$transaction() 안에 비동기 콜백 함수가 들어가서 일련의 비즈니스 로직 과정을 처리한다.

```tsx
async function transfer(from: string, to: string, amount: number) {
return await prisma.$transaction(async (tx) => {
// 1. 송신자 잔액 차감
const sender = await tx.account.update({
data: { balance: { decrement: amount } },
where: { email: from }
})

// 2. 잔액 검증
if (sender.balance < 0) {
throw new Error(`${from}의 잔액이 부족합니다`)
}

// 3. 수신자 잔액 증가
const recipient = await tx.account.update({
data: { balance: { increment: amount } },
where: { email: to }
})

return recipient
})
}
```


- 우리는 흔히 DB 쿼리에서 N+1 문제를 마주할 수 있습니다. 예를 들어, 게시글을 조회하면서 각 게시글에 해당하는 댓글들을 개별적으로 쿼리한다면, 댓글을 조회하는 쿼리가 각 게시글마다 추가로 발생하게 되어 N+1 문제가 발생합니다. 이를 Prisma에서 N+1문제를 해결할 수 있는 방법을 정리해주세요.

## 1. include - eager loading(즉시 로딩)

```tsx
const users = await prisma.user.findMany({
include: {
posts: true,
},
});
```

장점

- 코드 간단
- Prisma 내부에서 SQL join, in 쿼리로 최적화 수행
- 대부분의 단순한 N+1 문제는 이걸로 해결 가능

단점

- 항상 데이터를 같이 불러오므로, 불필요한 데이터까지 가져올 수 있음
- 중첩 관계가 깊을수록 쿼리 복잡도 증가 (JOIN 과다)

## 2. dataloader - batching & caching

GraphQL 서버나 NestJS 등에서 자주 사용되는 접근법

비슷한 쿼리들을 한 번에 묶어 Batch Query로 처리

```tsx
import DataLoader from 'dataloader';
import { prisma } from '../client';

// 1. 로더 생성
const postLoader = new DataLoader(async (userIds: string[]) => {
const posts = await prisma.post.findMany({
where: { userId: { in: userIds } },
});

// userId별로 그룹핑
const postMap = userIds.map(
(id) => posts.filter((p) => p.userId === id)
);
return postMap;
});

// 2. 서비스/리졸버 내에서 사용
const users = await prisma.user.findMany();
const usersWithPosts = await Promise.all(
users.map(async (u) => ({
...u,
posts: await postLoader.load(u.id),
}))
);

```

장점

- GraphQL과 궁합이 good (@nestjs/graphql)
- 캐싱 및 배칭으로 동일 쿼리 중복 방지

단점: Prisma 단독 사용 시엔 약간 과한 구조일 수 있음

## 3. FluentAPI

Prisma 5에서 도입됨

쿼리를 체이닝 방식으로 최적화

```tsx
const users = await prisma.user
.fluent() // Fluent API 시작
.include('posts')
.where({ isActive: true })
.orderBy({ createdAt: 'desc' })
.take(10)
.exec(); // 최종 실행
```

또는 custom include 로직을 재사용 가능하게 구성

```tsx
const withPosts = prisma.$extends({
model: {
user: {
withPosts() {
return this.findMany({
include: { posts: true },
});
},
},
},
});
// 사용
const users = await withPosts.user.withPosts();
```

장점

- 중첩 include를 깔끔하게 재사용 가능
- N+1 해결 로직을 하나의 Fluent Chain으로 관리 가능
- Service 계층에서 중복 쿼리 방지

단점

- 아직 비교적 새로운 기능
- 복잡한 관계의 자동 batching은 직접 제어해야 함 (DataLoader만큼 자동화되진 않음)