1. 라이브러리
  2. HTTP와 웹
  3. HTTP 상태 코드

업데이트됨 1개월 전

504 게이트웨이 타임아웃은 HTTP에서 가장 독특한 오류 중 하나입니다. 아무것도 고장나지 않았습니다. 업스트림 서버는 완벽하게 작동하고 있을 수도 있습니다—요청을 처리하고, 데이터베이스를 조회하며, 해야 할 일을 정확히 하고 있을 수도 있죠. 게이트웨이가 그냥 기다리다 지쳐서 포기한 것입니다.

이것이 504를 대부분의 오류와 다르게 만드는 이유입니다. 실패에 관한 것이 아닙니다. 시스템 간 기대치의 불일치에 관한 것입니다.

실제로 무슨 일이 일어나고 있는가

504가 보이면, 이런 일이 일어난 것입니다:

  1. 요청이 게이트웨이(Nginx, 로드 밸런서, CDN 등)에 도달했습니다
  2. 게이트웨이가 업스트림 서버로 요청을 전달했습니다
  3. 업스트림 서버가 처리를 시작했습니다
  4. 응답이 도착하기 전에 게이트웨이의 대기 시간이 초과되었습니다
  5. 업스트림이 아직 작업 중일 수 있음에도, 게이트웨이가 타임아웃이 발생했다고 알렸습니다
클라이언트 → 게이트웨이 (60초 타임아웃) → 백엔드 (90초 동안 처리 중)
                ↓
        "더 이상 기다리지 않겠어"
                ↓
        504 게이트웨이 타임아웃

백엔드는 30초 후에 처리를 마치고 응답을 보내지만... 이미 아무도 듣고 있지 않습니다.

타임아웃이 발생하는 이유

업스트림이 느린 작업을 처리하고 있습니다. 수백만 행을 스캔하는 데이터베이스 쿼리. 한없이 느린 외부 API. 실제로 2분이 필요한 보고서 생성. 작업 자체는 정당하지만, 게이트웨이에 설정된 대기 시간을 초과합니다.

자원 부족. 업스트림이 무언가를 기다리고 있습니다: 고갈된 풀에서의 데이터베이스 연결, 다른 프로세스가 잡고 있는 잠금, 스왑 중인 메모리. 작업을 느리게 처리하는 게 아니라—시작조차 못하고 기다리는 상태입니다.

네트워크 지연. 응답은 준비되었지만 게이트웨이와 업스트림 사이 네트워크가 느립니다. 패킷이 기어갑니다. 바이트가 도착하기 전에 게이트웨이의 타이머가 만료됩니다.

연쇄 지연. 백엔드가 다른 서비스를 호출하고, 그 서비스가 또 다른 서비스를 호출하고, 그 서비스가 데이터베이스를 호출합니다. 각 단계마다 지연이 쌓입니다. 응답이 다시 올라올 때쯤에는 원래 게이트웨이가 이미 포기한 상태입니다.

504와 그 사촌들

504 게이트웨이 타임아웃: 업스트림은 아마 괜찮습니다—그냥 느릴 뿐입니다. 게이트웨이가 포기한 것입니다.

502 잘못된 게이트웨이: 업스트림이 유효하지 않은 응답을 반환하거나 응답 도중 충돌했습니다. 실제로 뭔가 고장난 것입니다.

503 서비스 이용 불가: 업스트림이 명시적으로 "지금은 안 된다"고 했습니다. 의도된 상황입니다—과부하 상태이거나 점검 중일 수 있습니다.

408 요청 타임아웃: 방향이 완전히 다릅니다. 서버클라이언트의 요청 전송이 끝나기를 기다리다 포기한 것입니다.

핵심 차이: 504는 게이트웨이가 업스트림을 기다리는 인내심의 문제입니다. 업스트림 자체는 멀쩡할 수 있습니다.

타임아웃 설정

타임아웃은 희망이 아닌 현실을 반영해야 합니다.

server {
    # 빠른 엔드포인트는 짧은 타임아웃
    location /api/users {
        proxy_pass http://backend;
        proxy_read_timeout 30s;
    }

    # 보고서 생성은 시간이 정당하게 걸림
    location /api/reports {
        proxy_pass http://backend;
        proxy_read_timeout 180s;
    }

    # 헬스 체크는 즉각적이어야 함
    location /health {
        proxy_pass http://backend;
        proxy_read_timeout 5s;
    }
}

