1. Fastify
fastify 적용은 딱히 어려움은 없었다. 일찍 적용해서 개발 단계에서 충분히 테스트하는 것이 낫겠다. 나중에 fastify로 바꿔서 문제가 생기면 찾기도 어렵고 그것 자체가 장애라서 곤란하겠지..
https://docs.nestjs.com/techniques/performance 를 참고하면 크게 무리가 없다.
https://github.com/NGuard-Security/security_api/pull/7/commits/1f52ab480a02a26b69c1ee0cca3335d662b48e62 여기 수정을 참고하면 큰 문제 없다.
특이한 점은 express를 사용할 때는 request.ip 가 undefined로 나온다는 것이다. (node.js가 아니라 bun으로 실행해서 그럴수도.. 확인은 못 함) 몇 시간 구글링해도 remote ip를 얻는 방법을 찾지 못했는데 fastify를 바꿨더니 문제가 없어졌다.
2. 서버를 실행하고 종료시키는 스크립트를 만들었다.
kill로 강제 종료하는 게 맞는지 모르겠다.
dev_server.sh
echo "bun --watch ./src/main.ts"
bun --watch ./src/main.ts &
echo "server pid is $!"
echo $! > nest_server.pid
kill_server.sh
pid=$(<nest_server.pid)
echo "stop nest_server (pid is $pid)"
kill -9 $pid
rm nest_server.pid
3. https 적용 테스트
테스트 인증서 설치는
https://growth-msleeffice.tistory.com/129
https://velog.io/@haru/how-to-local-testing-by-https 를 참고했다. mkcert 설치는 brew install mkcert 로 했다.
fastify의 경우는 적용이 살짝 다른데.. https://docs.nestjs.com/faq/multiple-servers 를 참고했다.
locoalhost와 특정 도메인의 테스트 인정서는 적용하는데 문제 없었다. 다만 localhost에서의 접속은 문제가 없었지만 안드로이드 앱으로 접속하는 것은 verification 실패 예외가 발생하여 실패하였다. 정상적인 인증서를 사용하면 이것도 문제가 없을 듯 하다. 결론은 개발은 인증서 없이 http로, 운영서버는 실제 인증서(LetsEncrypt)를 가지고 https로 구동시킬 계획이다.
import fs from "fs";
const httpsOptions = {
key: fs.readFileSync('./dev.mdiwebma.com+2-key.pem'),
cert: fs.readFileSync('./dev.mdiwebma.com+2.pem'),
};
async function bootstrap() {
...
new FastifyAdapter({https: httpsOptions}),
...
4. gzip 적용하기
https://docs.nestjs.com/techniques/compression 를 참고해서 구현.. 1줄만 추가하면 gzip이 지원된다니 놀라울지경이다... nginx로 gzip을 지원하려고 쌩쇼를 했었는데 그때도 서버 응답에서는 gzip으로 응답하기가 가능했는데, 앱에서 gzip 압축해서 요청할 경우에는 처리하지 못했었다. 그런데 nest.js에서 await app.register(compression); 이 1줄 추가로 요청과 응답에서 gzip이 잘 동작했다.
Android app에서 테스트 코드..
// gzip으로 압축하여 요청
// TODO body의 크기가 일정 length보다 크면 gzip으로 압축하도록 수정
val data: ByteArray = jsonObject.toString().toByteArray()
val arr = ByteArrayOutputStream()
val zipper: OutputStream = GZIPOutputStream(arr)
zipper.write(data)
zipper.close()
val body = RequestBody.create(JSON_CONTENT_TYPE, arr.toByteArray())
//val body = RequestBody.create(JSON_CONTENT_TYPE, jsonObject.toString())
val request = Request.Builder()
.url(getUrl(path))
.header(JSON_WEB_TOKEN, jwt)
.header("Content-Type", "application/json; charset=utf-8")
.header("Content-Encoding", "gzip")
.header("Accept-Encoding", "gzip")
.post(body)
.build()
val response = httpClient.newCall(request).execute()
// gzip 응답처리..
var bodyString: String?
// gzip 디코딩
if (response.header("Content-Encoding") == "gzip" ||
response.header("content-encoding") == "gzip"
) {
val responseBody = response.body() ?: throw IOException("body is null")
BufferedInputStream(GZIPInputStream(responseBody.byteStream())).use { input ->
ByteArrayOutputStream().use { baos ->
val ba = ByteArray(1024)
while (true) {
val len = input.read(ba)
if (len == -1) break
baos.write(ba, 0, len)
}
bodyString = String(baos.toByteArray())
}
}
} else {
bodyString = response.body()?.string()
}