스테이징 환경을 거뜬히 통과한 API라도 실제 트래픽이 몰리는 순간 무너질 수 있습니다. 장애는 대개 극적으로 일어나지 않습니다. 수많은 작은 문제들이 쌓여 발생하는 '천 번의 칼질에 의한 죽음(death by a thousand cuts)'과 같습니다. 여기서는 스레드가 차단되고, 저기서는 불필요한 쿼리가 실행됩니다. 부하가 적을 때는 이러한 작은 비효율성이 숨겨져 있습니다. 하지만 운영 환경의 압박이 가해지면, 이들은 스레드 풀 고갈(thread pool starvation), 데이터베이스 병목(database choking), 그리고 사용자 신뢰를 무너뜨리는 응답 시간 지연으로 이어집니다. 성능 튜닝은 단 하나의 마법 같은 해결책을 찾는 것이 아닙니다. 모든 계층이 스레드, 메모리, 데이터베이스 연결의 희소성을 존중하도록 시스템을 구축하는 것입니다. 이러한 리소스를 유한한 것으로 취급할 때, 장애에 사후 대응하는 것이 아니라 장애를 예방하기 시작할 수 있습니다.

Sync-over-Async가 당신을 망치기 전에 먼저 제거하세요

트래픽이 많은 ASP.NET Core 애플리케이션에서 가장 파괴적인 패턴은 단연 sync-over-async입니다. 값을 즉시 필요로 하거나 호출 스택을 리팩터링하고 싶지 않아서 비동기 메서드에 .Result.Wait()를 호출할 때 이 패턴이 나타납니다. 그러한 결정은 호출 스레드를 차단합니다. 스레드는 다른 곳에서 이미 진행 중인 작업을 기다리며 유휴 상태로 머물게 되지만, 런타임은 이를 다른 요청에 재사용할 수 없습니다.

충분히 많은 요청이 이렇게 되면 스레드 풀이 고갈됩니다. 프로세서가 바쁘지 않기 때문에 CPU 그래프는 정상처럼 보일 수 있지만, 지연 시간(latency)은 폭발적으로 증가합니다. 요청들은 절대 해제되지 않을 스레드를 기다리며 큐에 쌓입니다. 해결책은 기계적이지만 절제력이 필요합니다. 호출 스택의 맨 위까지 await를 사용하십시오. 어떤 메서드가 비동기 API를 호출한다면, 그 메서드 자체도 비동기여야 합니다. 여기에는 지름길이 없습니다. 동기식 차단 요소를 하나씩 제거할 때마다 실제 트래픽을 처리할 수 있는 여유 공간을 확보하게 됩니다.

연결이 끊긴 클라이언트를 위해 작업을 낭비하지 마세요

클라이언트는 연결을 끊습니다. 브라우저는 탭을 닫습니다. 모바일 앱은 신호를 놓칩니다. 서버가 클라이언트가 떠났다는 사실을 모른다면, 아무도 받지 않을 응답을 위해 데이터베이스 쿼리를 계속 실행하고, JSON을 파싱하며, 스레드를 낭비하게 됩니다. 이를 지원하는 모든 비동기 작업에 CancellationToken을 전달하십시오. 이는 Entity Framework 쿼리, HttpClient를 사용한 HTTP 호출, 그리고 모든 장시간 실행되는 백그라운드 작업을 의미합니다.

연결이 끊기면 토큰이 취소를 트리거하고 작업이 즉시 중단됩니다. 이를 통해 데이터베이스 CPU 사이클을 보존하고 스레드를 더 빠르게 풀로 반환할 수 있습니다. 메서드 시그니처의 작은 변경만으로도 부하 상황에서 큰 효과를 볼 수 있습니다.

변하지 않는 데이터는 캐싱하세요

제품 카탈로그는 아마 매 요청마다 바뀌지 않을 것입니다. 설정 플래그는 확실히 그렇지 않고요. 하지만 많은 API가 동일한 정적 데이터를 위해 데이터베이스를 반복적으로 조회합니다. ASP.NET Core의 Output caching을 사용하면 렌더링된 응답을 저장하고, 컨트롤러나 데이터베이스를 다시 거치지 않고 메모리에서 직접 응답을 제공할 수 있습니다.

태그 기반 무효화(tag-based invalidation)를 신중하게 사용하십시오. 제품을 업데이트할 때는 해당 카테고리나 항목과 연결된 태그만 무효화하면 됩니다. 전체 캐시를 비울 필요는 없습니다. 이렇게 하면 캐시 적중률(cache hit ratio)을 높게 유지하면서 데이터베이스 쿼리 횟수를 낮게 유지할 수 있습니다.

데이터 액세스부터 먼저 해결하세요

대부분의 API에서 데이터베이스 호출은 요청 시간의 대부분을 차지합니다. 다른 무엇을 최적화하기 전에 이곳을 먼저 살펴보십시오.

데이터를 단순히 표시하기 위해서만 조회하고 업데이트할 계획이 없다면, Entity Framework 쿼리에 AsNoTracking()을 추가하십시오. EF Core는 변경 추적(change tracking)과 스냅샷 생성을 건너뜁니다.