함정: 모든 엔드포인트에 같은 타임아웃을 적용하는 것. 헬스 체크와 보고서 생성기는 공통점이 없습니다. 다르게 취급하세요.

타임아웃 체인 문제

분산 시스템이 복잡해지는 지점이 여기입니다. 이런 상황이 있을 수 있습니다:

  • 클라이언트 타임아웃: 120초
  • 게이트웨이 타임아웃: 60초
  • 백엔드 작업 시간: 90초

백엔드는 작업을 완료합니다. 하지만 게이트웨이는 60초에 타임아웃됩니다. 클라이언트는 504를 받습니다. 백엔드는 30초 후에 응답을 보낼 준비가 되었지만, 연결은 이미 끊겼습니다.

각 레이어는 아래 레이어들의 동작을 알고 있어야 합니다. 게이트웨이 타임아웃은 해당 게이트웨이가 앞단에 있는 백엔드의 가장 긴 정당한 작업 시간보다 길게 설정해야 합니다.

진짜 해결책: 기다리게 하지 마세요

타임아웃을 늘리는 것은 증상 치료입니다. 근본 원인은 느린 작업에 대한 동기적 대기입니다.

시간이 정당하게 걸리는 작업의 경우:

// 클라이언트를 기다리게 하지 마세요
app.post('/api/reports', async function(request, response) {
    const jobId = await queue.add('generate-report', request.body);
    
    // 작업 ID와 함께 즉시 응답
    response.status(202).json({
        jobId: jobId,
        statusUrl: `/api/jobs/${jobId}`
    });
});

// 완료 여부를 폴링할 수 있게 해주세요
app.get('/api/jobs/:id', async function(request, response) {
    const job = await queue.getJob(request.params.id);
    
    response.json({
        status: job.finished ? 'completed' : 'processing',
        progress: job.progress,
        result: job.finished ? job.result : null
    });
});

202 Accepted 패턴. "들었습니다. 처리 중입니다. 나중에 확인하세요." 즉시 응답하면 어떤 타임아웃도 문제가 되지 않습니다.

빠를 것 같은데 빠르지 않은 작업의 경우:

이유를 찾아내세요. 데이터베이스 인덱스를 추가하세요. 비용이 많이 드는 계산을 캐시하세요. 코드 경로를 프로파일링하세요. 빠른 엔드포인트에서 504가 발생한다면 타임아웃 설정 문제가 아니라 성능 버그입니다.

// 실제로 느린 구간을 추적하세요
app.use(function(request, response, next) {
    const start = Date.now();
    
    response.on('finish', function() {
        const duration = Date.now() - start;
        
        if(duration > 10000) {
            logger.warn('느린 요청', {
                url: request.url,
                duration: duration
            });
        }
    });
    
    next();
});

우아한 성능 저하

업스트림이 느릴 때, "오류를 반환하기"와 "영원히 기다리기" 사이에 다른 선택지가 있습니다.

app.get('/api/data', async function(request, response) {
    try {
        const data = await Promise.race([
            fetchFreshData(),
            new Promise(function(_, reject) {
                setTimeout(function() {
                    reject(new Error('timeout'));
                }, 5000);
            })
        ]);
        
        response.json(data);
    }
    catch(error) {
        // 최신 데이터가 너무 느린가요? 캐시된 데이터를 반환하세요.
        const cached = cache.get('data');
        
        if(cached) {
            response.json({
                ...cached,
                stale: true
            });
        }
        else {
            response.status(504).json({
                error: '제 시간에 데이터를 가져올 수 없습니다'
            });
        }
    }
});

오래된 데이터가 보통 데이터 없음보다 낫습니다. 최신 데이터 요청이 왜 느린지 파악하는 동안 사용자는 무언가를 볼 수 있습니다.

클라이언트 재시도 전략

504 오류는 종종 일시적입니다. 업스트림이 순간적으로 과부하 상태였거나 네트워크 순간 장애가 있었을 수 있습니다. 재시도는 합리적인 선택입니다—백오프와 함께.

