[정원사 프로젝트] 3. 백엔드 개발하기 (2)

정원사 프로젝트 회고, 백엔드 개발하기(2) : crawling + scheduler

Posted by yoogomja on June 20, 2020

프로젝트 회고 글타래

1. 크롤링 하기

지난 단계에서는 어떤 개발을 할지 대략적으로 기획하고, 개발 환경을 결정하고, 데이터베이스에 대략적인 모델을 만들어준 후 간단하게 github 정보를 가져와 삽입해보는 과정을 거쳤었다. 생각보다 mongoose문법은 어렵지 않았고, github API를 연동하는 것도 큰 문제없이 진행됐었다.

핵심적인 부분의 테스트를 마치고 났으니 이제 본격적으로 개발을 진행했다. 백엔드 개발은 크게 아래와 같은 순서로 작업할 예정이었다.

  1. github 크롤링 함수 묶음 개발
  2. 크롤링한 정보를 분석하는 함수 묶음 개발
  3. 해당 함수들을 정리해서 라우트 함수에 하나씩 할당

사족이지만, 이전부터 계속하고 있는 일인 ‘개발 순서 정하기’는 (모두가 그렇겠지만) 개발전에 항상 선행하는 작업이다. 내가 너무 기억력이 부족하다보니 그냥 무작정 개발을 들어가는 경우 뭘 개발하려고 했는지 잊거나 원래 목적과 다른 방향으로 진행하고있는 일이 생기고는 했었다. 그런 경우를 줄이려고 하다보니 뭘하려고 하는지 부터 결정하려고 하는 습관이 생겼다.

이 때 라우팅을 먼저하는 것이 아니라 예정된 API 목록에 필요한 기능을 먼저 작성한 후 라우팅을 할 예정이었기 때문에, 테스트에 필요한 라우터를 하나 빠르게 만들고 거기에서 바꿔가면서 작업을 하게됐었다.

최대한 재사용성을 높이기 위해 크롤링 부분만 함수를 별도로 제작했었다. 저장소 정보, 사용자 정보, 사용자 이벤트 등을 불러오는 함수의 묶음들을 만들어서 라이브러리처럼 사용하도록 했었다. 이 과정에서 callback방식이었던 것을 promise방식으로 변경해서 async / await를 사용할 수 있도록 설정해뒀었다. 작업을 마쳤을 때 저 부분이 생각보다 시간을 많이 줄여주지 않았나 싶다.

크롤링 제작 과정에서는 전에도 언급하였듯이, 제한된 요청 건수안에 효율적인 처리가 필요했기 때문에 최대한 중복 요청은 제외하고 필요한 요청만 하도록 작성해 두었었다. 그리고 데이터가 30개 미만(최대값 미만)인 경우 자동으로 마지막 페이지로 생각하고 추가 요청을 보내지 않는 등의 처리를 해두었었다. 당장 생각나는 가장 최선의 방법이었다고 생각한다.

크롤링 파트 개발은 몇 시간 안걸렸기에 완성하고 마무리 할때 즈음 한가지 의문점이 들었었다. 크롤링이 제한된 요청횟수를 가지고 있다보니, (op.gg의 사용자 정보 요청 처럼)사용자가 별도로 요청하더라도 무작정 아무때나 계속 요청을 허가할 수는 없는 노릇이었다. 그래서 크롤링 요청을 저장해둘 로그 DB가 필요하게 되었다.

등록된 사용자의 이름과 정보는 자동으로 users 컬렉션에 저장해두었었기 때문에, 어떤 사람을 대상으로 요청했었는지와 언제 요청했는지 정도만 갖고있으면 충분할 것으로 보였다. 그래서 빠르게 크롤링 조회 정보를 저장할 모델을 생성했고, 크롤링할 때마다 자동으로 저장하도록 기록함수를 별도로 만들게 되었다. 이 때쯤 lib폴더에 파일이 하나씩 쌓이기 시작했다.

이렇게 만들어둔 덕분에 추후에 화면개발을 진행했을 때 마지막 크롤링 요청시간을 조회한 후, 특정 시간 이내에 크롤링을 요청했다면 요청을 반려하는 기능을 만들 수 있었다. 인증된 요청 건수 5000건의 초기화 단위가 1시간이었기 때문에, 재 요청의 시간도 1시간으로 두었었다. 사용자 정보의 재 요청 시간을 한 시간 간격으로 두니 사용자에게 크롤링 요청 권한을 넘겨주어놔도 비교적 위험도가 적어지게 되었다.

