티스토리 뷰

728x90

이 APP에 대한 API 서버를 구현하는 것이 이번 과제. 인 줄 알았으나..

 

Numble Node.js 챌린지?

지금 재직 중인 회사에서 IoT SW 개발자로 근무한 지 약 1년 반이 다되어가고 있다. 회사에서 주로 하는 업무는 펌웨어 개발이지만, 업무 특성상 PoC를 위해 백엔드 개발도 겸해서 수행하는 경우가 종종 있다.

 

이전까지는 백엔드를 개발해본 적이 한 번도 없었지만, 막상 접해보니 백엔드 분야가 너무 재미있어서 이것저것 찾으며 공부하던 와중에 Numble에서 Node.js 챌린지를 진행한다는 소식을 접하였다. 챌린지 소개글을 훑어보니 Node.js를 활용해서 비대면 성병 검사 서비스인 Chekit App의 API 서버를 구현하면 되는 듯하였다.

 

마침 좀 더 다양한 실무 프로젝트를 접해보고 싶었으며, 난이도도 그리 높지 않아 보여서 고민하지 않고 바로 신청하였고, 그렇게 2주간의 Node.js 챌린지가 시작되었다.

 

하지만 소개글을 너무 대충 읽었던 탓일까,, 9월 2일에 진행된 OT에서 챌린지의 가이드를 듣고 보니, 이 챌린지는 단순히 API 서버를 구현하는 것이 아니라 Nest.js와 같은 백엔드 프레임워크를 직접 개발하고, 자신의 프레임워크를 활용해서 API 서버를 구현하는 극악무도한 난이도를 가진 챌린지였다...

 

그렇게 2주간의 Node.js 챌린지 지옥편이 시작되었다. 👿

 

모집 링크는 아래에서 확인할 수 있다.

 

 

[마감] Node.js with express 기반으로 비대면 진료 앱 클론 서버 만들기

상세 보기

www.numble.it

 

챌린지 과제

이번 Node.js 챌린지의 과제는 크게 두 가지로 나뉜다.

 

1. 백엔드 프레임워크 개발 과제

2. API 스펙 개발 과제

 

백엔드 프레임워크 개발 과제는 세부적으로 4가지로 나뉘는데 아래와 같다.

 

 

API 스펙 개발 과제는 아래 7가지의 API를 구현하는 과제이다.

 

이때 API 스펙 문서와 DB에 들어갈 샘플 데이터는 Numble 측에서 준비하였고, 챌린지 참가자는 해당 데이터를 활용해서 API 서버를 구현하고, 이를 테스트용 Chekit 앱에서 정상적으로 프로세스가 진행되는지를 확인하면 된다.

 

 

챌린지 결과부터 먼저 말하자면, API 스펙 개발 과제는 모두 완료하였고, 백엔드 프레임워크 개발 과제의 경우, 오류 Handling 모듈 개발 과제를 제외한 나머지를 완수하였다. 못다 한 부분은 이번 챌린지가 끝나더라도 계속 구현해나갈 예정이다.

 

Flint와 CheKit API 서버 구현

Routing 모듈 구현

이번 챌린지를 진행하면서 가장 먼저 구현한 모듈은 Routing 모듈인데, 구현 과정에서 가장 어려웠고, 헷갈렸던 녀석이기도 하다.

 

처음에는 Routing 모듈 개발 과제의 목표가 api 스펙 파일을 cli나 특정 모듈의 인자로 전달하면, 이를 파싱 해서 bolier plate처럼 자동으로 controller와 service 파일을 생성해주는 것으로 착각했었다. 그래서 약 4일간 bolier plate 방식으로 Routing 모듈을 만든 후에 실제로 사용을 해보았는데, 당연하게도 생성된 파일에 프레임워크 사용자가 개입할 수 있는 부분이 적어서 오히려 불편하였다.

 

그래서 개발 과제를 다시 분석한 다음 목표를 새로 세웠고, 그 결과 아래와 같이 동작되도록 구현하였다.

 

Route 생성

1. 간단하게 api 스펙 파일만 전달해서 route를 생성하고 싶은 경우

-> Router라는 클래스 데코레이터에 api 스펙 파일에 대한 정보를 인자로 전달하면, api 스펙 파일을 파싱 해서 자동으로 route를 생성해주고, 이에 대한 middleware 및 controller도 자동으로 연결해준다.

 

2. api 스펙 파일과는 무관하게, 직접 route 지정도 하고 싶은 경우

