URL 단축기 만들기 - 4. 백엔드 데이터베이스 세팅, 유저 서비스 만들기
지난 글에는 데이터베이스 모델링을 해봤는데요. 이번에는 만든 모델을 실제로 사용해 볼거에요!
그리고 사용해보기 위해서 유저 서비스도 작성해 줄거에요. 한번에 2가지를 다루다보니 글이 좀 길어질 것 같네요!
EdgeDB 관련 패키지 설치하기
먼저 edgedb 패키지를 설치해 줍시다.
yarn add edgedb
yarn add --dev @edgedb/generator
edgedb 패키지를 설치했어요. 이제 데이터베이스를 사용하기 전 데이터베이스 설정이 들어갈 설정 파일을 만들어 줄거에요.
설정 파일 만들기
프로젝트 루트 폴더에 config.example.json
을 만들어 주세요.
{
"database": {
"host": "localhost"
}
}
일단은 이정도만 채워뒀어요. 뭐 언젠가 다 채우겠지...
그리고 이걸 복사해서 config.json으로 이름을 바꿔주세요! 단, 이 config.json은 .gitignore에
추가해 주셔야 해요!
그리고! 우리는 개발 환경 세팅을 위해 edgedb project init
명령어를 실행했기 때문에 개발 환경에서는 db 설정이 필요 없어요. 지금의 config.json
에는 database 설정이 필요 없단 말이죠. 그러니 제거해 줄게요. 저게 있으면 생성된 인스턴스로 연결이 안되더라구요.
{}
설정 파일 읽기
먼저 설정에 들어갈 필드를 정의해 봅시다.
database
옵션에는 edgedb 연결 설정이 들어갈 예정이니 edgedb의 ConnectOptions를 그대로 넣어주도록 할게요.
import type * as EdgeDB from 'edgedb'
export type Config = {
database: EdgeDB.ConnectOptions
}
그러고 아래에서 설정 파일을 export 해줬어요.
설정 파일을 읽기 위해서 require
을 사용했어요.
export const config = require('../../../../config.json') as Config
이제 edgedb 클라이언트를 inject하기 위해..?서 데이터베이스 모듈을 만들어 볼거에요!
모듈 생성하기
nest g mo database
그리고 utils 폴더에 DI.ts
를 만들고 DI 심볼들을 넣어줄거에요.
export class DI {
static EdgeDB = Symbol('EdgeDB Client')
}
그 다음 database/client.ts
를 만들어 줬어요.
import createClient, { Client } from 'edgedb'
import { config } from 'src/utils/config'
export const edgedbClient = createClient(config.database)
export type EdgeDBClient = Client
이 구조는 다음에 한번 수정해 볼게요. (nestjs에 관련 기능이 있었던 것 같아요)
이제 database.module.ts
에서 이 클라이언트를 등록해 줍시다.
import { Module } from '@nestjs/common'
import { DI } from 'src/utils/DI'
import { edgedbClient } from './client'
@Module({
providers: [
{
provide: DI.EdgeDB,
useValue: edgedbClient,
},
],
exports: [DI.EdgeDB],
})
export class DatabaseModule {}
exports
까지 설정해서 다른 모듈에서 edgedb 클라이언트를 사용할 수 있게 해줬어요.
이렇게 데이터베이스 모듈이 만들어졌네요. 이제 이 글의 최종 목표인 유저 서비스 만들기! ...를 해볼거에요.
일단 nest cli로 모듈을 만들어 줄게요. 서비스도요.
nest g mo users
nest g s users
이러면 users
폴더에 모듈 파일들이 생겼을 거에요.
뭐 이건 나중에 보고! 서비스에 사용될 쿼리를 써보기로 할게요!
쿼리 작성해보기
쿼리 작성 전에! 아까 설치한 @edgedb/generate
패키지를 사용해서 쿼리를 ts 파일로 변환해주는 과정을 추가해 줄거에요.
package.json
에서 스크립트를 수정해 줄게요.
일단 generate
스크립트를 추가해 주세요. 그리고 빌드 전에 이 generate가 실행되도록 설정해 주세요!
{
// ...
"scripts": {
"build": "yarn generate && nest build",
"start": "yarn generate && nest start",
"dev": "yarn generate && nest start --watch --preserveWatchOutput",
// ...
"generate": "generate queries --target ts"
},
// ...
}
이제 코드 생성의 준비가 끝났어요! 이제 드디어 쿼리를 써보기로 할게요!
일단 먼저 유저를 찾는 쿼리를 써볼게요. 이름은 findUserById
로 했어요.
select User { userId, username, admin } filter .userId = <str>$userId;
$userId
는 js 코드에서 파라미터로 넘겨줄 부분이에요. 이런식으로 edgeql 쿼리문을 짠 뒤에 yarn generate
를 입력해 주면..!
import type {Client} from "edgedb";
export async function findUserById(client: Client, args: {
"userId": string;
}): Promise<{
"userId": string;
"username": string;
"admin": boolean;
} | null> {
return client.querySingle(`select User { userId, username, admin } filter .userId = <str>$userId;`, args);
}
이런 코드가 만들어 졌어요! 파일 이름은 그냥 뒤에 .ts가 붙은게 다네요.
일단 이 파트에서 사용할 쿼리는 다 써주는걸로 할게요.
select (
insert User {
userId := <str>$userId,
username := <str>$username
}
) {
userId,
username,
admin
}
delete User filter .userId = <str>$userId;
이렇게 쿼리를 다 작성했으면 다시 yarn generate
를 입력해주세요!
아 그리고 gitignore에 *.edgeql.ts
를 추가해 주세요. 상관없을수도 있지만 저는 자동생성되는 코드가 git에 올라가는걸 별로 좋아하진 않아서요.
이제 드디어 서비스 코드를 짜보도록 할게요!
서비스 코드 작성
일단 db에서 가져온 유저를 그대로 사용하기엔 뭔가 마음에 안드니까 따로 인터페이스를 하나 만들어 줄게요.
export interface User {
id: string
username: string
admin: boolean
}
그다음 이렇게 서비스 코드를 만들어줬어요!
import { Inject, Injectable, NotFoundException } from '@nestjs/common'
import { EdgeDBClient } from 'src/database/client'
import { DI } from 'src/utils/DI'
import { createUser } from './queries/createUser.edgeql'
import { deleteUser } from './queries/deleteUser.edgeql'
import { findUserById } from './queries/findUserById.edgeql'
import { User } from './types'
@Injectable()
export class UsersService {
constructor(@Inject(DI.EdgeDB) private db: EdgeDBClient) {}
/**
* Finds a user by id
* @param id id of the user to find
* @returns {User}
*/
async findUserById(id: string): Promise<User | null> {
const u = await findUserById(this.db, { userId: id })
if (!u) throw new NotFoundException('User not found')
return { id: u.userId, admin: u.admin, username: u.username }
}
/**
* Creates a user
* @param id the id to use for the user
* @param username the username to set for the user
* @returns {User}
*/
async createUser(id: string, username: string): Promise<User> {
const u = await createUser(this.db, {
userId: id,
username,
})
return { id: u.userId, admin: u.admin, username: u.username }
}
/**
* Deletes a user
* @param id the user id to delete
* @returns {string}
*/
async deleteUser(id: string): Promise<void> {
const deleted = await deleteUser(this.db, { userId: id })
if (!deleted) throw new NotFoundException('User not found')
}
}
아, 이 코드가 작동하려면 DatabaseModule
이 사용 가능해야 하겠죠? import 해줄게요.
import { Module } from '@nestjs/common'
import { DatabaseModule } from 'src/database/database.module'
import { UsersService } from './users.service'
@Module({
imports: [DatabaseModule],
providers: [UsersService],
})
export class UsersModule {}
이러면 이론상 작동은 할거에요. dev 명령어 실행해봐도 오류없이 켜지고요.
그치만! 우리는 테스트코드를 쓸거에요. 왜냐고요? 그냥 해보고 싶어서요(퍼어ㅓ걱)
import { Test, TestingModule } from '@nestjs/testing'
import { edgedbClient } from 'src/database/client'
import { DatabaseModule } from 'src/database/database.module'
import { UsersService } from './users.service'
describe('UsersService', () => {
let service: UsersService
let userId: string
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
imports: [DatabaseModule],
}).compile()
service = module.get<UsersService>(UsersService)
})
afterAll(async () => {
await edgedbClient.execute(`delete User filter .userId = "-1";`)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
it('find an undefined user', async () => {
await expect(service.findUserById('-1')).rejects.toThrowError(
'User not found',
)
})
it('create a user', async () => {
const action = service.createUser('-1', 'Test User')
await expect(action).resolves.toEqual({
id: '-1',
username: 'Test User',
admin: false,
})
const { id } = await action
userId = id
})
it('userId should be defined', () => {
expect(userId).toBeDefined()
})
it('find an undefined user', async () => {
await expect(service.findUserById(userId)).resolves.toEqual({
id: '-1',
username: 'Test User',
admin: false,
})
})
it('delete a user', async () => {
await expect(service.deleteUser(userId)).resolves.toBeUndefined()
})
it('delete a non-existing user', async () => {
await expect(service.deleteUser(userId)).rejects.toThrow('User not found')
})
})
이렇게 테스트코드를 써줬어요.
사실상 제가 코딩하면서 처음 써보는 테스트코드네요.
중요한 부분이 있다면 저기 imports
에 DatabaseModule
을 추가해 줘야해요.
users.module.ts와는 별개로 사용되기 때문에 저기서 또 import 해줘야 하더라고요.
그리고 afterAll에서는 다음에 테스트할때 오류가 발생하지 않도록 만약 아이템이 존재한다면 지우도록 해줬어요.
그 다음 yarn test:cov
를 돌려줬어요.

users.service.ts 커버리지 100%!
이렇게 유저 서비스를 완성했어요. 다음엔 링크 생성 서비스를 만들어보고 컨트롤러도 만들어볼게요. (인증은 최대한 미루는걸로...? 귀찮아요...!)
그럼 파링은 오늘도 소스코드 던져놓고 가는걸로 할게요! 읽어주셔서 감사합니당..