오늘 작업하다 사이트가 사이트맵잡 큐 때문에 뻗었습니다. 테스트서버인데 리부팅해도 계속 반복되길래 사이트맵 큐가 문제였네요. 테스트한다고 게시물 140만건, 쇼핑몰 2만건 넣어뒀는데 오늘 이걸 큐로 디비전체를 읽어와서 처리한다고 뻗었습니다. 이게 리부팅해도 자동 실행되서 계속 뻗었습니다.
게시물아니 상품이 작으면 상관없겠지만 어떤 사이트라도 장기 운영하게 되면 문제가 발생 합니다. AI 분석 첨부합니다.
이슈 요약
결론: 현재 사이트맵 구현은 게시물이 정상적으로 계속 누적되는 운영 환경에서 안전하게 확장되지 않습니다. 초기에는 정상으로 보여도 전체 조회량과 메모리 사용량이 누적 데이터에 비례해 증가하며, 사이트맵 표준 위반과 서비스 부하로 이어질 수 있습니다.
- 심각도: Critical
- 영향 범위: 게시물 또는 상품이 지속해서 누적되는 모든 설치 환경
- 확인 버전: GnuBoard 7.0.4
핵심 위험요소:
- 콘텐츠 변경마다 전체 사이트맵 캐시 삭제: 게시물·상품·페이지를 생성·수정·삭제하면 마지막 사이트맵 캐시가 즉시 삭제됩니다.
- 검색봇 요청에서 동기 전량 생성: 캐시가 없는 상태에서
/sitemap.xml을 요청하면 PHP-FPM이 전체 URL 조회와 XML 생성을 웹 요청 안에서 수행합니다.
- 전체 데이터를 메모리에 중복 적재: Eloquent 결과, 기여자별 URL 배열, 통합 배열, 최종 XML 문자열이 동시에 메모리를 사용합니다.
- 사이트맵 분할 미지원: 사이트맵 인덱스 없이 하나의 XML만 생성하므로 50,000 URL 또는 비압축 50MB 한도를 넘습니다.
- 단일 대형 캐시 값 저장: 완성된 XML 전체를 캐시 한 건으로 저장하며 Redis 사용 시 Redis 메모리까지 추가로 점유합니다.
- 큐 중복 실행 가능성: 잡 타임아웃 300초와 Redis 기본
retry_after 90초가 맞지 않고 잡 유일성 잠금도 없습니다.
- 관리자 요청도 동기 실행: 관리자 "지금 생성" API가 큐를 거치지 않고 HTTP 요청에서 전체 생성을 수행합니다.
- 확장성 경계 테스트 부재: 기본 기능 테스트는 있지만 분할 한도, 스트리밍 처리와 최대 메모리 사용량을 검증하지 않습니다.
가장 위험한 실행 흐름은 콘텐츠 변경 → 사이트맵 캐시 삭제 → 검색봇 접근 → PHP-FPM에서 전체 사이트맵 동기 생성입니다. 데이터가 누적될수록 이 흐름의 DB·메모리·응답시간 비용이 계속 증가합니다.
문제 정의
1. 전체 레코드와 URL을 PHP 메모리에 적재
게시판 기여자는 게시판별 공개 게시물을 get()으로 전부 조회하고 URL 배열을 만듭니다.
modules/_bundled/sirsoft-board/src/Seo/BoardSitemapContributor.php:58-88
modules/_bundled/sirsoft-ecommerce/src/Seo/EcommerceSitemapContributor.php:50-75
modules/_bundled/sirsoft-page/src/Seo/PageSitemapContributor.php:32-46
생성기는 각 기여자의 전체 배열을 array_merge()로 다시 합친 뒤 하나의 XML 문자열을 생성합니다.
app/Seo/SitemapGenerator.php:48-78
app/Seo/SitemapGenerator.php:171-247
이 과정에서 Eloquent 모델, 기여자별 URL 배열, 통합 URL 배열, 최종 XML 문자열이 동시에 존재합니다. 다국어 설정이면 URL 항목도 로케일 수만큼 증가합니다.
2. 사이트맵 표준의 파일 한도 미준수
단일 사이트맵은 최대 50,000 URL, 비압축 기준 50MB 이하여야 합니다. 현재 구현에는 사이트맵 인덱스와 파일 분할이 없습니다.
- 최소 파일 수는
ceil(전체 URL 항목 수 / 50,000)으로 계산
- 다국어 설정은 로케일별 URL 항목 수를 반영
- XML 크기가 50MB를 먼저 넘으면 URL 수와 관계없이 추가 분할
참고:
3. 대형 XML을 캐시 드라이버에 단일 값으로 저장
SitemapManager는 생성된 전체 XML을 seo.sitemap 키 하나로 캐시에 저장합니다.
app/Seo/SitemapManager.php:31-60
캐시 드라이버가 Redis이면 PHP에서 생성한 대형 문자열이 다시 Redis 메모리를 점유합니다. 사이트맵 원문은 캐시 값이 아니라 정적 파일 또는 오브젝트 스토리지 산출물로 관리해야 합니다.
4. 웹과 관리자 HTTP 요청에서 동기 생성 가능
사이트맵 캐시가 없으면 /sitemap.xml 요청을 처리하는 PHP-FPM 프로세스가 전체 사이트맵을 즉시 생성합니다.
app/Http/Controllers/Api/Public/SitemapController.php:26-43
검색봇의 첫 요청만으로 PHP-FPM과 DB가 장시간 점유될 수 있으므로 웹 요청에서 사이트맵을 생성해서는 안 됩니다.
관리자의 사이트맵 즉시 재생성 API도 큐를 거치지 않고 요청 안에서 SitemapManager::regenerate()를 동기 실행합니다.
app/Http/Controllers/Api/Admin/SeoCacheController.php:99-121
5. 콘텐츠 변경마다 전체 사이트맵 캐시 삭제
게시물, 상품, 페이지의 생성·수정·삭제 리스너가 사이트맵 산출물 전체를 나타내는 seo.sitemap 캐시 키를 삭제합니다.
modules/_bundled/sirsoft-board/src/Listeners/SeoBoardCacheListener.php:25-44
modules/_bundled/sirsoft-board/src/Listeners/SeoBoardCacheListener.php:153-178
modules/_bundled/sirsoft-ecommerce/src/Listeners/SeoProductCacheListener.php:25-40
modules/_bundled/sirsoft-ecommerce/src/Listeners/SeoProductCacheListener.php:92-117
modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php:23-43
modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php:63-85
캐시가 삭제된 뒤 검색봇이 /sitemap.xml을 요청하면 PHP-FPM에서 전체 사이트맵을 다시 생성합니다. 콘텐츠 변경과 검색봇 접근이 반복되면 정기 스케줄과 무관하게 전체 재생성이 여러 번 실행될 수 있습니다. 마지막 정상 사이트맵은 새 산출물이 준비될 때까지 유지하고, 콘텐츠 변경은 증분 재생성 예약만 해야 합니다.
6. 큐 중복 실행과 반복 재개 방지 부족
스케줄은 onOneServer()만 사용하며 실행 중인 잡 자체의 유일성이나 중복 실행을 보장하지 않습니다.
routes/console.php:38-52
app/Jobs/GenerateSitemapJob.php:19-55
잡 타임아웃은 300초지만 Redis 큐의 기본 retry_after는 90초입니다.
app/Jobs/GenerateSitemapJob.php:26-31
config/queue.php:68-70
처리 시간이 길어지면 동일 잡이 재예약될 수 있고, 서버 재기동 후 예약된 잡이 다시 실행되어 장애가 반복될 수 있습니다.
7. 확장성 경계 테스트 부재
생성기, 잡, HTTP 응답의 기본 단위·기능 테스트는 존재합니다. 그러나 현재 테스트는 소량 URL의 XML 형식, 잡 위임, 캐시 동작을 검증하며 사이트맵 인덱스, 50,000 URL·50MB 분할, 스트리밍 처리, 최대 메모리 사용량은 검증하지 않습니다.
tests/Unit/Seo/SitemapGeneratorTest.php:117-149
tests/Unit/Jobs/GenerateSitemapJobTest.php:36-74
tests/Feature/Seo/SitemapTest.php:79-113
특히 기능 테스트는 캐시가 없을 때 HTTP 요청에서 생성기를 호출하고 결과를 캐시하는 현재 동작을 정상 동작으로 고정하고 있습니다. 누적 데이터 회귀를 막으려면 이 테스트를 마지막 정상 산출물 제공과 비동기 갱신 동작으로 변경해야 합니다.
게시물 누적에 따른 위험
사이트맵은 기본적으로 매일 실행되며, 증분 처리 없이 실행 시점의 전체 URL을 다시 조회하고 생성합니다.
- 게시물이 하루 1,000건씩 쌓이면 단일 언어 기준 약 50일 만에 게시물 URL만으로 단일 사이트맵의 50,000 URL 한도를 넘습니다.
- 다국어 설정에서는 로케일 수만큼 URL 항목이 증가하므로 표준 한도에 더 빨리 도달합니다.
- 매일 처리해야 하는 DB 행 수와 PHP 배열 크기는 전체 누적 건수에 비례해 증가합니다.
- 일일 전체 재생성을 장기간 합산하면 처리 비용은 누적 기간에 대해 사실상 제곱 비율로 증가합니다.
- 게시물 변경마다 캐시가 삭제되므로 검색봇 접근 빈도에 따라 하루에도 여러 번 전체 재생성이 실행될 수 있습니다.
- 실행 시간이 길어질수록 큐 타임아웃, 중복 예약, PHP 메모리 부족, Redis 메모리 압박과 일반 요청 지연 가능성이 함께 커집니다.
따라서 초기에는 정상으로 보여도 게시물이 꾸준히 누적되면 사이트맵 표준 위반이 먼저 발생하고, 이후 생성 작업이 웹과 큐의 안정성까지 위협할 수 있습니다.
권장 해결안
P0. 즉시 반영할 안전장치
/sitemap.xml 캐시 미스 시 동기 생성을 제거합니다. 마지막 정상 산출물을 반환하고 없으면 503 Retry-After를 반환합니다.
- 콘텐츠 변경 시 마지막 사이트맵을 삭제하지 않고 변경 상태만 기록한 뒤 중복 제거된 비동기 재생성을 예약합니다.
- 관리자 즉시 재생성 API도 잡을 디스패치하고
202 Accepted를 반환하도록 변경합니다.
- 사이트맵 잡을
sitemap 전용 큐로 분리하고 워커를 1개로 제한합니다.
- 잡에 유일성 잠금과 실행 중복 방지를 적용합니다. 스케줄러 중복 방지와 큐 잡 유일성을 모두 보장해야 합니다.
retry_after를 잡 타임아웃보다 충분히 크게 설정합니다.
- 생성 시작 전에 예상 URL 수, 로케일 배수, 예상 파일 수를 계산하고 비정상 규모면 관리자에게 경고합니다.
- 실패 또는 타임아웃 시 이전 정상 사이트맵을 유지합니다.
P1. 근본 구조 변경
getUrls(): array 기반 수집을 iterUrls(): iterable 기반 스트리밍 계약으로 전환합니다.
- 기존 확장 호환성을 위해 새 스트리밍 인터페이스를 추가하고 기존 인터페이스는 어댑터로 지원합니다.
- 게시물과 상품은
lazyById() 또는 chunkById()로 읽고 Eloquent 전체 모델 컬렉션 생성을 피합니다.
- URL 50,000개 또는 XML 50MB 이전에 파일을 닫고 다음 파일을 생성합니다.
- 최상위
/sitemap.xml은 sitemapindex만 반환하고 분할 파일을 참조하게 합니다.
- 임시 파일에 스트리밍 기록한 뒤 생성 완료 시 원자적으로 교체합니다.
- 대형 XML 본문은 Redis에 저장하지 않고 정적 파일 또는 오브젝트 스토리지에 저장합니다. Redis에는 버전, 생성 시각, 파일 목록 등 작은 메타데이터만 저장합니다.
권장 분할 기준:
- 콘텐츠 유형: 정적 페이지, 게시판, 게시물, 상품, 카테고리
- 게시물: 게시판 ID와 안정적인 게시물 ID 범위 또는 날짜 단위
- 파일당 상한: 50,000 URL 및 비압축 50MB 중 먼저 도달하는 값
- 파일명 예시:
sitemap-posts-{board_id}-{range}.xml.gz
P2. 증분 생성과 운영 보호
- 신규·수정·삭제된 URL이 속한 분할 파일만 다시 생성합니다.
- RSS/Atom에는 최근 게시물과 갱신 항목만 제공하고 사이트맵 인덱스와 병행합니다.
- 잡을 낮은 우선순위로 실행하고 CPU 및 메모리 제한을 둡니다.
- 처리 체크포인트와 진행률을 저장해 실패 시 전체를 처음부터 재생성하지 않게 합니다.
- 생성 시간, 조회 행 수, 파일 수, 최대 RSS, DB 시간, 실패 횟수를 관측 지표로 기록합니다.
사이트맵 정책 권장안
게시물 전체를 검색엔진에 알리는 것 자체는 잘못이 아닙니다. 다만 하나의 XML과 하나의 메모리 작업으로 처리하면 안 됩니다.
- 주요 메뉴와 정적 페이지: 별도 소형 사이트맵
- 전체 공개 게시물: 사이트맵 인덱스 아래 분할된 정적 사이트맵
- 최근 변경 게시물: RSS/Atom으로 보조 제공
- 검색 노출이 불필요한 게시판 또는 게시물: 관리자 정책으로 제외
대규모 커뮤니티에서는 전체 공개 URL의 분할 사이트맵 + 최근 변경 RSS 조합을 기본값으로 권장합니다.
DB 조회 권장안
스트리밍 조회와 함께 실제 실행 계획을 확인해 다음 형태의 인덱스를 검토합니다.
- 게시물:
(board_id, status, is_secret, id)
- 상품:
(display_status, id)
운영 대형 테이블에 인덱스를 추가할 때는 온라인 DDL 가능 여부, 쓰기 부하, 디스크 여유를 먼저 확인해야 합니다.
완료 기준
- 데이터 건수와 로케일 수가 증가해도 전체 URL을 배열에 적재하지 않고 스트리밍으로 생성됩니다.
- 각 사이트맵 파일이 50,000 URL 및 비압축 50MB 제한을 지킵니다.
- 생성 프로세스의 최대 RSS가 데이터 건수에 비례해 증가하지 않고 설정된 메모리 예산 안에서 유지됩니다.
- 생성 중 일반 웹 요청에서 5xx가 발생하지 않습니다.
- 콘텐츠 변경 시 마지막 정상 사이트맵이 삭제되지 않고 비동기 갱신만 예약됩니다.
- 동일 잡을 여러 번 디스패치해도 실제 생성은 한 번만 실행됩니다.
- 잡 실패 및 서버 재기동 후에도 마지막 정상 사이트맵이 계속 제공됩니다.
/sitemap.xml 요청과 관리자 재생성 요청은 DB 전체 조회나 동기 생성을 수행하지 않습니다.
- 사이트맵 인덱스와 모든 분할 파일이 표준 검증을 통과합니다.
오늘 작업하다 사이트가 사이트맵잡 큐 때문에 뻗었습니다. 테스트서버인데 리부팅해도 계속 반복되길래 사이트맵 큐가 문제였네요. 테스트한다고 게시물 140만건, 쇼핑몰 2만건 넣어뒀는데 오늘 이걸 큐로 디비전체를 읽어와서 처리한다고 뻗었습니다. 이게 리부팅해도 자동 실행되서 계속 뻗었습니다.
게시물아니 상품이 작으면 상관없겠지만 어떤 사이트라도 장기 운영하게 되면 문제가 발생 합니다. AI 분석 첨부합니다.
이슈 요약
결론: 현재 사이트맵 구현은 게시물이 정상적으로 계속 누적되는 운영 환경에서 안전하게 확장되지 않습니다. 초기에는 정상으로 보여도 전체 조회량과 메모리 사용량이 누적 데이터에 비례해 증가하며, 사이트맵 표준 위반과 서비스 부하로 이어질 수 있습니다.
핵심 위험요소:
/sitemap.xml을 요청하면 PHP-FPM이 전체 URL 조회와 XML 생성을 웹 요청 안에서 수행합니다.retry_after90초가 맞지 않고 잡 유일성 잠금도 없습니다.가장 위험한 실행 흐름은
콘텐츠 변경 → 사이트맵 캐시 삭제 → 검색봇 접근 → PHP-FPM에서 전체 사이트맵 동기 생성입니다. 데이터가 누적될수록 이 흐름의 DB·메모리·응답시간 비용이 계속 증가합니다.문제 정의
1. 전체 레코드와 URL을 PHP 메모리에 적재
게시판 기여자는 게시판별 공개 게시물을
get()으로 전부 조회하고 URL 배열을 만듭니다.modules/_bundled/sirsoft-board/src/Seo/BoardSitemapContributor.php:58-88modules/_bundled/sirsoft-ecommerce/src/Seo/EcommerceSitemapContributor.php:50-75modules/_bundled/sirsoft-page/src/Seo/PageSitemapContributor.php:32-46생성기는 각 기여자의 전체 배열을
array_merge()로 다시 합친 뒤 하나의 XML 문자열을 생성합니다.app/Seo/SitemapGenerator.php:48-78app/Seo/SitemapGenerator.php:171-247이 과정에서 Eloquent 모델, 기여자별 URL 배열, 통합 URL 배열, 최종 XML 문자열이 동시에 존재합니다. 다국어 설정이면 URL 항목도 로케일 수만큼 증가합니다.
2. 사이트맵 표준의 파일 한도 미준수
단일 사이트맵은 최대 50,000 URL, 비압축 기준 50MB 이하여야 합니다. 현재 구현에는 사이트맵 인덱스와 파일 분할이 없습니다.
ceil(전체 URL 항목 수 / 50,000)으로 계산참고:
3. 대형 XML을 캐시 드라이버에 단일 값으로 저장
SitemapManager는 생성된 전체 XML을seo.sitemap키 하나로 캐시에 저장합니다.app/Seo/SitemapManager.php:31-60캐시 드라이버가 Redis이면 PHP에서 생성한 대형 문자열이 다시 Redis 메모리를 점유합니다. 사이트맵 원문은 캐시 값이 아니라 정적 파일 또는 오브젝트 스토리지 산출물로 관리해야 합니다.
4. 웹과 관리자 HTTP 요청에서 동기 생성 가능
사이트맵 캐시가 없으면
/sitemap.xml요청을 처리하는 PHP-FPM 프로세스가 전체 사이트맵을 즉시 생성합니다.app/Http/Controllers/Api/Public/SitemapController.php:26-43검색봇의 첫 요청만으로 PHP-FPM과 DB가 장시간 점유될 수 있으므로 웹 요청에서 사이트맵을 생성해서는 안 됩니다.
관리자의 사이트맵 즉시 재생성 API도 큐를 거치지 않고 요청 안에서
SitemapManager::regenerate()를 동기 실행합니다.app/Http/Controllers/Api/Admin/SeoCacheController.php:99-1215. 콘텐츠 변경마다 전체 사이트맵 캐시 삭제
게시물, 상품, 페이지의 생성·수정·삭제 리스너가 사이트맵 산출물 전체를 나타내는
seo.sitemap캐시 키를 삭제합니다.modules/_bundled/sirsoft-board/src/Listeners/SeoBoardCacheListener.php:25-44modules/_bundled/sirsoft-board/src/Listeners/SeoBoardCacheListener.php:153-178modules/_bundled/sirsoft-ecommerce/src/Listeners/SeoProductCacheListener.php:25-40modules/_bundled/sirsoft-ecommerce/src/Listeners/SeoProductCacheListener.php:92-117modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php:23-43modules/_bundled/sirsoft-page/src/Listeners/SeoPageCacheListener.php:63-85캐시가 삭제된 뒤 검색봇이
/sitemap.xml을 요청하면 PHP-FPM에서 전체 사이트맵을 다시 생성합니다. 콘텐츠 변경과 검색봇 접근이 반복되면 정기 스케줄과 무관하게 전체 재생성이 여러 번 실행될 수 있습니다. 마지막 정상 사이트맵은 새 산출물이 준비될 때까지 유지하고, 콘텐츠 변경은 증분 재생성 예약만 해야 합니다.6. 큐 중복 실행과 반복 재개 방지 부족
스케줄은
onOneServer()만 사용하며 실행 중인 잡 자체의 유일성이나 중복 실행을 보장하지 않습니다.routes/console.php:38-52app/Jobs/GenerateSitemapJob.php:19-55잡 타임아웃은 300초지만 Redis 큐의 기본
retry_after는 90초입니다.app/Jobs/GenerateSitemapJob.php:26-31config/queue.php:68-70처리 시간이 길어지면 동일 잡이 재예약될 수 있고, 서버 재기동 후 예약된 잡이 다시 실행되어 장애가 반복될 수 있습니다.
7. 확장성 경계 테스트 부재
생성기, 잡, HTTP 응답의 기본 단위·기능 테스트는 존재합니다. 그러나 현재 테스트는 소량 URL의 XML 형식, 잡 위임, 캐시 동작을 검증하며 사이트맵 인덱스, 50,000 URL·50MB 분할, 스트리밍 처리, 최대 메모리 사용량은 검증하지 않습니다.
tests/Unit/Seo/SitemapGeneratorTest.php:117-149tests/Unit/Jobs/GenerateSitemapJobTest.php:36-74tests/Feature/Seo/SitemapTest.php:79-113특히 기능 테스트는 캐시가 없을 때 HTTP 요청에서 생성기를 호출하고 결과를 캐시하는 현재 동작을 정상 동작으로 고정하고 있습니다. 누적 데이터 회귀를 막으려면 이 테스트를 마지막 정상 산출물 제공과 비동기 갱신 동작으로 변경해야 합니다.
게시물 누적에 따른 위험
사이트맵은 기본적으로 매일 실행되며, 증분 처리 없이 실행 시점의 전체 URL을 다시 조회하고 생성합니다.
따라서 초기에는 정상으로 보여도 게시물이 꾸준히 누적되면 사이트맵 표준 위반이 먼저 발생하고, 이후 생성 작업이 웹과 큐의 안정성까지 위협할 수 있습니다.
권장 해결안
P0. 즉시 반영할 안전장치
/sitemap.xml캐시 미스 시 동기 생성을 제거합니다. 마지막 정상 산출물을 반환하고 없으면503 Retry-After를 반환합니다.202 Accepted를 반환하도록 변경합니다.sitemap전용 큐로 분리하고 워커를 1개로 제한합니다.retry_after를 잡 타임아웃보다 충분히 크게 설정합니다.P1. 근본 구조 변경
getUrls(): array기반 수집을iterUrls(): iterable기반 스트리밍 계약으로 전환합니다.lazyById()또는chunkById()로 읽고 Eloquent 전체 모델 컬렉션 생성을 피합니다./sitemap.xml은sitemapindex만 반환하고 분할 파일을 참조하게 합니다.권장 분할 기준:
sitemap-posts-{board_id}-{range}.xml.gzP2. 증분 생성과 운영 보호
사이트맵 정책 권장안
게시물 전체를 검색엔진에 알리는 것 자체는 잘못이 아닙니다. 다만 하나의 XML과 하나의 메모리 작업으로 처리하면 안 됩니다.
대규모 커뮤니티에서는
전체 공개 URL의 분할 사이트맵 + 최근 변경 RSS조합을 기본값으로 권장합니다.DB 조회 권장안
스트리밍 조회와 함께 실제 실행 계획을 확인해 다음 형태의 인덱스를 검토합니다.
(board_id, status, is_secret, id)(display_status, id)운영 대형 테이블에 인덱스를 추가할 때는 온라인 DDL 가능 여부, 쓰기 부하, 디스크 여유를 먼저 확인해야 합니다.
완료 기준
/sitemap.xml요청과 관리자 재생성 요청은 DB 전체 조회나 동기 생성을 수행하지 않습니다.