-> Router 데코레이터로 감싸져 있는 클래스 아래에 Flint에서 제공하는 HTTP 메소드 데코레이터를 활용해서 사용자가 직접 route를 생성한다.

 

// https://github.com/GwangrokBaek/CheKIT-backend/blob/master/src/routes/api.router.ts

import { Get, Router } from "../../packages/flint/common"

@Router("", {
	files: ["api.json"],
	directory: __dirname,
	log: true,
	jwt: "auth.middleware.ts",
})
export class ApiRouter {
	constructor(private readonly apiController: any) {
		this.apiController = apiController
	}

	@Get("/test1")
	testApi1(): any {
		return this.apiController.testApi()
	}
}

 

위 코드에서 확인할 수 있듯이 Router 클래스 데코레이터의 두 번째 인자로 api 스펙 파일에 대한 정보를 전달해주면, 사용자가 route를 직접 설정해주지 않더라도 Flint에서 자동으로 생성해준다. Router 클래스 데코레이터에 대한 내용은 여기에서 확인할 수 있다.

 

또한, 스펙 파일과는 무관하게 직접 route를 설정하고 싶은 경우에는 위에 보이는 testApi1 메소드와 같이 이를 직접 정의해준 다음, 알맞은 HTTP 메소드 데코레이터로 해당 메소드를 감싸주면 된다. HTTP 메소드 데코레이터에 대한 내용은 여기에서 확인할 수 있다.

 

Middleware 연결

Auth check를 수행하는 middleware를 연결하거나, 입력 파라미터에 대해 validator를 구성하고 싶은 경우가 있을 수 있다.

 

이때 validator의 경우, 스펙 파일에 명시되어 있는 headers나 body 등의 type과 required 값을 파싱 해서 Flint가 validator를 자동으로 생성해준다.

// api 스펙 파일

"signup": {
		"url": "/v3/user/reg",
		"method": "post",
		"description": "email을 고유 아이디로 새로운 사용자 계정을 생성합니다.",
		"body": {
			"email": {
				"type": "email",
				"required": true,
				"description": "이메일 주소"
			},
			"key": {
				"type": "string",
				"required": true,
				"description": "패스워드"
			},
			"name": {
				"type": "string",
				"description": "사용자의 이름"
			}
		},
		// 생략
	},

 

스펙 파일에 대한 파싱을 수행하고, 이를 통해 validator를 생성하는 부분은 여기에서 확인할 수 있다.

 

또한 middleware의 경우, 아래와 같이 사용자가 직접 구현한 middleware를 연결할 수 있는데, 시간이 부족하여 api 스펙 파일을 사용하여 route를 생성하는 경우에 대해서만 개발하였다.

 

// https://github.com/GwangrokBaek/CheKIT-backend/blob/master/src/middlewares/auth.middleware.ts
import * as jwt from "jsonwebtoken"

const authChecker = async (req, res, next) => {
	try {
		const headers = req.headers
		const token = headers.authorization.split(" ")[1]
		const decoded = await jwt.verify(token, process.env.SECRET)

		if (decoded) {
			req.userId = decoded.id
			next()
		} else {
			res.status(401).json({ error: "Unauthorized" })
		}
	} catch (error) {
		res.status(401).json({ error: "Unauthorized" })
	}
}

export default authChecker

 

위 middleware 파일을 아래와 같이 Router 클래스 데코레이터의 두 번째 인자에 함께 전달해주면 된다.

 

@Router("", {
	files: ["api.json"],
	directory: __dirname,
	log: true,
	jwt: "auth.middleware.ts",
})

 

Logger 모듈 구현

Routing 모듈 구현을 완료한 다음에는 Logger 모듈 구현을 진행하였는데, 아래 목표에 초점을 두어 구현하였다.

 

1. 로그가 Flint 시스템에서 발생했는지, 사용자 App에서 발생했는지 식별 가능

2. 어느 모듈에서 발생한 로그인지 구분 가능

3. Timestamp 출력

4. Loglevel을 구분하여 사용자가 지정한 level에 대해서만 로그가 출력되게끔 구현

 

export class ApiController {
	constructor(
		private readonly logger: any,
	) {
		this.logger = logger
	}

	async test(req, res) {
		this.logger.info("hello world")
		return {}
	}
}

 

위의 코드에서 볼 수 있듯이, 사용자는 logger에 대한 dependency를 주입하여 logger를 각각의 모듈에서 사용할 수 있으며, 결과적으로 command 창에서는 아래와 같은 로그를 확인할 수 있다.

 

 

