[Node.JS]code refactoring result: profiling

박두팔이·2024년 3월 28일
0

Node.JS

목록 보기
15/20

1. 코드 리팩토링의 필요성?

  • 현재는 AVALVE_DATASERVER 디렉토리에 모든 파일이 담겨 있다.
  • 프로젝트의 규모가 증가함에 따라 파일이나 코드의 양이 증가할 수 있다. 이를 효율적으로 관리하기 위해 모듈화하고 구조화해야 한다.
  • 파일 구조화는 아키텍처와 단일 책임 원칙을 기반으로 이루어져야 한다. 이렇게 하면 각 기능 별로 코드를 명료하게 분리할 수 있다.
  • 더 나아가 프로젝트의 확장성을 고려한다면, 현재 시점에서 유연한 확장이 용이한 코드로 수정해야 할 필요성이 있다.

2. 아키텍쳐 설계

  • 기존 디렉토리 구조

  • Layered Architecture

    • 각 계층에 특정한 책임과 역할을 가지도록 하여 애플리케이션을 독립적으로 개발, 테스트, 유지보수에 용이
    • 가독성 있는 코드로 개발자 간 의사소통 원활
    • 인터페이스를 정의하여 계층을 분리하고 재사용에 용이

  • 2.1 Architecture 설계
    • Presentation Layer

    • Business Logic Layer

    • Data Access Layer

    • Infrastructure Layer


3. transaction

  • try/catch 을 통한 트랜잭션 처리
  • error 핸들링, rollback 처리를 통한 데이터의 일관성과 무결성 유지

4. 두 개의 port → 단일 인스턴스, 단일 포트로 변경

  • socket.io, http 통신을 위한 두 개의 port를 하나의 port로 관리하여 리소스 사용 최적화, 포트 충돌 방지
  • 배포 과정의 단순화

5. 스코프 변경

  • 기존 var 접근 제어자를 사용 하던 방식을 const, let등으로 변경
  • var 사용을 지양함으로써, 변수 재 할당의 문제로 인한 검증되지 않은 사용자의 데이터 수정을 방지

