Backend study

Nest.js 서버에 fastify, https, gzip 적용하기

Daejeong Kim 2024. 4. 27. 23:17

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()
            }