Yarn 이 왜빠른지 궁금해서 쓴 글

HumbleMincho·2024년 7월 19일
3
post-thumbnail

1. 병렬 설치

NPM (초기 버전)

초기 NPM 버전은 순차적 설치 방식을 사용했다.
예를 들어, A, B, C 세 개의 패키지를 설치한다고 가정해 보자

  1. A 패키지 다운로드
  2. A 패키지 설치
  3. B 패키지 다운로드
  4. B 패키지 설치
  5. C 패키지 다운로드
  6. C 패키지 설치

이 방식에서는 각 패키지의 설치가 완료될 때까지 다음 패키지의 설치를 시작하지 않는다.

Yarn

Yarn은 처음부터 병렬 설치를 지원했다.

같은 세 개의 패키지를 설치할 때

  1. A, B, C 패키지 동시 다운로드 시작
  2. 다운로드 완료된 순서대로 설치 시작

이 방식은 네트워크 대역폭을 최대한 활용하고, CPU와 디스크 I/O를 효율적으로 사용한다.
예를 들어, A 패키지가 크고 다운로드에 시간이 오래 걸린다면, 그 동안 B와 C의 다운로드와 설치가 진행될 수 있다.

NPM (최신 버전)

NPM 5 이후 버전에서는 Yarn과 유사한 병렬 설치를 도입했다.
그러나 초기 구현에서는 Yarn만큼 효율적이지 않았다


2. 캐싱 메커니즘

Yarn

Yarn의 캐싱 시스템은 매우 공격적이라고 볼 수 있다.
패키지를 처음 다운로드할 때, 전역 캐시에 저장한다. 이 캐시는 버전별로 관리된다.
예를 들어, lodash@4.17.20을 설치한다고 가정해 보자

  1. Yarn은 먼저 로컬 캐시 확인.
  2. 캐시에 없다면 다운로드하고 캐시에 저장.
  3. 다음에 같은 버전의 lodash가 필요할 때, 네트워크 요청 없이 캐시에서 직접 가져옴.

또한, Yarn은 오프라인 미러링을 지원한다. 이는 모든 필요한 패키지를 로컬에 미리 다운로드해 놓고, 완전히 오프라인 상태에서도 설치할 수 있게 한다.

NPM

NPM도 캐싱을 지원하지만, 초기 버전에서는 덜 효율적이었다.
NPM 5 이후 개선되었지만, 여전히 일부 시나리오에서 Yarn보다 덜 효율적일 수 있다.
NPM의 캐시는 패키지 이름버전을 기반으로 하는 폴더 구조를 사용한다.
캐시된 패키지를 사용할 때, NPM은 무결성 검사를 수행하는데, 이 과정이 추가적인 시간을 필요로 할 수 있다.

3. 잠금 파일 사용

Yarn (yarn.lock)

Yarn의 잠금 파일은 매우 상세하다.
1. 각 패키지의 정확한 버전
2. 의존성 트리
3. 무결성 체크섬

등을 포함한다. 이 정보는 매우 구조화되어 있어 빠르게 읽고 처리할 수 있다.

예를 들어, yarn.lock 파일의 일부분은 다음과 같을 수 있다:

lodash@^4.17.20:
  version "4.17.20"
  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
  integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==

이 상세한 정보 덕분에 Yarn은 의존성 트리를 빠르게 재구성하고 설치할 패키지를 정확히 결정할 수 있다.

NPM (package-lock.json)

NPM 5에서 도입된 package-lock.json도 유사한 기능을 한다.
그러나 초기 버전에서는 일부 일관성 문제가 있었고, Yarn의 잠금 파일만큼 효율적이지 않았다.

NPM의 잠금 파일 예:

{
  "name": "lodash",
  "version": "4.17.20",
  "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
  "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
}

4. 네트워크 최적화

Yarn

Yarn은 네트워크 요청을 매우 효율적으로 관리한다. 요청을 큐에 넣고 우선순위를 지정하며, 동시에 여러 요청을 처리한다. 또한, 네트워크 오류에 대한 재시도 메커니즘이 잘 구현되어 있어, 일시적인 네트워크 문제를 잘 처리한다.

예를 들어, 100개의 패키지를 설치해야 한다면

  1. Yarn은 이를 여러 그룹으로 나눈다 (예: 10개씩 10그룹)
  2. 각 그룹을 병렬로 다운로드 시작.
  3. 다운로드가 완료된 패키지부터 즉시 설치 시작
  4. 네트워크 오류가 발생하면 자동으로 재시도.

