백엔드 프로젝트 과정

이건선·2023년 5월 4일
0

해결

목록 보기
30/48

백엔드 팀의 과정

Nest.js와 TypeORM을 사용하게 된 과정

  1. express sequelize로 개발시작

  2. 개발 과정에서 sequelize의 리미트와 오프셋 설정을 쿼리스트링으로 가져올 시에 타입이 맞지 않아서 페이지 네이션을 정상작동하지 않는 문제 발생했다.

  3. 타입 체크의 중요성을 깨닫고 취업시장에서도 타입스크립트를 반 필수적으로 요구하는 것을 발견했다.

  4. TypeScript가 기본으로 내장되어있는 Nest.js에 관심을 가지게 되었다.

  5. express sequelize 개발 완료 후 Nest.js로 마이그레이션 계획 세웠다.

  6. Nest.js의 ORM을 어떻게 조합할까 고민하고 직접 사용해서 테스트했다.

    1. Prisma
      1. 장점:
        1. 스타팅이 매우 쉽고 간결하다.
          1. Sequelize,TypeOrm에 비해서 스타팅이 쉽다는 것은 매우 큰 장점이다. ORM을 사용하기 위해서 prisma orm 설치하고 서비스 파일을 관리하는 것 이외에 다른 부가적인 설정들을 건드리지 않아도 된다. sequelize에 익숙하다면 몇몇 부분을 제외하고 Sequelize와 비슷한 방식으로 Prisma ORM을 사용 할 수 있다.
        2. prisma pull
          1. prisma pull은 Prisma에서 제공하는 강력한 기능으로 이미 작성되어 있는 데이터베이스를 기반으로 삼아서 스키마를 자동으로 한 번에 만들어 준다. 이렇게 함으로써 이미 구성되어있는 데이터 베이스를 손쉽게 가져와서 개발 시간을 단축 할 수있게한다. 또한 스키마 변경시 prisma push하고 prisma migration해서 쉽게 데이터 베이스를 관리 할 수 있다.
      2. 단점 :
        1. prisma pull
          1. prisma pull은 강력한 기능이지만 이렇게 가져온 스키마를 데이터 베이스와 연동하기 위해서 prisma migration을 실행하면 높은 확률로 데이터 베이스의 초기화를 묻는 경고 메세지가 나타난다. y를 누르면 예외없이 데이터베이스를 초기화 해버린다. 그렇게 한 번 migration을 진행한다면 이 후에는 경고 메세지가 나타나지 않지만, 그 한번이 치명적이다.
          2. 위 문제를 해결하기 위해서 여러 곳을 찾아다녔다. 하지만 공식사이트와 그 어느 사이트에서도 이 문제에 관한 해답을 찾지 못했다. 어떤 블로그에서 product 환경시에는 발생하지 않는다고 글을 보았지만 해결법을 제시하지는 않았다.
        2. 객체 중첩 해결방법 부재
          1. Sequelize에서는 객체 중첩을 간단하게 해소하기 위해서 raw:true 기능을 제공하지만 Prisma에서는 제공하지 않는다. 따라서 데이터 가공을 위해서 직접 객체 중첩을 풀어주는 메서드를 만들어야 한다. 이렇게 가공 메서드를 사용하면 데이터를 클라이언트에 제공할 때 속도 저하가 발생한다.
        3. Sequelize.fn()과 TypeORM QueryBuilder에 비교하면 애매한 입장에 서있는 내장함수
          1. Sequelize는 Sequelize.fn() 내장함수를 제공한다. 이 내장함수는 SQL 쿼리를 직접 작성할 필요가 없어 코드의 복잡성이 줄어든다.
          2. TypeORM QueryBuilder는 메서드 체인 방식으로 쿼리를 작성할 수 있어 코드의 가독성이 상대적으로 좋다
          3. Prisma는 아쉽게도 SQL 쿼리를 실행할 수 있다는 것 외에 Prisma executeRaw 가 가진 특출난 장점이 없다. 즉, Sequelize.fn(), TypeORM QueryBuilder이 가지지 못한 장점이 없는 것이다. 그래서 alias를 지정하기 위해서 SQL 쿼리를 실행 할 수 밖에 없게 됨으로써 프로퍼티의 키네임을 바꾸기 위해서는 SQL 쿼리를 실행 하던가 아니면 매핑해서 데이터의 이름을 바꿔야하는 두 가지 선택지만 남게 된다. 데이터를 매핑하게 된다면 많은 데이터를 한번에 가져와야 할 때 데이터 처리 속도에서 손해를 보게되고 SQL 쿼리를 쓰자면 코드의 가독성과 일관성이 떨어질 수 있다는 점이 아쉽다.
    2. Sequelize :
      1. 장점 :
        1. 강력한 기능
          1. sequelize를 설치하고 간단한 모델설정을 마치면 손쉽게 데이터 베이스에 접근하고 사용이 가능하다.
        2. 내장 함수의 용이함
          1. Sequelize.fn() 을 사용하면 ORM사용중에도 내장 함수를 사용 할 수 있다. 의도하고 있는 특정 부분에만 내장함수를 사용해서 복잡한 쿼리문을 줄일 수 있다는 것은 큰 장점이다.
        3. 데이터 베이스 동기화
          1. 데이터 베이스 동기화도 간단하다. 모델을 이용한 싱크방식이 권장되는 방법은 아니지만 DEV단계에서 유용한 옵션으로 모델의 {force: false, alter: true} 옵션을 사용하면 데이터 베이스의 모델이 변경 되어도 데이터 베이스 드랍없이 데이터 베이스에 적용이 가능하다.
      2. 단점 :
        1. 데이터 타입에 따른 오류 발생
          1. 사실 TypeScript를 사용하지 않았을 때 모든 ORM에서 발생 할 수 있는 문제다. 하지만 우리팀의 경우를 소개해 보자면 GET요청으로 쿼리스트링 숫자를 받아오고 받아온 숫자를 이용해서 페이지 네이션 기능을 구현해야하는데, 이 때 리미트와 오프셋에 들어갈 인자 값의 타입이 string이라서 오류가 발생하는 문제가 있었다.
    3. TypeORM :
      1. 장점 :
        1. 강력한 기능
          1. sequelize와 대부분의 장점을 공유하면서 Sequelize와는 차별화 된 주요 기능이 두 가지 존재한다.
          2. 하나는 migration으로 엔티티 변경사항을 남길 수 있는 Timestamp 기능이다. 이를 통해서 데이터베이스의 변경 사항을 안전하게 적용하고 추적 할 수 있다.
          3. 두번째는 TypeORM에서 제공하는 QueryBuilder로 메서드 체인 방식으로 쿼리를 작성할 수 있어 코드의 가독성이 상대적으로 좋다. 메서드 체인 방식으로 이루어지기 때문에 if 분기를 통한 코드의 재사용성도 고려해 볼 수 있다.
      2. 단점
        1. 스타팅 난이도가 높다
          1. TypeORM을 사용하기 위해서 의존성을 주입해 주어야하고 config 파일을 작성해야한다. 그런데 이 config파일의 설정 난이도가 높다. 또한 작성해야하는 config 설정중에서 cli 프로퍼티가 DataSource에서 삭제됨에 따라서 이것을 보고 따라 할 수가 없게 되었다. migration cli명령어도 script상에서 수정 해주어야 하며 이 때, ts파일의 위치를 파악하지 못하므로 ts-node 라이브러리를 설치해서 script를 수정하기까지 해야한다.
        2. QueryBuilder의 자체 난이도
          1. 메서드 체인 방식으로 작동하는 QueryBuilder는 쿼리 빌더 메서드를 제외하고, 내부의 자동완성 기능을 지원하지 않는다. 따라서 개발자가 입력시 컬럼의 값을 명확하게 알고 있어야하며 인자를 넘겨 줄 때도 setParameter로 추가해줘야한다.
    • express와 Nest.js를 비교하면 데이터를 가져오는데 누가 더 빠를까?
      • 처음에는 막연하게 Prisma로 작성한 Nest.js가 더 빠르겠지 생각했다. 그러나 같은 코드를 작성하고 테스트 해본 결과 Prisma를 이용해서 데이터를 가져올 때, 객체 중첩을 푸는 과정이 존재하기 때문에 express와 비교해서 약 80ms정도의 손해를 더 보았다. 따라서 express와 Nest.js의 속도비교는 체감상 무의미하고 raw쿼리와 가깝게 잘 짠 코드거나 ORM의 객체 중첩해제 기능지원 여부에 따라서 속도가 결정 된다는 것을 깨달았다.
    1. 위와같은 테스트 결과로 처음에는 Nest.js Prisma 조합을 채택했지만 위에 기술한 단점 중 데이터 베이스 초기화에서 유래되는 Technical debt 를 피하기 위해서 Nest.js와 TypeORM 조합으로 선회해서 Nest.js로 마이그레이션을 진행했다.

    CI/CD Jenkins ⇒ GitHub Actions

    1. Jenkins vs GitHub Actions?

      1. 우선 완성되어있는 express sequelize를 CI/CD하기 위해서 Jenkins vs GitHub Actions 중에서 선택해야했다.

      2. 쉽게 하는 방법으로 GitHub Actions를 선택 할 수있었지만 현재 디펙토에 가까운 프로그램이 Jenkins 라는 의견이 모아졌기 때문에 Jenkins를 채택했다. 호스트 서버에 Jenkins를 설치하는 방법은 두가지 있었는데

        1. 호스트 서버에 직접 설치하는 방법

        2. Docker를 설치하고 image를 이용하는 방법

          Jenkins설치하는 방법 두 가지 다 해 본결과 Docker의 image를 이용하는 편이 편리 했다. 그러나 Jenkins 플러그인 설치시 재부팅과 동시에 초기화 되는 문제가 발생했다. 조사해본 결과 Docker image로 Jenkins container구성시에 -v 옵션으로 젠킨스 컨테이너가 재부팅 해도 젠킨스의 데이터가 보존 될 수 있도록 설정해야 한다는 사실을 알았다. 그리고 그리고 젠킨스의 컨테이너 내부에서 호스트의 Docker 데몬과 통신 할 수 있도록 -v /var/run/docker.sock:/var/run/docker.sock 옵션을 추가해야했다. 이 옵션은 컨테이너 안에서 도커 명령어를 실행할 수 있게 해준다.

    2. Jenkins선언적 파이프라인을 이용한 **CI/CD 플랜

      1. Git main repo에 merge 발생한다.

      2. Jenkins**web 후크로 이를 감지하고 repo의 main을 클론한다.

      3. Jenkins서버에서 클론을 바탕으로 Docker 이미지를 만들고 Jenkins의 빌드 환경변수를 Docker image에 입력해서 버전을 관리한다.

          IMAGE_TAG = "gunsun/realjeans:1.0.${env.BUILD_NUMBER}"
      4. 생성한 이미지를 Docker hub에 올린다.

      5. 각 서버는 프리티어로 만들어졌기 때문에 절대적으로 용량이 부족하다. 따라서 배포 서버에 존재하는 기존 Docker container를 stop하고 삭제하며 구버전 이미지도 삭제한다. 버전 관리는 Docker hub가 담당한다.

      6. EC2서버는 총 4개로 Nginx 서버, Jenkins 서버, 배포서버 2대이다.

      7. Nginx EC2서버에서 라운드 로빈 방식을 취하고 있다.

      8. 서버가 2개 밖에 없기 때문에 롤링 방식으로 진행한다.

      9. 배포 서버는 각각 1번서버 2번서버로 분리되어있고 Nginx에서 각 서버를 10초마다 테스트 하고 서버가 살아있는 유무에 따라서 배포 순차적으로 진행 할 것이다.

      10. 이러한 일련의 과정을 Jenkins선언적 파이프라인 stage로 분할하고 성공하거나 실패하면 슬랙으로 팀원에게 알람이 전달된다.

      11. Nest.js build를 못버티는 Jenkins EC2 프리티어 서버

        1. node.js express를 사용할 때는 문제가 없었지만 Nest.js로 마이그레이션을 마쳤는데 Nest.js로는 CI/CD가 이루어지지 않았다.
        2. Nest.js의 빌드 과정에서 멈추는 문제가 발생했다. EC2 서버를 살펴보니 cpu 사용량 99.7%에 달했다.
        3. 혹시 Docker의 이미지를 만들때 node 알파인 버전을 사용하면 빌드시에 필요한 메모리가 조금이라도 줄어들지 않을까 생각했는데 효과가 없었다.
        4. 왜 빌드 과정에서만 멈추는지 궁금해서 조사해봤다.
        5. NestJS는 TypeScript를 기반으로 하는 프레임워크다. TypeScript는 JavaScript의 상위 집합이며, 브라우저와 Node.js에서 기본적으로 이해할 수 없는 구문을 사용하기 때문에 NestJS 애플리케이션을 실행하기 전에는 TypeScript 코드를 JavaScript로 변환하는 빌드 과정이 필요하다는 것을 깨달았다.
        6. 이때문에 node.js express에서는 문제가 없었지만 Nest.js에서는 빌드 과정에 문제가 발생했던것
        7. 결국 TypeScript의 JavaScript 컴파일시에 근본적인 해결법은 램을 늘리는 것이었고 1GB를 제공하는 t2에서 2GB를 제공하는 t3.small로 티어를 올렸다.
        8. 성공은 했지만 프로젝트의 크기에 비해서 CI/CD에 소모되는 시간값이 컸다. 그리고 이것마저도 빌드의 안정성이 보장되지 않았고 빌드에 실패하는 경우가 대부분이었다.

      //사진 첨부

      1. 돌고 돌아 GitHub Actions 써야한다. 그리고 **버리기 아까운 Jenkins EC2서버
        1. 멘토님과 상담에서 이러한 고민을 질문했고, 근본적인 해결법은 EC2 서버의 사양을 늘리는 것이지만 만약에 Jenkins EC2서버가 터지면 어떻게 해결할 것이냐는 질문을 던지셨다.
        2. Unstable Program을 신용 할 수 없었고 기존의 Jenkins 에서 GitHub Actions으로 CI/CD를 옮기기로 결정했다.
        3. GitHub Actions에서 제공하는 컴퓨터 스펙은 2-core CPU, 7 GB of RAM memory, 14 GB of SSD disk space로 기존의 Jenkins EC2서버를 상회했다. 비용적인 측면에서도 공개 repo의 경우 무료다.
        4. 기존의 Jenkins EC2서버는 선언적 파이프 라인 코드를 변경해서 main에 merge가 발생할 때, 각각 배포서버의 저장공간, docker image, docker container 상태를 확인하고 슬랙으로 정보를 보내는 임무로 변경했다.
      2. GitHub Actions을 이용한 **CI/CD 플랜
        1. Git main repo에 merge 발생한다.

        2. Jenkins EC2서버는 main에 merge가 발생할 때, 각각 배포서버의 저장공간, docker image, docker container 상태를 확인하고 슬랙으로 정보를 보낸다.

        3. Git Hub Actions이 이를 감지하고 이미지를 빌드해서 Docker hub에 올린다.

        4. 각 서버는 프리티어로 만들어졌기 때문에 절대적으로 용량이 부족하다. 따라서 기존의 docker container를 stop하고 삭제하며 기존의 구버전 이미지도 삭제한다. 버전 관리는 Docker hub가 담당한다.

        5. 서버가 2개 밖에 없기 때문에 롤링 방식으로 진행한다.

          run: |
                    docker stop realjeans;docker rm realjeans;docker rmi ${{ secrets.DOCKERHUB_USERNAME }}/realjeans:realjeans
          
                    docker pull ${{ secrets.DOCKERHUB_USERNAME }}/realjeans:realjeans
          
                    docker run -i -t --env-file /home/ubuntu/newJeans.env -d -p 3000:3000 --name realjeans ${{ secrets.DOCKERHUB_USERNAME }}/realjeans:realjeans
      3. GitHub Actions을 이용한 **CI/CD결과
        1. 압도적인 성능개선 효과가 있었다. build 발생시 메모리 부족으로 build가 되지 않던 불안정한 배포 상태가 해결되었고 배포 속도도 향상되었다.
      4. Jenkins vs GitHub Actions!
        1. Jenkins
          1. 장점
            1. 무료로 이용이 가능하다.
            2. 선언적 파이프라인이나 Groove문법을 이용한 파이프라인의 작성으로 세밀한 조정이 가능하다.
            3. Blue Ocean Plugin , Git Parameter Plug-In, Slack Notification Plugin같은 ****다양한 플러그인으로 유용하게 커스텀 할 수있다.
            4. 서버의 사양에 따라서 성능이 증가 할 수있다.
          2. 단점
            1. 스타팅이 굉장히 어렵고 난이도가 높다.
            2. 호스트 서버에 어떻게 어떤식으로 설치할것인가 부터 시작해서, 배포하는 서버에 PUB KEY를 넘겨주고 SSH키를 가져와서 Jenkins서버에 등록하고, Git Web후크를 위한 토큰 준비해야하며 배포 서버에 선언적 파이프라인을 사용하기 위해서 플러그인도 설치해야 한다. 그리고 선언적 파이프라인을 사용하기 위해 sh지식도 약간이나마 있어야한다.
            3. Jenkins를 사용하기 위해서는 호스트 서버가 필요하고 이 호스트 서버의 사양이 낮다면 build과정 진행하지 못한다.
        2. GitHub Actions
          1. 장점
            1. Git hub에서 제공하는 기능이기 때문에, 여러 설정과 설치법이 필요한 Jenkins에 비해서 상대적으로 친숙하게 사용할 수 있고 배포 서버에 연결하는 방법이 간단하다.
            2. workflows 작성법이 Jenkins의 pipeline script에 비해서 상대적으로 친숙하다.
            3. GitHub Actions를 사용하기위해서 서버가 필요하지 않다. 쓰고있는 서버가 좋지않다면 GitHub Actions은 좋은 선택이다.
          2. 단점
            1. 프로젝트가 큰 규모의 프로젝트라면 GitHub Actions의 사양으로는 감당하기 여려워진다.
            2. repo가 퍼블릭이 아니라면 비용을 지불해야 한다.
profile
멋지게 기록하자

0개의 댓글