-
Notifications
You must be signed in to change notification settings - Fork 3
monorepo 변경 후 DataSource 주입 실패 (작성 중)
현재 저희 프로젝트에서는 typeORM을 사용하여 데이터베이스에 접근하고 있습니다.
그리고 Repository 계층을 커스텀하기 위해 다음과 같이 Repository를 extends하여 사용 중입니다.
import { DataSource, Repository } from 'typeorm';
import { Node } from './node.entity';
import { Injectable } from '@nestjs/common';
@Injectable()
export class NodeRepository extends Repository<Node> {
constructor(private dataSource: DataSource) {
super(Node, dataSource.createEntityManager());
}
async findById(id: number): Promise<Node | null> {
return await this.findOneBy({ id });
}
}
그리고 기존 구조에서 turbo를 사용하여 monorepo 구조로 변경하였습니다.
변경한 이후 각 workspace(frontend, backend) 별로 node_modules가 존재하고 root에도 node_modules가 존재합니다.
그런데 이렇게 monorepo 구조로 변경한 뒤 DataSource가 주입되지 않는 이슈가 발생했습니다.
이 이슈의 원인을 파악하기 위해서 @nestjs/typeorm과 typeorm의 차이점에 대해 알아야 합니다.
TypeORM은 Node.js용 객체 관계형 매핑(ORM) 패키지이고 엔티티와 테이블 간 매핑을 담당합니다.
@nestjs/typeorm은 nest.js의 DI 컨테이너와 호환되도록 typeorm의 인스턴스를 관리합니다.
@nestjs/typeorm은 typeorm에 의존하기 때문에 nest.js에서 typeorm을 사용하려면 두 패키지를 함께 설치해야 합니다.
yarn add @nestjs/typeorm typeorm
DataSource를 주입받기 위해서 TypeOrmModule의 forRoot 메소드를 통해 데이터베이스에 대한 설정을 해야 합니다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'root',
database: 'test',
entities: [],
synchronize: true,
}),
],
})
export class AppModule {}
다음은 공식 문서의 forRoot 메소드에 대한 설명입니다.
The forRoot() method supports all the configuration properties exposed by the DataSource constructor from the TypeORM package. In addition, there are several extra configuration properties described below.
위 설명을 통해 @nestjs/typeorm 패키지는 typeorm 패키지에 의존하는 것을 알 수 있습니다.
위 설정이 끝난다면 프로젝트 내 모든 곳에서 typeorm의 DataSource와 EntityManager를 주입받을 수 있습니다.
그리고 Repository를 사용하기 위해서 해당 모듈에서 forFeature를 사용하여 어떠한 엔티티에 대한 레포지토리를 사용할 지 알려줘야 합니다.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
controllers: [UsersController],
})
export class UsersModule {}
위 설정을 마쳤다면 Repository를 커스텀 할 수 있습니다.
현재 저희 프로젝트에서 사용하는 엔티티는 Node, Page, Edge가 있고 모두 Repository를 커스텀해서 사용 중입니다.
아래는 저희 프로젝트에서 사용하는 NodeRepository입니다.
import { DataSource, Repository } from 'typeorm';
import { Node } from './node.entity';
import { Injectable } from '@nestjs/common';
@Injectable()
export class NodeRepository extends Repository<Node> {
constructor(private dataSource: DataSource) {
super(Node, dataSource.createEntityManager());
}
async findById(id: number): Promise<Node | null> {
return await this.findOneBy({ id });
}
}
설정을 모두 마쳤다면 DataSource는 의존성 주입이 성공적으로 이루어져야 합니다.
실제로 위 코드에서 DataSource는 의존성 주입이 잘 되었으나 monorepo로 변경한 다음 의존성 주입이 이루어지지 않았습니다.
workspace가 생기면서 의존성에 문제가 생긴 것으로 추측하였고 정확한 원인 파악을 위해 monorepo의 hoisting에 대해 알아야 합니다.
package.json에 workspace를 정의하여 apps 디렉토리 내부의 모든 디렉토리는 workspace로 인식합니다.
{ ...
"workspaces": [
"apps/*"
],
...
}
저희 프로젝트에서는 apps라는 디렉토리 내부에 backend와 frontend workspace를 생성했고 각 workspace 내부에는 package.json이 존재합니다.
이 상태에서 backend 디렉토리에 typeorm을 설치해보겠습니다.
yarn add typeorm
typeorm을 backend 디렉토리에서 생성했지만 backend 디렉토리 내부의 node_modules에 존재하지 않고 루트 디렉토리 내부의 node_modules에 존재하는 것을 확인할 수 있습니다.
이렇게 monorepo를 구성할 때 종속성이 루트 디렉토리에 설치되는 것을 hoisting이라고 합니다.
그렇다면 backend 디렉토리에서는 어떻게 루트 디렉토리에 있는 패키지를 사용할 수 있을까요?
node.js에서는 module을 import 할 때 현재 디렉토리부터 부모 디렉토리를 거쳐 루트 디렉토리까지 모든 node_modules를 확인합니다.
즉 backend 디렉토리 내부의 node_modules에는 패키지가 존재하지 않지만 루트 디렉토리의 node_modules에는 패키지가 존재하기 때문에 해당 패키지를 import 할 수 있습니다.
그렇다면 backend 디렉토리 내부에서 이미 설치한 패키지와 다른 버전의 패키지를 동일하게 설치한다면 어떻게 될까요?
typeorm 1.0.0 버전을 설치하고 typeorm 2.0.0 버전을 설치한다면 어떻게 될까요?
node.js가 패키지를 import 하는 과정을 생각해보면 쉽게 이해할 수 있습니다.
typeorm 1.0.0 버전은 hoisting이 발생하여 루트 디렉토리 내부에 설치되고 typeorm 2.0.0 버전은 backend 디렉토리 내부에 설치됩니다.
backend 디렉토리는 가장 가까운 node_modules를 참고하기 때문에 2.0.0 버전을 import 하고 다른 workspace에서는 루트 디렉토리의 1.0.0 버전을 import 하게 됩니다.
그렇다면 지금까지 학습한 내용을 정리해보겠습니다.
- @nestjs/typeorm은 typeorm을 import 하여 DataSource를 의존성 주입할 수 있게 도와준다.
- monorepo에서 의존성을 설치하면 hoisting이 발생하여 루트 디렉토리에 생성된다.
- 이미 생성된 패키지의 다른 버전을 생성하면 해당 workspace 디렉토리 내부에 생성된다.
monorepo 구조로 변경한 뒤 DataSource 주입이 되지 않았던 이유는 서로 다른 버전의 typeorm이 루트 디렉토리와 backend 디렉토리 내부에 존재했기 때문입니다.
그런데 @nestjs/typeorm의 경우 오직 루트 디렉토리에만 존재했습니다.
@nestjs/typeorm은 내부적으로 typeorm을 import 하여 사용합니다.
즉 @nestjs/typeorm 패키지를 사용하여 데이터베이스 설정을 했을 때 @nestjs/typeorm은 가장 가까운 루트 디렉토리의 typeorm의 DataSource를 생성하여 DI 컨테이너에 등록합니다.
DataSource를 성공적으로 주입하기 위해서는 루트 디렉토리 typeorm의 DataSource를 사용해야 합니다.
하지만 backend 디렉토리에서 typeorm을 import 할 경우 루트 디렉토리가 아닌 가장 가까운 backend 디렉토리에 설치된 DataSource를 가져옵니다.
루트 디렉토리 DataSource와 백엔드 디렉토리 DataSource는 분명히 다른 클래스이기 때문에 주입에 실패하는 현상이 발생한 것이었습니다.
이제 다시 node repository 코드를 봅시다.
apps/backend/src/node/node.repository.ts
import { DataSource, Repository } from 'typeorm';
import { Node } from './node.entity';
import { Injectable } from '@nestjs/common';
@Injectable()
export class NodeRepository extends Repository<Node> {
constructor(private dataSource: DataSource) {
super(Node, dataSource.createEntityManager());
}
async findById(id: number): Promise<Node | null> {
return await this.findOneBy({ id });
}
}
위에서 DataSource는 백엔드 디렉토리 내부 node_modules의 typeorm에서 가져온 것이지만 DI 컨테이너에 등록된 DataSource는 루트 디렉토리 내부 node_modules의 typeorm에서 가져온 것이기 때문에 주입에 실패합니다.
해결 방안은 간단합니다.
@nestjs/typeorm이 사용하는 typeorm과 백엔드 디렉토리에서 사용하는 typeorm을 같은 것을 사용하면 됩니다.
첫 번째 시도한 방법입니다.
nest.js에게 의존성 주입을 맡기지 않고 DataSource를 주입합니다.
import { DataSource, Repository } from 'typeorm';
import { Node } from './node.entity';
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
@Injectable()
export class NodeRepository extends Repository<Node> {
constructor(@InjectDataSource() private dataSource: DataSource) {
super(Node, dataSource.createEntityManager());
}
async findById(id: number): Promise<Node | null> {
return await this.findOneBy({ id });
}
}
@nestjs/typeorm에서 InjectDataSource를 통해 DataSource를 주입하면 이 DataSource는 자연스럽게 @nestjs/typeorm이 존재하는 위치의 node_modules부터 탐색하기 때문에 같은 DataSource를 사용하게 됩니다.
하지만 이는 임시방편이고 근본적인 해결책은 아닙니다.
근본적으로 해결하기 위해서는 @nestjs/typeorm과 backend 디렉토리가 사용하는 typeorm을 통일시켜야 합니다.
처음에는 루트 package.json과 backend package.json에 버전이 다른 typeorm이 설치되었을 것이라고 생각했습니다.
하지만 오직 backend package.json에만 typeorm이 존재했습니다.
혹시나 해서 yarn이 아닌 npm으로 설치했더니 이번에는 중복 설치가 발생하지 않고 루트 디렉토리에만 typeorm이 설치가 되어 DataSource가 정상적으로 주입되었습니다.
https://toss.tech/article/node-modules-and-yarn-berry https://toss.tech/article/lightning-talks-package-manager https://medium.com/@designdevelop/yarn-workspaces-%EB%AA%A8%EB%85%B8%EB%A0%88%ED%8F%AC-%EB%8F%84%EC%9E%85%EA%B8%B0-c0310ca41c0e https://docs.nestjs.com/techniques/database https://classic.yarnpkg.com/blog/2018/02/15/nohoist/
⚓️ 사용자 피드백과 버그 기록
👷🏻 기술적 도전
📖 위키와 학습정리
✏️ 에디터
Novel이란?
Novel 스타일링 문제
에디터 저장 및 고려 사항들
📠 실시간 협업, 통신
Yorkie와 Novel editor 연동
YJS, Websocket, React-Flow
YJS, Socket.io
WebSocket과 Socket.io에 대해 간단히 알아보기
YJS 가이드 근데 이제 Socket.io를 곁들인
🏗️ 인프라와 CI/CD
NCloud CI CD 구축
BE 개발 스택과 기술적 고민
private key로 원격 서버 접근
nCloud 서버, VPC 만들고 설정
monorepo로 변경
⌛ 캐시, 최적화
rabbit mq 사용법
🔑 인증, 인가, 보안
passport로 oAuth 로그인 회원가입 구현
FE 로그인 기능 구현
JWT로 인증 인가 구현
JWT 쿠키로 사용하기
refresh token 보완하기
🧸 팀원 소개
⛺️ 그라운드 룰
🍞 커밋 컨벤션
🧈 이슈, PR 컨벤션
🥞 브랜치 전략
🌤️ 데일리 스크럼
📑 회의록
1️⃣ 1주차
킥오프(10/25)
2일차(10/29)
3일차(10/30)
4일차(10/31)
2️⃣ 2주차
8일차(11/04)
9일차(11/05)
11일차(11/07)
13일차(11/09)
3️⃣ 3주차
3주차 주간계획(11/11)
16일차(11/12)
18일차(11/14)
4️⃣ 4주차
4주차 주간계획(11/18)
23일차(11/19)
24일차(11/20)
25일차(11/21)
5️⃣ 5주차
5주차 주간계획(11/25)
29일차(11/25)
32일차(11/28)
34일차(11/30)
6️⃣ 6주차
6주차 주간계획(12/2)
37일차(12/3)