NPM

초기 NPM 버전은 이러한 최적화가 부족했다.
각 패키지를 개별적으로 처리하고, 네트워크 오류 처리도 덜 효율적이었다.
최신 버전에서는 많이 개선되었지만, 여전히 일부 시나리오에서 Yarn보다 덜 효율적일 수 있다.


5. 알고리즘 최적화!

Yarn

Yarn은 의존성 트리를 구성할 때 매우 효율적인 알고리즘을 사용한다. 이는 결정론적 알고리즘으로, 같은 입력에 대해 항상 같은 출력을 생성한다.

Yarn의 의존성 해결 알고리즘은 다음과 같은 단계로 작동한다.

의존성 그래프 구축

Yarn은 먼저 모든 패키지의 의존성 정보를 수집하여 방향성 비순환 그래프(DAG)를 구축한다. 각 노드는 패키지를, 엣지는 의존성 관계를 나타낸다.

버전 제약 조건 수집

각 패키지가 요구하는 의존성의 버전 범위를 수집한다. 예를 들어:

A 패키지: "C": "^1.0.0"
B 패키지: "C": "^2.0.0"

버전 후보 목록 생성

각 패키지에 대해 가능한 모든 버전의 목록을 생성한다. 이 때 semver(Semantic Versioning) 규칙을 적용한다

제약 조건 해결

Yarn은 SAT(Boolean Satisfiability Problem) solver를 사용하여 모든 버전 제약 조건을 동시에 만족시키는 조합을 찾는다.

a) 각 패키지 버전을 boolean 변수로 변환
b) 버전 제약 조건을 논리식으로 표현
c) SAT solver를 사용하여 모든 조건을 만족하는 해를 찾음

최적화

여러 가능한 해결책 중에서 Yarn은 다음 기준을 사용하여 최적의 해결책을 선택한다.

a) 최신 버전 선호
b) 중복 설치 최소화
c) breaking changes 가능성 최소화

잠금 파일 생성

3. 잠금파일 사용과 중복되는 내용이지만 알고리즘 관점으로 본다면 최종 결정된 의존성 트리를 yarn.lock 파일에 기록한다. 이 파일은 정확한 버전 정보무결성 해시를 포함한다.

구체적인 예시

패키지 AB가 모두 C에 의존하지만 다른 버전을 요구하는 경우:

A@1.0.0
└── C@^1.0.0

B@1.0.0
└── C@^2.0.0
  1. Yarn이 의존성 그래프 구축.
  2. C의 가능한 모든 버전(예: 1.0.0, 1.1.0, 2.0.0, 2.1.0) 나열.
  3. SAT solver를 사용하여 다음 논리식을 해결: (C@1.0.0 OR C@1.1.0) AND (C@2.0.0 OR C@2.1.0)
  4. 가능한 해결책: C@1.1.0과 C@2.1.0 설치
  5. 최적화: 최신 버전인 C@2.1.0을 선택하고, A 패키지에 대해 호환성 경고 발생.

최종 의존성 트리

root
├── A@1.0.0
├── B@1.0.0
└── C@2.1.0

이 과정은 밀리초 단위로 매우 빠르게 실행되며, 수천 개의 패키지가 있는 복잡한 프로젝트에서도 효율적으로 작동한 Yarn의 이러한 알고리즘은 의존성 해결 과정을 매우 빠르고 결정론적으로 만들어, 항상 동일한 환경에서 동일한 결과를 보장한다.

NPM

초기 NPM 버전은 이러한 복잡한 의존성 해결에 더 많은 시간이 걸렸다.
최신 버전에서는 개선되었지만, 특히 매우 복잡한 의존성 트리에서는 여전히 Yarn보다 느릴 수 있다.


6. 플랫 모드

Yarn

Yarn은 가능한 경우 의존성을 '플랫하게' 설치하려고 시도한다. 이는 중복 설치를 최소화하고 디스크 공간을 절약한다.

예를 들어

A
├── C@1.0.0
└── B
    └── C@1.1.0

이를 다음과 같이 평탄화할 수 있다.

A
├── C@1.1.0
└── B

전통적인 방식에서는 B의 두 버전을 모두 설치하지만, Yarn은 가능한 경우 하나의 버전만 설치하려고 시도한다. 이는 설치 시간과 디스크 공간을 절약한다.

NPM

NPM도 최근 버전에서 이와 유사한 기능을 제공하지만, 초기에는 이러한 최적화가 없었다.


7. 메모리 사용!

