유동
[node.js] 어플리케이션에서 데이터베이스 엔티티 작성하기 본문
구체적인 코드는 github에서 확인 가능합니다
백엔드 어플리케이션 개발 시에 우리는 prisma나 typeorm같은 orm을 사용할겁니다.
typeorm같은 경우에는 아래처럼 어플리케이션단에서 class에 @Entity 데코레이터를 붙여 해당 클래스가 데이터베이스의 테이블과 1 : 1 매칭이 된다고 명시해줍니다.
prisma의 경우에는 이처럼 독자적인 스키마 파일을 구성해서 엔티티를 표현합니다
해당 글에서는 prisma를 예를 들어 설명하겠습니다.
먼저 간단한 테이블 예제를 보여드리겠습니다.
- 유저 테이블이 있고, 103개의 학과 이름이 저장되어있는 major테이블이 있습니다. 또 유저는 학과를 여러 개 가질수 있기 때문에 1 : N 관계가 성립되겠습니다.
- 또, 학과는 여러 유저가 가지고 있을 수 있으니 유저와 학과는 N:M관계이기 때문에 account_major이라는 매핑 테이블을 만들어 관리 해주겠습니다.
만약 npx prisma generate 명령어를 실행하였다면, prisma는 각 테이블에 대한 타입을 자동으로 만들어줍니다.
여기서 우리는 prisma가 제공해주는 엔티티를 사용해서 쿼리문을 날릴 수도 있습니다.
import { account, account_major, major } from '@prisma/client';
// 이처럼 prisma가 객체를 자동으로 만들어줍니다
account.idx;
account_major.account_idx;
major.idx;
이렇게 prisma가 제공하는 타입을 사용해도 문제가 없습니다. 오히려 prisma가 직접 타입을 만들어주기까지 하니 땡큐입니다.
- 하지만 소프트웨어설계 관점에서는 좋지 않은 패턴입니다. 데이터베이스와 직접 연관이 되어있는(동기화 되어있는) 엔티티들을 외부로 노출시키는 셈이 되기 때문입니다.
어플리케이션(즉 코드관점)단에서 바라본 데이터베이스는 외부 의존성입니다. - 소프트웨어 설계 관점에서 외부 의존성과의 결합을 최소화하는것은 매우 중요합니다. 만약 데이터베이스 스키마가 변경된 경우에, 해당 변경사항이 어플리케이션 전반에 영향을 미치지 않게 하는것이 가장 이상적인 형태입니다.
- 비단 데이터베이스 스키마의 변경뿐만 아니라 orm이 변경되는 경우에, 데이터베이스가 변경되는 경우에,(그럴일은 웬만하면 없겠지만)도 코드레벨에서는 영향을 받지 않는것이 정말 중요합니다.
prisma에서 typeorm으로 변경되었을 경우에, 극단적으로 RDBMS에서 파일 시스템으로 변경이 되더라도 영향을 받지 않아야 합니다. - (application -> database) => (application <- database)
어플리케이션의 엔티티가 데이터베이스를 바라보는것이 아니라, 데이터베이스가 어플리케이션의 엔티티를 바라보게 해야 의존성을 최소화할수있습니다.
이를 위해 우리는 데이터베이스 엔티티와 어플리케이션 내부에서 사용하는 모델을 분리할수 있습니다.
엔티티를 정의해봅시다
- 여기서 한가지 생각해야할 점이 있습니다. 엔티티를 클래스로 만들어야하나? 인터페이스로 만들어도 되지 않나?
저는 엔티티를 다음과 같이 인터페이스로 만드는것을 선호합니다. 이유는 타입스크립트의 다양한 util타입(Pick, Omit, Partial 등)들을 사용해 쉽게 타입을 정의하고, 다른 인터페이스와 상호작용하여 유연하게 타입을 정의할수 있기 때문입니다.
- 만약 이처럼 엔티티를 구상한다면, typescript의 namespace기능을이용해서 엔티티 이름으로 묶어줍니다.
이렇게 묶어준다면 따로 dto를 클래스로 만들어주지 않고, 인터페이스 접근만으로 유연하게 타입을 만들 수 있습니다.
하지만, 만약 class-validator나 swagger같이 프로퍼티에 데코레이터를 이용해야하는 상황이 온다면 어쩔 수 없이 class를 사용하긴 하겠지만.
express환경에서는 express-validator라는 좋은 선택지가 있기 때문에, 굳이 클래스로 만들어서 복잡도를 높이는행위는 지양하는 편입니다.
express-validator를 이요한 스키마 validation은 다른 글에서 설명하도록 하겠습니다. 위에서 인터페이스들을 정의했으니 한번 살펴봅시다
// account.router.ts
/**
* @POST /account/signup
* 회원가입
**/
accountRouter.post('/signup', /** 이부분에서 json schema validator*/, async (req, res, next) => {
// 만들었던 IAccount namepsace의 signup 인터페이스에 접근합니다
const signupInput: IAccount.ICreateAccount = req.body;
// 그대로 서비스 레이어로 넘겨주겠습니다
const accountIdx = await accountService.signup(signupInput);
return res.send({accountIdx})
})
// account.service.ts
export class AccountService {
// prisma 의존성 주입
constructor(private readonly prisma: PrismaService) {}
/**
* 회원을 생성합니다
* @return 생성된 사용자의 인덱스 번호
**/
async signup(signupInput: IAccount.ICreateAccount): Promise<IAccount['idx']> {
// 생성 트랜잭션 들어가기 전에 학과배열 검사를 했다고 가정하겠습니다 (존재하는 학과인덱스 인지)
// 유저생성 트랜잭션
const createAccountTx = await this.prisma.$transaction(async (tx) => {
// 유저 생성
const createAccountResult = await tx.account.create({
data: ...signupInput,
select: {
idx: true
}
});
// 유저 - 학과 매핑테이블 삽입 (손코딩이라 실제 작동되는지는 보장못합니다 ㅠㅠ)
await tx.account_major.createMany({
data: {
accountIdx: createAccountResult.idx,
majorIdx: signupInput.major.map(major => ({
majorIdx: major.idx
}))
}
});
return createAccountResult;
});
// 생성된 사용자의 인덱스 번호 반환
return createAccountTx.idx;
}
}
- 위와 같이 인터페이스 엔티티를 만들고 해당 인터페이스를 이용해 서비스 로직을 구현할 수 있습니다. 저는 express로 프로젝트를 하면 위와 같이 interface로 엔티티를 만들어 사용하곤 합니다.
validate단에서(위에서는 express-validator) schema validate만 성공적으로 이루어진다면, 위처럼 미리 정의해둔 인터페이스를 이용해 타입스크립트의 최대 장점중 하나인 구조적 타이핑을 이용해 어플리케이션단에서 타입을 검증하며 컴파일 타임에 최대한 버그를 찾아내며 에러를 방지할 수 있습니다.
또, prisma를 이용한다면 더욱 빛을 발할겁니다. Prisma가 만들어주는 타입과 내가 만들어준 인터페이스가 일치하거나 종속된다면 컴파일러가 통과시켜줍니다ㅎㅎ
'node.js' 카테고리의 다른 글
소셜로그인(Google)과 OAuth flow (0) | 2025.03.26 |
---|---|
[Node] 기존 JavaScript와 Express 에서 Typescript와 Prisma로의 전환 (0) | 2024.05.31 |
[NodeJS] 서버에서 영상을 배달하는법 HTTP Live Streaming(HLS) (0) | 2024.05.25 |
API란, REST api란 (스터디 자료) (0) | 2024.03.31 |