Skip to content

rae-han(6주차): 6. 멀티패러다임 프로그래밍 #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 19, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions raehan/6. 멀티패러다임 프로그래밍.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
## 1️⃣ executeWithLimit: 기본 동시성 제어 함수

### ● 개념

- 여러 개의 비동기 작업(Promise 반환 함수들)을 일정 개수(**`limit`**) 단위로 묶어서,
순차적으로 실행하는 함수입니다.

### ● 전통적 명령형 구현

```javascript
async function executeWithLimit(fs, limit) {
const results = [];
for (let i = 0; i < fs.length; i += limit) {
const batchPromises = [];
for (let j = 0; j < limit && i + j < fs.length; j++) {
batchPromises.push(fs[i + j]());
}
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
}
return results;
}
```

- **동작**:
1. 전체 작업을 **`limit`** 크기만큼 잘라서(batch)
2. 각 batch를 **`Promise.all`**로 병렬 실행
3. batch가 끝나면 다음 batch를 실행
4. 모든 결과를 순서대로 합쳐서 반환

### ● 함수형 스타일 변환

- **`map`**, **`chunk`**, **`Promise.all`** 등 함수형 유틸리티를 활용해 선언적으로 바꿀 수 있음
- 예시: FxTS 라이브러리 사용

```javascript
fx(fs)
.map((f) => f())
.chunk(limit)
.map((ps) => Promise.all(ps))
.to(fromAsync)
.then((arr) => arr.flat());
```

- **장점**: 코드가 간결해지고, 선언적이어서 의도가 명확해짐

---

## 2️⃣ runTasksWithPool: 풀 사이즈 기반 동시성 제어

### ● 요구사항 변화

- 단순히 batch 단위로 순차 실행하는 게 아니라,
**동시에 실행되는 작업의 개수(poolSize)를 항상 일정하게 유지**하고 싶음
- 즉, poolSize만큼 작업을 동시에 실행하고,
하나가 끝나면 대기 중인 작업을 즉시 추가하는 방식

### ● ChatGPT 명령형 구현

```javascript
async function runTasksWithPool(fs, poolSize) {
const results = [];
const activePromises = [];
for (let i = 0; i < fs.length; i++) {
const p = fs[i]()
.then((res) => {
results[i] = res;
})
.then(() => {
// 완료된 작업을 activePromises에서 제거
const idx = activePromises.indexOf(p);
if (idx > -1) activePromises.splice(idx, 1);
});
activePromises.push(p);
if (activePromises.length >= poolSize) {
await Promise.race(activePromises);
}
}
await Promise.all(activePromises);
return results;
}
```

- **핵심 로직**
- **`activePromises`** 배열을 사용해 현재 실행 중인 작업을 추적
- poolSize에 도달하면 **`Promise.race`**로 가장 먼저 끝나는 작업을 대기
- 작업이 끝나면 배열에서 제거, 새 작업을 추가
- 모든 작업이 끝날 때까지 반복
- **문제점**
- 상태 관리가 복잡(배열 인덱스, 제거 등)
- 코드 가독성 떨어짐

---

## 3️⃣ 멀티패러다임적 개선: TaskRunner & TaskPool 도입

### ● 개선 목표

- 복잡한 상태 관리(작업 완료 추적, 결과 저장 등)를
더 명확하고 직관적으로 만들기

### ● TaskRunner 클래스

- 각 비동기 작업을 객체로 감싸서,
**상태(완료 여부, Promise 등)를 명확하게 관리**

```javascript
class TaskRunner {
constructor(f) {
this.f = f;
this._promise = null;
this._isDone = false;
}
get promise() {
return this._promise ?? this.run();
}
get isDone() {
return this._isDone;
}
async run() {
if (this._promise) return this._promise;
return (this._promise = this.f().then((res) => {
this._isDone = true;
return res;
}));
}
}
```

### ● 개선된 runTasksWithPool

```javascript
async function runTasksWithPool(fs, poolSize) {
const tasks = fs.map((f) => new TaskRunner(f));
let pool = [];
for (const nextTask of tasks) {
pool.push(nextTask);
if (pool.length < poolSize) continue;
await Promise.race(pool.map((task) => task.run()));
// 완료된 작업 제거
pool.splice(
pool.findIndex((task) => task.isDone),
1
);
}
// 남은 작업 모두 완료 대기
return Promise.all(tasks.map((task) => task.promise));
}
```

- **장점**
- 각 작업의 상태를 객체로 관리 → 코드가 명확해짐
- 배열 메서드(map, findIndex, splice)로 풀 관리가 간단해짐
- for...of와 await로 제어 흐름이 직관적

---

## 4️⃣ 동적 풀 크기 및 무한 작업 지원: TaskPool 클래스

### ● 클래스 기반 확장

- **상태와 로직을 한 곳에 모아 관리**
- 동적 풀 크기 조절, 무한 작업 지원 등 다양한 요구사항 대응

```javascript
class TaskPool {
constructor(fs, poolSize) {
this.tasks = fs.map((f) => new TaskRunner(f));
this.pool = [];
this.poolSize = poolSize;
}
setPoolSize(poolSize) {
this.poolSize = poolSize;
}
canExpandPool() {
return this.pool.length < this.poolSize;
}
async runAll() {
let i = 0;
const { length } = this.tasks;
while (i < length) {
const nextTask = this.tasks[i];
this.pool.push(nextTask);
const isNotLast = ++i < length;
if (isNotLast && this.canExpandPool()) continue;
await Promise.race(this.pool.map((task) => task.run()));
this.pool.splice(
this.pool.findIndex((task) => task.isDone),
1
);
}
return Promise.all(this.tasks.map((task) => task.promise));
}
}
```

### ● 무한 반복 작업 지원 (이터러블/이터레이터)

- 작업 리스트를 배열이 아니라 **이터러블**로 받아서,
무한 반복 작업(예: 크롤러, 스트림 등)에도 대응
- while(true) 루프와 이터레이터의 next() 사용

---

## 5️⃣ runAllSettled: 실패 무시하고 전체 결과 수집

- **runAll**: 하나라도 실패하면 전체 중단 (**`Promise.all`**과 동일)
- **runAllSettled**: 실패해도 모든 작업이 끝날 때까지 기다리고,
성공/실패 결과를 모두 배열로 반환 (**`Promise.allSettled`**와 동일)

```javascript
async runAllSettled() {
return Promise.allSettled(await this.run(() => undefined));
}
```

- errorHandle 콜백을 통해 에러 처리 전략을 유연하게 선택할 수 있음

---

## 6️⃣ 결론 및 멀티패러다임적 사고의 장점

- **객체지향**: 복잡한 상태(작업 풀, 동적 크기 등) 관리에 강점
- **함수형**: 데이터 변환, 리스트 처리에 강점
- **명령형**: 흐름 제어, 반복, 조건 분기 등에서 명확성 제공
- **조화롭게 섞으면** 복잡한 동시성 문제도 가독성, 유지보수성, 확장성을 모두 확보할 수 있음