Express?? 너무나 당연히 썼던 서버 프레임워크 - (3) 성능편, 개발영역

2020. 11. 5. 22:23Project-Review

앞에서 보안에 대한 생각이 참 많았다. 물론 이 글을 쓰면서도 내가 저걸 다 할 줄 안다!라고 하는 것을 절대! 아닌 것을 알았으면 좋겠다. 그렇지만, 앞으로의 개발에 대해서 다시 한 번 생각하게 되었다는 것은 확실하게 말할 수 있다.

 

그래서 이번에는 공식문서 Advanced탭에 같이 있는 Performance Best Practice라는 녀석을 한 번 살펴보려고 한다. 다시 한 번 말하지만, 이런 것을 본다고 '나는 고수야! 그러니까 이런 걸 서술해!' 보다는 '이런 것도 있었는데, 같이 사용하면 한 단계 발전하는데 도움이 되겠어!'라는 마인드로 봐주길 바란다.

 

처음 들어가자마자 눈에 띄는 것은 Devops에 속하므로 개발운영 두 부분으로 나뉘는 것이다. 데브옵스는 위키피디아에서 아래와 같이 정의하고 있다.

데브옵스는 소프트웨어의 개발과 운영의 합성어로서, 소프트웨어 개발자와 정보기술 전문가 간의 소통, 협업 및 통합을 강조하는 개발 환경이나 문화를 말한다.

처음에 나는 데브옵스가 도커 같은 새로운 기술을 말하는 줄 알았지만, 찾아보니 문화로 가장 1순위의 목표가 '운영팀과 개발팀의 원활한 의사소통으로, 운영팀에서 고객의 수요에 맞춰 서버와 데이터베이스를 통해 서비스가 제대로 돌아갈 수 있는 환경을 개발팀에게 제공해주는 것'이라고 한다. 자세한 것은 AWS에서 설명하고 있으므로 찾아보면 될 것 같다. 이번 장에서는 개발 영역만 알아보도록 하겠다.

 

https://aws.amazon.com/ko/devops/what-is-devops/

 

DevOps란 무엇입니까? – Amazon Web Services(AWS)

소프트웨어와 인터넷은 쇼핑에서 엔터테인먼트 그리고 뱅킹에 이르기까지 전 세계와 산업을 변화시켰습니다. 이제 소프트웨어는 비즈니스를 지원하는 것에 그치지 않고, 비즈니스의 모든 부분

aws.amazon.com

 

 

(1) gzip 압축사용

gzip이 뭔지 찾아보니 다음과 같은 말이 나왔다.

단일 파일 / 스트림 무손실 데이터 압축 유틸리티이며 결과 압축 파일에는 일반적으로 접미사 .gz가 있다.

생각해보니 개발하면서 다운을 받았거나 그럴 때 .tar.gz 이런 파일들이 있었던 것 같다. 앞에 tar가 있어도 되나?하는 생각에 찾아보니 포함된다고 한다. Gzip으로 사용하는 이유는 '본문의 크기를 줄일 수 있기 때문'이라고 한다. Express에서는 'compression'이라는 미들웨어를 추천해주고 있다. 사용방법은 아래와 같다.

var compression = require('compression')
var express = require('express')
var app = express()
app.use(compression())

(2) 동기를 사용하지 말자

이건 어느정도 짐작은 했지만, 이렇게까지 말할 정도인가..라는 생각을 했었다. 왜냐하면 async/await를 사용하면 편한 것들이 너무나 많기 때문이다. 그렇지만 Express에서는 '트래픽이 많아질수록 이러한 호출이 합산되어 앱 성능이 저하된다'라고 말한다. 또한, 프로덕션에서는 항상 비동기로 사용되며, 동기가 정당화 되는 때는 초기 시작 뿐이라고 한다.

만약 자신이 Node.JS에서 동기를 사용하고 있는지 확인하고 싶다면 --trace-sync-io 기능으로 동기 API를 사용할 때마다 경고 및 스택 추적을 인쇄할 수 있다고 한다. 물론 프로덕션에서 사용하는 것이 아니라 코드를 프로덕션에 사용할 준비가되었는지 확인하기 위해서임을 잊지말자

 

(3) 로깅을 제대로 하자