2. 스케줄러 만들기

2.1. 최소한의 라우팅 하기

크롤링을 완성하고 임시 라우팅 함수로 테스트를 거치고 나니, 생각보다 문제없이 작동했다. 이제 이름을 등록하는 과정이 필요했고, 임시 라우터 외에 처음으로 정식으로 사용할 라우터를 만들게 되었다. 사용자의 이름을 넘겨주면 해당 이름으로 github측에 데이터를 조회한 후, 실제 존재하는 사용자일 경우 사용자 컬렉션에 추가해주고 아니라면 에러 처리하는 API 함수를 만드는 과정이었다.

이 때쯤 주소에 최소한의 룰을 정하기 시작했었다. RESTful하도록 주소를 설계해보고 싶었기에, (이해한게 맞는지 모르겠지만)최대한 직관적으로 알아볼 수 있도록 주소를 설정해봤다. 우선 위의 동작은 사용자등록하는 것이었기때문에, 사용자에 관련된 행동을 모두 처리할 라우터를 만들었다.

이 때 nodejs가 하는 연산들은 모두 /api라는 경로 아래에 두기로했다. 추후에 클라이언트와 합쳐졌을 때 한 호스트에서 주소요청이 오갈 것이기 때문에 클라이언트 요청과 서버 요청의 구분을 두기 위해서였다.

이 라우터는 /api/users라는 경로로 접근할 수 있게 만들어 두었다. 이 때에 처음으로 정한일은 두가지 였다.

  1. 요청의 주체가 되는 경우에 기능들을 모아둘 것
  2. 최대한 동사는 지양하고, 명사로 주소를 정의하되 컬렉션 형태의 리소스는 복수로 지정할 것

1번의 이유는, 주소를 읽어보면 한번에 무엇을 하려고 하는 것인지 파악할 수 있도록 하려는 목적이 컸다. 그래서 폴더 형식이 아니라 앞에서부터 주소를 문장처럼 읽을 수 있길 바랬다. 예를 들어 특정 프로젝트에 등록된 사용자의 목록을 가져와라는 것이 요청이라면, 주체는 사용자의 목록이라고 보기로 한 것이다. 그래서 /users/challenges/:challenge_id의 형태의 주소를 사용하기로 한 것이다.

나중에 깨달았지만, 저 형태는 계층구조를 따르고 있지않아 오히려 가독성을 해치는 구조였다는 것을 알았다. RESTful API에 대한 이해가 떨어졌던 큰 예가 아닌가 싶다. 그래서 저 API구조는 추후에 대대적으로 수정되게 된다.

그리고 2번의 경우는 RESTful한 주소 형태를 띄기 위해 가장 기초적인 부분이 아니었나싶다. 최대한 명사 형태로 주소를 만들기로 했고, 만약 몇가지 데이터를 아우르는 컬렉션에 대한 요청이라면 복수형으로 사용하기로 했다. 그리고 행동은 HTTP Method로 구분짓는 것 까지가 처음 정했던 규칙이었다.

저런 룰로 사용자 추가를 위해 POST /api/users/:user_name이라는 형태로 첫 라우터를 만들었었다. 이후에도 저런 형태를 계속해 따르게 된다.

2.2. node-cron ? node-scheduler ?

위에서 주소를 한번 디자인 해 본 후, 요청을 보내면 크롤링하는 기능까지 모두 작성했었다. 그 다음엔 자동으로 크롤링하는 기능이 필요했다. 이 프로젝트의 기능 중 하나는 특정 시간마다 데이터를 새로 크롤링해와 사용자들의 정보를 갱신한다는 것이었다. 나도 자동으로 크롤링을 하도록 스케줄러를 작성할 필요가 있었고, nodejs환경에서 사용할 수 있는 스케줄러 패키지들을 좀 검색해보았다.

스케줄러들은 대부분 비슷한 형태로 작동하고 있어 적용법이 어렵지는 않았으므로 테스트를 거쳐 node-schedulernode-cron으로 압축했다. 처음에는 node-cron으로 제작해보았는데, 생각보다 제시간에 동작하지 않는 치명적인 버그가 종종 발생했다. 아마도 내가 잘못 적용했지 싶긴하지만, 어쨌든 업데이트의 누락이 계속 발생하는 치명적인 문제가 있었으니 제외하기로했다.