Yarn

Yarn은 메모리 사용을 매우 효율적으로 관리한다. 대규모 프로젝트에서도 메모리 사용량이 급격히 증가하지 않는다.

예를 들어, 1000개의 패키지를 설치할 때

  1. Yarn은 필요한 정보만 메모리에 유지
  2. 사용하지 않는 데이터는 신속하게 메모리에서 해제
  3. 가비지 컬렉션을 효율적으로 관리하여 memory leaking 방지

yarn의 가비지컬렉션(이하 GC) 방식

1. 캐시 구조

Yarn은 글로벌 캐시를 사용하여 다운로드한 패키지를 저장한다. 이 캐시는 보통 사용자의 홈 디렉토리에 위치한다 (예: ~/.yarn/cache).

2. 캐시 엔트리

각 패키지 버전은 고유한 식별자(해시)를 가지고 캐시에 저장된다.
예: react-16.8.6-994e41b6.tar.gz

3. 컬렉션 트리거

GC는 다음 상황에서 자동으로 또는 수동으로 실행될 수 있다.

  • yarn install 실행 시 (자동)
  • yarn cache clean 명령어 실행 시 (수동)
  • 특정 시간이 경과했을 때 (자동, 설정 가능)

4. GC 프로세스

  1. 사용 중인 패키지 식별

    • 현재 프로젝트의 yarn.lock 파일을 분석
    • 전역적으로 설치된 패키지 확인
    • 다른 프로젝트의 yarn.lock 파일도 선택적으로 검사 (설정에 따라)
  2. 미사용 패키지 식별

    • 캐시 디렉토리의 모든 파일을 스캔
    • 사용 중인 패키지 목록과 비교하여 미사용 패키지 식별
  3. 삭제 대상 결정

    • 미사용 상태가 특정 기간(기본 30일) 이상 지속된 패키지를 삭제 대상으로 표시
    • 이 기간은 설정을 통해 조정 가능 (pruneOffsetInDays)
  4. 삭제 실행

    • 삭제 대상으로 표시된 패키지들을 캐시에서 제거
    • 삭제 과정에서 파일 시스템 오류 등이 발생하면 해당 항목을 건너뛰고 계속 진행

5. 부분 GC

전체 GC 대신 특정 조건에 따라 부분적으로 GC를 실행할 수 있다.

  • 특정 패키지나 버전에 대해서만 GC 실행
  • 특정 시간 범위 내의 캐시 항목만 GC 대상으로 설정

6. GC 최적화

  • 병렬 처리: 여러 캐시 항목을 동시에 처리하여 GC 속도 향상
  • 증분 GC: 전체 캐시를 한 번에 처리하지 않고, 일부분씩 처리하여 시스템 부하 감소

7. 안전 장치

  • 잠금 파일: GC 실행 중 다른 Yarn 작업이 캐시를 수정하지 못하도록 잠금
  • 롤백 메커니즘: GC 중 오류 발생 시 캐시 상태를 이전으로 복원할 수 있는 기능

8. 설정 옵션

yarn config 세팅으로 GC 관련 설정을 조정할 수 있다.

  • cacheFolder: 캐시 위치 지정
  • cacheMaxAge: 캐시 항목의 최대 유지 기간
  • pruneOffsetInDays: 미사용 패키지 삭제 전 대기 기간

예시 명령어:

yarn cache clean  # 전체 캐시 정리
yarn cache clean package-name  # 특정 패키지의 캐시만 정리

NPM

초기 NPM 버전은 대규모 프로젝트에서 메모리 사용이 비효율적이었다.
이는 특히 제한된 리소스의 환경(예: CI/CD 파이프라인)에서 문제가 될 수 있었다.
최신 버전에서는 개선되었지만, 여전히 일부 시나리오에서 Yarn보다 더 많은 메모리를 사용할 수 있다.


결론

이러한 모든 요소들이 결합되어 Yarn이 NPM보다 일반적으로 더 빠른 성능을 보인다.
그러나 NPM도 지속적으로 개선되고 있어, 최신 버전에서는 그 격차가 많이 줄어들었다고는 한다.

실제 성능 차이는 프로젝트의 크기, 의존성의 복잡성, 사용 중인 NPM과 Yarn의 버전 등에 따라 달라질 수 있다.대규모 프로젝트나 복잡한 의존성 구조를 가진 프로젝트에서는 Yarn의 장점이 더 두드러질 수 있다.

profile
javascript, typescript, react-native, react.js, next.js

0개의 댓글