Logger 모듈을 구현할 때 어려웠던 점은 2. 어느 모듈에서 발생한 로그인지 구분 가능하게끔 하는 부분이었다. 이를 처리하기 위해서는 logger의 메소드가 호출될 때 콜 스택을 확인하고, 상위 caller에 대해 파싱을 해주어야 하는데.. javascript에서 콜 스택을 정상적으로 얻을 수 있는 방법에 대해 아무리 찾아봐도 찾을 수가 없었다 😨

 

그래서 편법을 사용해서 아래와 같이 처리하였다.

 

private getModuleName(method: Function): string {
		let obj = {} as any
		Error.captureStackTrace(obj, method)

		let moduleName: string = ""
		let stackArray: string[] = obj.stack.split("\n")

		const filteredArray = stackArray.filter(function (val) {
			return !/Error|new|at \/|at Object|at __awaiter|at Generator|at next|at Function.|at Layer.|at Route./g.test(
				val
			)
		})

		let moduleString = filteredArray[0].split("at ")
		moduleString = moduleString[1].split(" ")
		let module = moduleString[0].split(".")

		moduleName = module[0]

		return moduleName
	}

 

Logger 모듈에서는 logger의 메소드가 호출될 경우, getModuleName 메소드를 통해 모듈 이름을 얻어온다.

 

이때 getModuleName 메소드에서 Error를 강제로 생성해서 해당 Error가 발생한 지점으로부터의 콜 스택을 획득하고, 정규표현식을 사용해 필요 없는 스택들을 제거한 다음, 남아있는 스택 중 가장 최상위에 위치해 있는 스택을 모듈 이름으로 가져오게끔 하였다.

 

Logger 모듈에 대한 전체 코드는 여기에서 확인할 수 있다.

 

Flint-Initializer 구현

Flint에서 필요로 하는 필수적인 모듈들에 대한 구현을 대부분 완료하였다. 이제 서버를 생성하고, 사용자의 모듈들이 이 서버 위에서 돌아가게끔 하는 Flint-Initializer를 구현할 차례가 다가왔다.

 

Flint-Initializer의 목표는 아래와 같이 설정하였다.

 

1. 사용자는 서버 생성에 관여할 필요가 없다.

2. 사용자 모듈들의 인스턴스를 Flint-Initializer가 관리하고, 각각에서 필요로하는 의존성을 동적으로 주입시켜준다.

3. 각각의 모듈에 대한 metadata를 Flint-Initializer가 취합하고 처리한다.

 

// https://github.com/GwangrokBaek/CheKIT-backend/blob/master/src/modules/app.module.ts

import { Module } from "../../packages/flint/common"
import { ApiRouter } from "../routes/api.router"
import { ApiController } from "../controllers/api.controller"
import { Logger } from "../../packages/flint/common"
import { AppDB } from "./app.db"
import { User } from "../models/user.model"
import { Doctor } from "../models/doctor.model"

@Module()
export class AppModule {
	public imports: any[]
	public routers: any[]
	public providers: any[]

	constructor() {
		const logger = new Logger("error")
		const appDb = new AppDB(logger)
		const apiController = new ApiController(logger, User, Doctor)
		const apiRouter = new ApiRouter(apiController)

		this.imports = []
		this.routers = [apiRouter]
		this.providers = [apiController, appDb]
	}
}

 

결과적으로 2번에 대한 구현은 완료하지 못했다 😭
현재는 위 코드에서 볼 수 있듯이 사용자가 module을 생성할 때에 직접 인스턴스를 생성하고, 필요한 의존성을 주입해주어야 한다.

 

이렇게 생성된 모듈들은 아래와 같이 Flint의 엔트리 코드인 main.ts에서 Flint-Initializer에 의해 최종적으로 생성되며, Flint-Initializer는 앞서 Routing 모듈에서 설정하였던 Route 관련 metadata를 취합하여 서버의 Router에 등록시켜준다.

// https://github.com/GwangrokBaek/CheKIT-backend/blob/master/src/main.ts

import { AppModule } from "./modules/app.module"
import { Flint } from "../packages/flint/core"

async function main() {
	const appModule = new AppModule()
	const app = await Flint.create([appModule])
	await app.listen(3000)
}

main()

 

 

Flint-Initializer와 관련된 부분은 여기에서 확인할 수 있다.

 

 

디자인 패턴 설계과 API 구현

이제 Flint와 관련된 구현은 모두 완료하였다. 물론 아직 미흡한 부분도 많고, 빠진 부분도 조금씩 있지만 이러한 부분들은 이번 챌린지가 끝나고 시간을 들여 찬찬히 완성해나갈 예정이다.

 