그 후 node-scheduler를 사용해보니 설정법은 동일했지만, 제시간에 정상적으로 잘 작동하는 것을 확인했다. 그리고 여러 스케줄을 만들어서 각자 다른 시간에 독립적으로 실행할 수 있도록 등록해둘 수 있다는 점이 매우 장점으로 작용했다. 그래서 나중에 시간마다 여러 스케줄을 등록할 것을 염두에 두고 스케줄러 부분을 별도로 코드로 작성해 사용하게 되었다.

이 떄 자동으로 크롤링되는 시간은 4시간 간격으로, 한번에 많은 요청을 해야하니 일부러 좀 긴 간격으로 조정해 두었다. 혹시 조회가 느리다면 사용자에 대해 1시간에 한번씩 직접 요청할 수 있었으므로, 문제는 없으리라 생각했다.

정리하며

이어진 개발 과정

백엔드 개발은 이후, 정리해둔 API 목록대로 작업했다. 모든 API는 동일한 형태의 응답을 반환하도록 디자인 해두었었다. 자동으로 상태 코드와 메시지를 반환하지만, 내쪽에서도 몇가지 경우의 수를 준비해 둘 필요가 있었기 떄문에 다음과 같은 응답 형태를 띄도록 해두었었다.

1
2
3
4
5
6
7
8
9
10
res.json({
    code : 1                // 응답 코드, 0보다 큰 경우 정상, 0보다 작거나 같은 경우 비정상 처리
    status : 'SUCCESS'      // 응답 상태 코드, 별도의 메시지가 들어갈 경우가 생길 것을 대비함
    message : "조회되었습니다"  // 여차하면 처리 후 해당 메시지를 클라이언트에서 안내 메시지로 출력할 요량으로 만들었다.
    data : []               // 실제 데이터를 data라는 부분에 넣어두었었다. 다만 이것 때문에 구조가 복잡해지기도 했다. 
    error : {               // 에러가 있을 때에만 넘겨주었다. 
        message : "",       // 실제 에러 메시지가 들어있었다. 
        object : {}         // 에러의 객체 그 자체를 넘겨줄 때 사용했다.
    }
})

모든 API에서 이런 기본 구조를 갖도록했다. 물론 이 덕분에 클라이언트 부에서 response.data.data같은 형태의 보기 힘든 코드가 생겨나게 되었는데, 자체적으로 만드는 에러같은 것들을 사용하기 위해서 이런 구조를 사용하게 되았다. 이런 구조를 띄는 것이 옳은지는 아직도 잘 모르겠다. 데이터 자체만 돌려주는 것이 맞는 것인지 저게 더 안전한 것인지 아직도 헷갈린다.

주로 사용된 패키지들

정말 다양한 외부 기술들을 사용했는데, 주로 사용한 것은 다음과 같았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
    "dependencies": {
        "@babel/core": "^7.9.0",                // nodejs를 babel로 실행하기 위해 사용
        "@babel/node": "^7.8.7",
        "@babel/preset-env": "^7.9.5",
        "axios": "^0.19.2",                     // 비동기 통신에 사용됐다.
        "cors": "^2.8.5",                       // 개발 당시 크로스 오리진 문제 해결을 위해 사용
        "dotenv": "^8.2.0",                     // 개발과 배포의 환경변수 조작을 위해 사용했다. 주로 db 호스트를 적어두었다.
        "express": "~4.16.0",                   // 주요 뼈대
        "http-proxy-middleware": "^1.0.4",      // 개발 시, react 개발서버로 주소를 프록시 연결하기 위해서 사용했다.
        "moment": "^2.24.0",                    // 날짜 조작에 자주 사용했다.
        "mongodb": "^3.5.6",                    // 데이터 베이스 연결 부 
        "mongoose": "^5.9.9",                   // 주로 사용한 데이터베이스 라이브러리였다.
        "node-schedule": "^1.3.2",              // 특정 시간마다 작업을 스케줄링하기 위해 사용했다.
        "octonode": "^0.9.5",                   // 깃허브 API를 쉽게 사용하기 위해 사용했다.
  },
}