O
OpenLog
Search
Log in
Back to Suggestions
New Suggest
Suggest edit for "Promise로 동시요청 처리하기"
Suggestion title
Description
</>
Edit
Preview
## Summary ## Reason
</>
Edit
Preview
```ts const A = (async () => { ... })() // 즉시실행 IIFE ``` async로 선언된 함수를 호출하면 즉시 Promise를 반환하고, 내부 비동기 작업이 완료되면 해당 Promise가 resolve된다. (async 함수임을 보여주기 위해 예시는 즉시실행 화살표함수로 작성) 이 특성을 이용하면 동시에 들어온 여러 요청을 하나의 비동기 작업을 합쳐서 처리할 수 있다. (in-flight dedupe) 원리는 첫번째 요청이 왔을때 비동기 작업을 시작한다음 Promise를 상위 스코프 변수로 할당하고, 다음 요청이 들어왔을 때 이 변수에 값이 존재하면 Promise를 반환하고 없으면 이전 로직을 실행하도록 하는 것이다. 동일한 Promise를 여러 요청이 await하면, 작업이 완료되는 순간 모든 요청이 같은 값으로 함께 resume 된다. 예시를 통해 알아보자. ```ts @Injectable() export class RedisCampaignCacheRepository implements CampaignCacheRepository { private readonly logger = new Logger(RedisCampaignCacheRepository.name); private readonly KEY_PREFIX = 'campaign:'; private readonly CAMPAIGN_CACHE_TTL = 60 * 60 * 24; private readonly ALL_CAMPAIGNS_CACHE_TTL_MS = 10_000; // RTB decision hot path (짧은 TTL로 Redis SCAN/JSON.GET 비용 완화) private allCampaignsCache: { value: CachedCampaign[]; expiresAtMs: number; } | null = null; private allCampaignsInFlight: Promise<CachedCampaign[]> | null = null; ``` 위 코드에서 `allCampaignsCache`는 비동기작업이 완료된 후 반환하는 모든 캠페인 목록을 저장하는 변수고, `allCampaignsInFlight`가 바로 현재 진행 중인 비동기 작업의 Promise값을 저장하는 변수다. ```ts async getAllCampaigns(): Promise<CachedCampaign[]> { const nowMs = Date.now(); // 전체 캠페인 캐시값이 존재하고 TTL이 안지났으면 그 값을 리턴 const cached = this.allCampaignsCache; if (cached && cached.expiresAtMs > nowMs) { return cached.value; } if (this.allCampaignsInFlight) { return this.allCampaignsInFlight; } const work = (async () => { try { const pattern = `${this.KEY_PREFIX}*`; const keys: string[] = []; // SCAN으로 모든 campaign:* 키 조회 let cursor = '0'; do { const result = await this.ioredisClient.scan( cursor, 'MATCH', pattern, 'COUNT', 100 ); cursor = result[0]; keys.push(...result[1]); } while (cursor !== '0'); if (keys.length === 0) { return []; } // JSON.GET는 다건 호출 시 latency가 커져 in-flight 요청이 쌓이며 heap spike로 이어질 수 있음 // → pipeline + batch로 라운드트립을 줄입니다. const campaigns: CachedCampaign[] = []; const BATCH_SIZE = 200; for (let i = 0; i < keys.length; i += BATCH_SIZE) { const batchKeys = keys.slice(i, i + BATCH_SIZE); const pipeline = this.ioredisClient.pipeline(); batchKeys.forEach((key) => { pipeline.call('JSON.GET', key); }); const results = await pipeline.exec(); if (!results) continue; results.forEach(([error, result], idx) => { if (error) { this.logger.warn(`캠페인 조회 실패: ${batchKeys[idx]}`, error); return; } if (result && typeof result === 'string') { try { campaigns.push(JSON.parse(result) as CachedCampaign); } catch (parseError) { this.logger.warn( `캠페인 JSON 파싱 실패: ${batchKeys[idx]}`, parseError ); } } }); } return campaigns; } catch (error) { this.logger.error('모든 캠페인 조회 실패', error); return []; } finally { this.allCampaignsInFlight = null; } })(); this.allCampaignsInFlight = work; const campaigns = await work; this.allCampaignsCache = { value: campaigns, expiresAtMs: Date.now() + this.ALL_CAMPAIGNS_CACHE_TTL_MS, }; return campaigns; } ``` 이 구현에서 중요한 포인트는 시작 지점과 대기 시점을 분리했다는 점이다. `getAllCampaigns`메서드는 첫 요청에서 비동기 작업을 시작하고 그 Promise를 `work`라는 변수에 할당한다. 이 resolve되지 않은 Promise는 `allCampaignsInFlight`에 할당되고 다음 라인에서 `await work`를 만나면 이전에 실행한 `(async () => {...}()`는 완료될때까지 대기상태에 들어가고 이벤트루프는 다른 요청을 처리한다. `await work`하는 동안 들어오는 다른 요청들은 아래 코드로 인해 새로운 작업을 시작하지 않고 이전 요청에서 resolve되지 않은 동일한 Promise를 반환받게 된다. ```ts async getAllCampaigns(): Promise<CachedCampaign[]> { ... if (this.allCampaignsInFlight) { return this.allCampaignsInFlight; } ``` 결국 `work`가 resolve되면 Promise를 반환받은 `await getAllCampaigns`모든 요청이 동일한 결과로 함께 resume 된다. 그 결과로 요청이 동시에 몰려도 `SCAN + JSON.GET + JSON.parse`의 무거운 작업을 요청 수 만큼 전부 실행하지 않고 첫 요청 1번만 실행하고 나머지는 같은 `Promise`로 처리하도록 최적화를 할 수 있게 되는 것이다. 대신 그 1번의 작업이 느리면 그 순간 들어온 요청들이 전부 느려진다는 주의점도 있다.
Files changed
No changes yet.
Cancel
Submit suggestion