이제 본격적으로 Chekit APP에 대한 API 서버를 구현할 차례이다.

우선 API 서버의 디자인 패턴으로는 MVC를 선택하였다. 그 이유는 아래와 같다.

 

1. 여러 디자인 패턴 중 가장 간단한 패턴

2. 이번 Chekit API 서버의 경우, View를 따로 구성해 줄 필요가 없기에 MVC 패턴의 단점이 부각되지 않는 점

 

MVC 패턴을 적용하여 설계한 디렉토리 구조는 아래와 같다.

 

 

그리고 여태껏 개발한 Flint 프레임워크의 경우, 우선은 별도 레포지토리가 아닌 아래와 같이 모노 레포로 구성을 하였다.

결과적으로 Flint 프레임워크는 packages 디렉토리 아래에 위치하며, 사용자의 API 서버에 대한 코드는 src 디렉토리 아래에 위치한다.

 

 

DB로는 mongodb를 사용하였으며, javascript에서 이를 편하게 사용하기 위해 mongoose 패키지를 활용하였다. 이때 mongoose Schema 및 CRUD 관련 코드들은 models 디렉토리 아래에 위치시켰다.

 

또한, router에 의해 호출되는 controller들은 controllers 디렉토리 아래에 위치해 있는데, 아래 코드에서 볼 수 있듯이 여기에서 요청에 대한 로직을 수행하게 된다.

 

// https://github.com/GwangrokBaek/CheKIT-backend/blob/master/src/controllers/api.controller.ts

import * as jwt from "jsonwebtoken"

export class ApiController {
	constructor(
		private readonly logger: any,
		private readonly userModel: any,
		private readonly doctorModel: any
	) {
		this.logger = logger
		this.userModel = userModel
		this.doctorModel = doctorModel
	}

	async test(req, res) {
		this.logger.info(``)
		return {}
	}

	async signup(req, res) {
		let result

		try {
			const body = req.body

			await this.userModel.create(body.email, body.key, body.name)

			const token = await jwt.sign(
				{ id: body.email },
				process.env.SECRET,
				{
					expiresIn: "1h",
				}
			)

			result = { status: "ok", data: { token: token } }
		} catch (error) {
			this.logger.error(error)

			if (error.code === 11000) {
				result = { status: "user_duplicate" }
			} else {
				result = { status: "nok" }
			}
		}

		return result
	}

	async withdrawal(req, res) {
		let result

		try {
			await this.userModel.deleteByEmail(req.userId)

			result = { status: "ok" }
		} catch (error) {
			this.logger.error(error)
			result = { status: "nok" }
		}

		return result
	}
    
    // 생략
}

 

이렇게 각각의 Flint 및 API 서버에 대한 구현을 마무리하였다. 이번 프로젝트의 Github 레포 링크는 여기에서 확인할 수 있다.

 

최종적으로 CheKit 앱을 통해 검사 신청 프로세스 중 가장 마지막 API인 register API에 대한 결과 스크린을 아래와 같이 확인할 수 있다면, API 개발 과제를 성공적으로 수행한 것이다.

 

이번 챌린지에 있어서 최고로 기뻤던 순간 ㅠ

 

글을  마치며

백엔드 개발도 생소한데, 하필 백엔드 프레임워크 개발은 처음이었다 보니 많은 난관이 있었다. 하지만, 챌린지를 참여하는 다른 분들이 qna 채널에 올린 질문과 멘토님의 답변 덕분에 인사이트를 얻을 수 있었고, 또 스터디를 정기적으로 진행하면서 서로의 정보를 공유하는 과정에서 많은 도움을 얻을 수 있었다.

 

처음에는 예상치 못한 과제 난이도로 인해 많이 당황했었고, 업무와 병행하면서 2주 만에 이걸 대체 어떻게 완성하라는 거라며 불평도 했었지만, 지금은 오히려 Numble의 이번 챌린지 덕분에 많은 것을 배울 수 있었어서 매우 감사한 마음이다. 지금까지는 AWS에서 MSA 단위로 개발하면서 백엔드를 살짝 경험했던 터라, 이러한 API 서버에 대한 개념이 많이 부족했었는데 이번 챌린지를 수행하면서 디자인 패턴, IoC, DI 등등 그동안 간과했던 부분에 대해 심도 있게 공부할 수 있었다.

 

다음 챌린지로는 대용량 트래픽에 대응하는 챌린지가 있을 예정이라 들었는데, 그때도 고민하지 않고 바로 신청해야겠다!

728x90