async function fetchWithRetry(url, maxRetries = 3) {
    for(let attempt = 0; attempt < maxRetries; attempt++) {
        const response = await fetch(url);
        
        if(response.status === 504 && attempt < maxRetries - 1) {
            // 재시도마다 더 오래 기다리세요
            await new Promise(function(resolve) {
                setTimeout(resolve, Math.pow(2, attempt) * 1000);
            });
            continue;
        }
        
        return response;
    }
}

지수 백오프: 1초, 그 다음 2초, 그 다음 4초. 시스템이 회복할 시간을 주세요.

서킷 브레이커

업스트림이 계속 타임아웃된다면, 요청을 멈추세요. 서킷 브레이커는 이미 힘겨워하는 시스템에 요청을 계속 쌓지 않도록 막아줍니다.

const CircuitBreaker = require('opossum');

const breaker = new CircuitBreaker(fetchFromBackend, {
    timeout: 30000,
    errorThresholdPercentage: 50,
    resetTimeout: 60000
});

breaker.fallback(function() {
    return { error: '서비스가 일시적으로 이용 불가합니다', cached: getCachedData() };
});

실패율이 50%를 넘으면 서킷이 열립니다. 요청은 타임아웃을 기다리는 대신 즉시 폴백을 받습니다. 60초 후에 업스트림이 회복되었는지 확인하기 위해 요청 하나를 시도합니다.

분산 시스템이 드러내는 것

504 오류는 분산 시스템의 본질을 드러냅니다: 분산 시스템은 인내심과 가정 위에서 작동합니다. 각 구성 요소는 다른 구성 요소가 "충분히 빠르게" 응답할 것이라고 가정합니다. 그 가정이 깨질 때—한 시스템의 "충분히 빠른"이 다른 시스템의 그것과 맞지 않을 때—실제로 아무것도 고장나지 않았음에도 오류가 발생합니다.

해결책은 단순히 설정 조정이 아닙니다. 긴 대기가 전파되지 않고, 오래된 데이터를 허용할 수 있으며, "아직 처리 중"이 유효한 응답인 시스템을 설계하는 것입니다. 분산 시스템에서 시간은 고갈될 수 있는 자원임을 받아들이는 것입니다.

504 게이트웨이 타임아웃에 관한 자주 묻는 질문

504는 서버가 충돌했다는 의미인가요?

아닙니다. 504는 구체적으로 업스트림 서버가 제때 응답하지 않았다는 것을 의미합니다—서버는 여전히 실행 중일 수 있고, 지금 이 순간도 요청을 처리하고 있을 수 있습니다. 게이트웨이가 기다리기를 멈춘 것입니다. 이것은 업스트림이 실제로 잘못된 응답을 반환하거나 실패했음을 나타내는 502와 다릅니다.

504를 방지하기 위해 모든 타임아웃을 늘려야 하나요?

타임아웃을 늘리면 문제를 감춥니다. 엔드포인트가 2초 안에 응답해야 하는데 가끔 90초가 걸린다면, 성능 버그가 있는 것입니다. 그것을 먼저 고치세요. 보고서 생성이나 배치 처리처럼 진짜로 시간이 필요한 작업에만 긴 타임아웃을 쓰세요.

같은 요청이 보통은 잘 되는데 왜 504가 간헐적으로 발생하나요?

간헐적인 504는 보통 자원 경합이 원인입니다: 데이터베이스 연결 풀 고갈, 가비지 컬렉션 중단, 네트워크 혼잡, 또는 부하 상황에서 업스트림 서비스가 가끔 느려지는 것. 시스템은 대부분 잘 버티지만, 가끔 그렇지 않습니다. 실패할 때 무엇이 다른지 살펴보세요—대개 타이밍이나 부하의 차이입니다.

504 이후 클라이언트가 재시도해도 안전한가요?

대체로 그렇습니다, 단 몇 가지를 주의해야 합니다. GET 요청(읽기)은 백오프와 함께 자유롭게 재시도하세요. POST/PUT/DELETE(쓰기)는 작업이 멱등적인 경우에만 재시도하세요—그렇지 않으면 중복 처리가 발생할 수 있습니다. 원래 요청이 완료되었는지 확실하지 않다면, 재시도하기 전에 먼저 확인하세요.

이 페이지가 도움이 되었나요?

😔
🤨
😃