개발하면서 가장 많이 쓴 코드 중 하나가 console.log와 console.error였다. 배포할 때도 이런 코드들을 넣고 있었다. 하지만, 대상이 터미널이나 파일이라면 console도 동기라는 것은 처음 알았다. 그래서 프로덕션시에 적합하지 않다고 하는데, 그래서 디버깅을 사용하나보다. Express는 그래서 디버깅과 앱 로깅 두 가지 부분에 대한 사용 방법을 권하고 있다.

 

  • 디버깅에선 console.log보다는 debug라는 모듈을 사용하자. 뭔가 익숙해서 npm debug를 들어가서 example 명령어를 봤더니 아래와 같았다. 어디서 봤지 생각해봤는데, express generator 사용시, npm start에서 저렇게 쓰고 있는데, 역시...아는 만큼 보인다는게 사실인 것 같다.
set DEBUG=* & node app.js

 

  • 앱 활동의 경우 디버깅보다는 기록, 로깅을 사용하는 경우이기 때문에 winston이나 Bunyan을 추천하고 있다. winston은 Express logging이라고만 쳐도 구글 1페이지가 거의 winston일 정도로 유명해서 알고는 있었는데 Bunyan도 유명한가보다.

    또한, 예외 처리도 중요한 것 중 하나라고 한다. 만약, 제대로 된 예외 처리가 없다면 Express는 멈추고, 앱은 중단된다. (중단 될때의 방법은 아래에 있으니 우선 넘어가자) 그래서 try - catch나 Promise사용을 권장하는데, 이 중에서도 Promise를 추천하고 있다. 이유는 try - catch 구문은 동기이기 때문이다. 그래서 아래와 같은 사용법을 권한다.
app.get('/', function (req, res, next) {
  // do some sync stuff
  queryDb()
    .then(function (data) {
      // handle data
      return makeCsv(data)
    })
    .then(function (csv) {
      // handle csv
    })
    .catch(next)
})

app.use(function (err, req, res, next) {
  // handle error
})

흠..그런데 데이터가 이러면 Promise로 안오는 건 어떡하지?라는 생각을 하자마자 읽어보니 주의사항으로 Promise로 오지 않는 것들은                     Blubird.PromisifyAll() 과 같은 도우미 함수를 사용하여 기본 개체를 변환하라고 말해주었다. (역시 내가 생각한 것은 누군가도 생각한 것이다!). 그럼에도 한 가지 더 문제가 생긴다! emitter는 어떻게 처리하지???

 

(3) -1 Event Emitter

우선 나는 이 글을 작성하면서까지 Event Emitter에 대해 어떻게 말을 해야 할지 몰랐다. 그래서 구글링 한 문서들 중에 velog.io/@wow/이벤트-흐름 라는 사이트에서 한 줄의 문장이 가장 이해하기 쉽게 나온다고 생각했다. 개인적으로 한 번 쭉 읽어보는 것을 추천한다.

(물론 안다면 PASS!)

자바스크립트가 '단일 스레드' 기반의 언어라는 말은 '자바스크립트 엔진이 단일 호출 스택을 사용한다.'는 관점에서만 사실이다. 실제 자바스크립트가 구동되는 환경(브라우저, Node.js 등)에서는 주로 여러 개의 스레드가 사용되며, 이러한 구동 환경이 단일 호출 스택을 사용하는 자바 스크립트 엔진과 상호 연동하기 위해 사용하는 장치가 바로 '이벤트 루프'인 것이다.

Event Emitter의 경우 exception을 발생시키므로 아래와 같은 해결책을 Express에서는 주고 있다. 코드 이해를 위해 위의 사이트를 살짝이라도 보자!

const wrap = fn => (...args) => fn(...args).catch(args[2])

app.get('/', wrap(async (req, res, next) => {
  const company = await getCompanyById(req.query.id)
  const stream = getLogoStreamById(company.id)
  stream.on('error', next).pipe(res)
}))

 

물론 더 풀어서 얘기한다면 얘기할 것이 산더미지만, 대략적으로라도 쭉 살펴보고 부족한 부분을 리팩토링 하거나 다음 프로젝트때 사용해보도록 하자! 다음장에서는 환경 및 설정에 대해 써 보겠다.