6. 이중 if문 제거

  • ✨ 이중 if문 제거 및 객체 지향

    1) 🚨 중첩된 if문의 문제

    • 현재 로직의 구조를 살펴보면,

      ```jsx
      if(){ // 첫번째 분기점
      	실행
      } else if {
        실행
        
        if() { // 두번째 분기점
      	  실행
      	} else if {
      		실행
      	}
      }  
      ```

      1.1 🤷‍♀️ Why is it a problem?

    • 새로운 조건이 추가되거나 기존 로직이 변경될 때, 영향을 받는 부분을 파악하고 수정하는 것이 복잡해짐(의존성의 문제)

    • 예상치 못한 버그

    • 중첩된 if-else구문은 가독성 저하의 문제를 야기

    • 테스트 코드 작성에 대한 어려움

    • 따라서, 현업에서는 분기점을 지양한다.


      2) ✨ 해결 방법?

    • 분기를 최소화하여 작은 단위로 분리하고 전략 패턴을 사용하여 공통적인 부분은 추상화하여 재 사용하는 방식으로 구조화한다.

    • *전략 패턴?
      - 알고리즘을 정의하고 캡슐화하는 방식
      - execute()를 사용하여 인스턴스에 접근하고 함수를 실행할 수 있음


      3) 🪄 실제 적용: 전략 패턴을 적용한 구조 재 정의

    • 3.1 /upload Flow of Control

      // app.js
      app.use('/upload', uploadMiddleware, uploadRouter);
      
      // uploadRouter.js
      router.head('/upload_status', (req,res) => 
      	    uploadController.checkUploadStatusHeader(req, res, dbConnection));
          return router;
      
      // uploadController.checkUploadStatusHeader.js
      checkUploadStatusHeader: async (req, res, dbConnection) => {
        try {
            await s3Service.checkUploadStatusHeader(req, res, dbConnection);
            logger.info('upload request header status controller To service');
            res.status(200).send('Upload status checked successfully')
        } catch (error) {
            logger.error('Error procession upload status', error);
            res.status(500).send('Error processing upload');
        }
      }
    • 3.2 if(1) 제거: req.headers.status의 upload_start, upload_finish 에 대한 분기점 제거

      • 3.2.1 UploadInterface인터페이스 정의
        ```jsx
        // 인터페이스 정의
        class UploadInterface {
          async execute(req, dbConnection) {
            throw new Error('Execute method should be implemented');
          }
        }
        ```
        
        - 에러 메세지 일괄 적용
        - 필수 구현 기능 표준화
      • 3.2.2 UploadInterface를 상속받는 class정의.
        • UploadStart, UploadFinis
          // 'upload_start'
          class UploadStart extends UploadInterface {
            async execute(req, dbConnection) {
              logger.info("(HEAD) upload start");
              await deviceRepository.updateHttpAccessStatusToEnabled(req.headers.token, dbConnection);
            }
          }
          
          // 'upload_finish' 
          class UploadFinish extends UploadInterface { 
            async execute(req, dbConnection) {
              logger.info("(HEAD) upload finish");
              await deviceRepository.clearHttpAccessByToken(req.headers.token, dbConnection);
              ...
              ...
              (생략)
      • 3.2.3 각 클래스는 인터페이스에 선언된 execute() 오버라이드해야함
      • 3.2.4 checkUploadStatusHeader()가 controller에서 호출되면!
        // s3Service.js
        const uploadObjectCreated = makeUploadObject[req.headers.status];
        
        if (uploadObjectCreated) {
          await uploadObjectCreated.execute(req, dbConnection);
          ...
          ...
         (생략)
        }
        • uploadObjectCreated 변수 선언 및 할당
          • makeUploadObject[req.headers.status]; 코드가 실행되면 req.headers.status의 값에 해당하는 인스턴스 값을 선언된 변수에 할당한다.

            // 인스턴스 생성
            const makeUploadObject = {
              'upload_start': new UploadStart(),
              'upload_finish': new UploadFinish(fileListManager)
            };
        • 기존에는 if문을 통해 req.header.status에 대한 값을 분기했다면, 현재는 Map구조를 사용하여 key와 value를 쌍으로 묶어 클라이언트 요청 시 필요한 인스턴스만을 생성하도록 변경했다.
        • 이렇게 할당된 변수에 담긴 값은 key에 따라 생성된 **new 인스턴스**이다.
        • 여기서 사용된 if문은 분기 처리가 아닌 인스턴스 생성에 목적을 두고 있기 때문에 앞서 설명된 중첩된 if문의 문제와는 다르다.
    • 3.3 if(2) 제거: req.headers.device_name의 Sensorbox, yongin_camera에 대한 분기점 제거

      • 기존에는 중첩된 if문을 통해 req.headers.device_name에 대한 로직을 처리했다면, switch문을 통해 Sensorbox, yongin_camera에 대한 Url주소만 할당하여 다소 복잡했던 로직을 단순화시켰다.
        ex) 총 책임자 → 역할 분리
        ```
        // 디바이스 타입에 따라 다른 람다 URL 사용
            const deviceName = req.headers.device_name;
            let lambdaUrl = '';
        
            switch (deviceName) {
              case 'Sensorbox':
                lambdaUrl = config.lambdaUrls.SENSORBOX;
                break;
              case 'yongin_camera':
                lambdaUrl = config.lambdaUrls.YONGIN_CAMERA;
                break;
              default:
                logger.info('Unknown device type');
                return;
            }
        ```
    • 3.4 람다 함수 처리

      • 기존에는 람다로 처리되는 함수가 중복되어 작성되어 있었다.
      • url에 대한 주소가 그대로 코드에 노출되어 있었다.
        if (req.headers.device_name == "Sensorbox") { 
            for (var item of json_list) {
                logger.info("%s Lambda URL Request", item)
                request.post({
                    headers: {"json_filename": item},
                    url: 'https://3hu37ei2ieqgwo5tpqqbjkrhfy0answm.lambda-url.ap-northeast-2.on.aws/'
                }, function(error, response, body) {
                    logger.info(body)
                });
            }
            json_list = []
        } else if (req.headers.device_name == "yongin_camera") {
            for (var item of json_list3) {
                logger.info("%s Lambda URL Request", item)
                request.post({
                    headers: {"json_filename": item},
                    url: 'https://hbbbcwsqbealfcfrmu7e5hluia0huqwu.lambda-url.ap-northeast-2.on.aws/'
                }, function(error, response, body) {
                    logger.info(body)
                });
            }
            json_list3 = []
        }
        
      • 변경 후,
        • aws lambda url을 config파일에서 객체로 관리
        • if문 안에서 내부 if으로 작성된 로직 중 공통된 로직을 하나의 함수로 정의한 뒤, 분리하였다.
          // configs.js
          lambdaUrls: { 
                  sensorbox: process.env.SENSORBOX,
                  yonginCamera: process.env.YONGIN_CAMERA
          }
          
          // s3Service.js
          function processLambdaRequest(jsonUploadList, lambdaUrl) {
            jsonUploadList.forEach(item => {
              logger.info("%s Lambda URL Request", item);
          
              axios.post(lambdaUrl, {}, {
                headers: { 'json_filename': item }
              })
              .then(response => {
                logger.info(response.data);
              })
              .catch(error => {
                logger.error(error.response ? error.response.data : error.message);
              });
            });
          }
        • http통신을 위한 request모듈은 현재 관리되지 않고 있기 때문에 axios 라이브러리로 변경하였다.
    • 3.5 파일 리스트 관리

      • 기존, upload_finish인 경우 json_list, json_list2, json_list3 을 처리한 뒤 초기화하는 로직이 구현되어 있었다.
        var json_list = [];
        var json_list2 = []; 
        var json_list3 = []; 
      • 리팩토링 후, 변수를 변경하고 파일 리스트를 관리하는 클래스를 정의하고 addFile(), clearFiles() 에서 Sensorbox, yongin_camera에 대한 파일을 분류한 뒤, 업로드 처리가 끝나면 리스트를 초기화 한다.
        // 파일 리스트를 관리하는 클래스
        class FileListManager {
          constructor() {
            this.sensorboxJsonFiles = []; // Sensorbox 디바이스에서 업로드된 JSON 파일 목록
            this.allUploadedJsonFiles = []; // 모든 디바이스에서 업로드된 JSON 파일 목록
            this.yonginCameraJsonFiles = []; // yongin_camera 디바이스에서 업로드된 JSON 파일 목록
          }
        
          addFile(deviceName, fileName) {
            if (deviceName === "Sensorbox") {
              this.sensorboxJsonFiles.push(fileName);
            } else if (deviceName === "yongin_camera") {
              this.yonginCameraJsonFiles.push(fileName);
            }
            this.allUploadedJsonFiles.push(fileName);
          }
        
          clearFiles() {
            this.sensorboxJsonFiles = [];
            this.allUploadedJsonFiles = [];
            this.yonginCameraJsonFiles = [];
          }
        }
        

7. rename

  • 모듈, 상수, 변수, 함수, 인자, 파라미터, url 등 재 명명하여 직관적으로 기능을 파악할 수 있도록 수정

8. 라이브러리 변경

  • 기존 http통신을 위한 request → axios로 변경

9. 문법 업데이트

  • 기존 비동기 처리를 위한 Promise객체 사용을 직관적인 async/await 문법으로 변경
  • 구조 분해 할당 문법 사용
    • 구조 분해 할당 문법 정리
      • 객체나 배열 내부의 값을 쉽게 추출하여 변수에 할당할 수 있게 해주는 강력한 JavaScript 문법

      • req.headers 객체에서 device_ownerdevice_name 속성을 추출하고, 이들을 각각 deviceOwnerdeviceName이라는 새로운 변수에 할당

        const { device_owner: deviceOwner, device_name: deviceName } = req.headers;

10. 기존 코드에서 PROBLEMS 20건 이상 해결

  • PROBLEMS ex. file.fieldname이 존재하지 않는 경우 파일의 메타데이터를 생성하는 데 실패할 수 있음. 이런 부분에 대한 예외처리 로직이 필요

  • result. 화면 출력

  • 성능 비교

리팩토링 결과 보고서

Node.js 애플리케이션 성능 분석 보고서

개요

본 보고서는 Node.js 애플리케이션의 리팩토링 전후 성능 분석 결과를 기반으로 작성되었습니다. 리팩토링의 목적은 코드의 가독성, 유지보수성을 높이고, 실행 성능을 개선하는 것이었습니다. 이를 위해 Node.js의 내장 프로파일러를 사용하여 성능 데이터를 수집하고 비교 분석하였습니다.

분석 방법

  • 프로파일링 도구: Node.js 내장 프로파일러
  • 데이터 수집: 프로파일링 결과로부터 실행 시간, 함수 호출 횟수, 가비지 컬렉션(GC) 활동 등을 수집
  • 비교 기준: 총 실행 시간(틱 수), JavaScript 실행 비율, C++ 및 기타 시스템 호출, 주요 함수의 실행 비율 및 시간

실행 조건

아래와 같은 데이터리스트 전송

  • 조건(1): img/ 100개
  • 조건(2): json/ 9.07KB, 1개

결과:

애플리케이션 리팩토링 효과 분석 보고서

리팩토링 과정을 거친 후, 우리의 애플리케이션 성능은 상당한 개선을 보였습니다. 특히, 전체 프로파일링 틱 수에서의 감소는 응답성의 대폭적인 향상을 의미합니다. 이 문서에서는 리팩토링 전과 후의 성능 지표를 비교 분석하여 리팩토링의 효과를 상세히 설명합니다.

성능 개선 개요

  • 프로파일링 틱 수: 리팩토링 후, 전체 프로파일링 틱 수는 75.9% 감소하였습니다. 이는 애플리케이션의 성능이 기존 대비 크게 향상되었음을 의미합니다. 프로파일링 틱 수의 감소는 시스템의 부하가 줄어들었으며, 이로 인해 애플리케이션이 더 빠르게 반응할 수 있게 되었다는 점을 시사합니다.
  • ntdll.dll 실행 비율: 리팩토링을 통해 ntdll.dll에서의 실행 비율이 2.3% 감소하였습니다. 이는 시스템 수준에서의 최적화를 반영하며, 애플리케이션의 효율성을 향상시킨 중요한 요소입니다.
  • node.exe 실행 비율: node.exe에서의 실행 비율이 2.2% 증가하였습니다. 이는 Node.js 런타임 내의 코드 실행이 활발해졌음을 나타내며, 리팩토링을 통해 자바스크립트 코드의 효율이 개선되었음을 의미합니다.
  • 가비지 컬렉션 실행 비율: 가비지 컬렉션(GC) 실행 비율은 8.3% 증가하였습니다. 이는 메모리 관리가 더 활발히 이루어졌음을 의미하며, 리팩토링을 통해 메모리 효율성이 개선되었을 가능성이 있습니다.

리팩토링의 영향

리팩토링 과정에서 시스템 라이브러리 부하가 현저히 감소한 것은 중요한 성과입니다. 특히, ntdll.dll 같은 시스템 라이브러리에서의 시간 소비가 줄어든 것은 시스템 호출과 I/O 작업의 최적화를 의미합니다. 이는 리팩토링을 통해 시스템 레벨에서의 작업이 효율적으로 재구성되었음을 보여줍니다.

또한, Redis를 활용한 캐싱 전략의 도입은 데이터베이스 접근 최소화와 네트워크 지연 감소를 가져왔습니다. 이러한 최적화는 전체 시스템의 성능 향상에 크게 기여하였으며, 고 부하 상황에서의 응답 시간 단축과 사용자 경험 향상을 가능하게 하였습니다.

결론

리팩토링을 통한 성능 개선은 숫자로 증명됩니다. 프로파일링 틱 수의 대폭적인 감소와 시스템 라이브러리 부하의 감소는 리팩토링이 애플리케이션의 성능에 긍정적인 영향을 미쳤음을 명확히 보여줍니다. 가비지 컬렉션 실행 비율의 증가는 더 활발한 메모리 관리를 의미하며, 이는 메모리 효율성의 개선으로 이어졌을 가능성이 높습니다.

종합적으로, 리팩토링은 애플리케이션의 성능을 크게 향상시켰으며, 이는 사용자 경험의 개선으로 직결됩니다. 따라서 리팩토링은 애플리케이션 개발 과정에서 중요한 단계로 간주되어야 합니다.


💡 리팩토링을 하며 느낀, 개발 환경 개선을 위한 시사점

  • jest를 사용한 test code작성: 유닛테스트로 진행할건지, api로 쪼갤건지 고민. 통합테스트는 비추
  • db 마이그레이션: Knex.js or TypeORM라이브러리 사용?
  • 성능테스트: 프로파일링을 통한 CPU 및 메모리 사용량 등 성능 향상 모니터링
profile
기억을 위한 기록 :>

0개의 댓글