그누보드7 대용량 게시판 성능 테스트
AI 시켜서 테스트 해봤습니다. 더미생성기로 게시물등록 후 테스트 해봤습니다. 초기 버전보다 낫지만 개선이 필요해 보입니다.
게시물이 30~40만건 쌓이고 깊은페이지 이동시 서버 부하 발생이 될 수 있습니다. 이건 검색 봇들이 잘 하는 행동입니다.
대용량이라고 했지만 20만,40만,120만인데 대용량은 아닙니다?
테스트 일자: 2026-07-15
## 요약
- 첫 페이지는 게시물 수가 20만~120만 건으로 늘어도 평상시 응답시간 차이가 크지 않았다.
- 10 VU 테스트에서 모든 게시판이 오류율 0%, p95 273~349ms를 기록했다.
- 반면 깊은 페이지는 게시물 수와 OFFSET이 커질수록 급격히 느려졌다.
- 120만 건 게시판은 1,000페이지에서 4.8~10.4초, 2,000페이지부터 30초 안에 응답하지 못했다.
- 결론적으로 첫 페이지 동시 처리보다 OFFSET 기반 깊은 페이지네이션이 현재의 핵심 병목이다.
## 테스트 환경
### 부하 발생기
| 항목 | 사양 |
|---|---|
| 장비 | Mac mini (Mac16,11) |
| CPU | Apple M4 Pro 12코어 |
| 메모리 | 48GB |
| OS | macOS 26.5.2 |
| 도구 | k6 v1.6.1, darwin/arm64 |
### 대상 서버
| 항목 | 사양 |
|---|---|
| 인스턴스 | AWS EC2 t3.small, burstable |
| CPU | Intel Xeon Platinum 8259CL 2 vCPU |
| 메모리 | 1.9GB |
| Swap | 1.9GB, 테스트 전 약 467MB 사용 |
| 디스크 | EBS 60GB, 루트 58GB 중 13GB 사용 |
| OS | Ubuntu 24.04.4 LTS |
| 웹 서버 | Nginx 1.24.0 |
| PHP | PHP-FPM 8.5.8, memory_limit 256MB, max_execution_time 120초 |
| PHP-FPM | dynamic, max_children 6 |
| 애플리케이션 | 그누보드7 7.0.3, Laravel 12.62.0 |
| DB | MySQL 8.4.10 |
| InnoDB buffer pool | 384MB |
| MySQL max_connections | 20 |
테스트 중 서버 CPU는 전체 구간 평균 약 30%, 순간 최대 93%였다. run queue는 최대 8이었으며, active swap in/out과 유의미한 I/O wait는 관찰되지 않았다.
## 테스트 데이터
| 게시판 | 원글 | 댓글 | 마지막 페이지 |
|---|---:|---:|---:|
| gallery | 약 20만 건 | 약 25만 건 | 10,000 |
| test40 | 약 40만 건 | 약 50만 건 | 20,000 |
| freebd | 약 120만 건 | 약 150만 건 | 60,000 |
댓글은 별도 테이블에 저장되고 목록 쿼리는 게시글의 `comments_count`를 읽으므로, 댓글 행 수가 목록 OFFSET 비용에 직접 합산되지는 않는다. 다만 전체 DB 크기와 상세·댓글 API 부하는 증가시킨다.
## K6 측정 방법
- 게시판 3개를 동시에 때리지 않고 게시판별로 독립 실행했다.
- 실행 순서: gallery 5 VU, test40 5 VU, freebd 5 VU, gallery 10 VU, test40 10 VU, freebd 10 VU.
- 각 테스트는 첫 페이지 API를 30초 동안 호출했다.
- VU는 응답을 받은 뒤 1초 대기하고 다음 요청을 보냈다.
- 매 응답에서 HTTP 200, 첫 페이지 여부, 게시글 배열 존재 여부를 검증했다.
- 브라우저의 JavaScript 렌더링 시간은 제외되며, 맥미니에서 서버까지의 네트워크와 API 처리시간은 포함된다.
## 첫 페이지 K6 결과
| 게시판 | VU | 평균 | 중앙값 | p95 | p99 | 최대 | 처리량 | 오류율 |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| gallery 20만 | 5 | 134ms | 128ms | 222ms | 291ms | 343ms | 4.37 req/s | 0% |
| gallery 20만 | 10 | 162ms | 141ms | 273ms | 524ms | 631ms | 8.45 req/s | 0% |
| test40 40만 | 5 | 127ms | 106ms | 230ms | 280ms | 347ms | 4.34 req/s | 0% |
| test40 40만 | 10 | 164ms | 133ms | 347ms | 515ms | 622ms | 8.44 req/s | 0% |
| freebd 120만 | 5 | 126ms | 105ms | 226ms | 287ms | 341ms | 4.36 req/s | 0% |
| freebd 120만 | 10 | 152ms | 129ms | 349ms | 622ms | 675ms | 8.52 req/s | 0% |
`p95 349ms`는 전체 요청의 95%가 349ms 이내에 완료됐다는 뜻이다. 10 VU에서 처리량은 약 2배로 증가했고 오류는 없었지만, p95와 최대 응답시간은 증가했다.
## 깊은 페이지 결과
| 게시판 | 첫 페이지 | 느려지는 구간 | 깊은 페이지 | 판정 |
|---|---:|---:|---:|---|
| 20만 건 | 약 0.12초 | 1,000페이지 약 1.55초 | 10,000페이지 약 2.0초 | 사용 가능하지만 깊은 페이지는 느림 |
| 40만 건 | 약 0.12초 | 2,000페이지 약 2.78초 | 20,000페이지 약 3.72초 | 깊은 페이지 사용성이 낮음 |
| 120만 건 | 약 0.12초, cold outlier 2.52초 | 1,000페이지 4.8~10.4초 | 2,000페이지부터 30초 초과 | 사실상 사용 곤란 |
## 원인
목록은 `simplePaginate()`를 사용해 매 요청의 전체 COUNT는 피하지만, 페이지 이동은 여전히 OFFSET 방식이다.
- `modules/_bundled/sirsoft-board/src/Repositories/PostRepository.php`
- `modules/_bundled/sirsoft-board/src/Services/PostService.php`
- `modules/_bundled/sirsoft-board/src/Http/Resources/PostCollection.php`
OFFSET이 커지면 DB는 앞의 행을 읽고 버린 뒤 필요한 20건을 반환한다. 첫 페이지는 LIMIT 20과 캐시된 전체 건수 덕분에 빠르지만, 마지막 페이지로 갈수록 수십만~백만 행을 스캔·정렬하게 된다.
공개 게시판 API에는 `throttle:600,1`이 적용돼 있다. 대기 시간 없는 단일 IP K6 테스트는 분당 600건 이후 429를 반환하므로, 이번 VU 비교에는 사용자당 1초 대기를 적용했다.
## 결론 및 개선 방향
5 VU는 PHP-FPM worker 6개 범위의 기준선이고, 10 VU는 소형 서버에서 동시 사용자 증가 영향을 확인하기에 적절하다. 따라서 하나만 고르기보다 5 VU를 기준선, 10 VU를 1차 부하 기준으로 함께 유지하는 것이 맞다.
현재 서버에서도 첫 페이지는 10 VU를 오류 없이 처리했다. 서버 증설은 처리량과 캐시 여유를 늘릴 수 있지만, OFFSET 증가 비용 자체는 해결하지 못한다.
우선순위는 다음과 같다.
1. 일반 구간은 기존 페이지 번호를 유지하고, 일정 페이지 이후 커서 방식으로 전환한다.
2. 깊은 페이지가 꼭 필요하면 ID만 먼저 찾고 실제 20개 행을 다시 조회하는 deferred join을 적용한다.
3. 게시판·삭제 상태·정렬 컬럼 순서에 맞는 복합 인덱스를 검토한다.
4. 5/10 VU 첫 페이지와 깊은 페이지 쿼리를 별도 성능 회귀 테스트로 운영한다.
게시물이 30~40만건 쌓이고 깊은페이지 이동시 서버 부하 발생이 될 수 있습니다. 이건 검색 봇들이 잘 하는 행동입니다.
대용량이라고 했지만 20만,40만,120만인데 대용량은 아닙니다?
테스트 일자: 2026-07-15
## 요약
- 첫 페이지는 게시물 수가 20만~120만 건으로 늘어도 평상시 응답시간 차이가 크지 않았다.
- 10 VU 테스트에서 모든 게시판이 오류율 0%, p95 273~349ms를 기록했다.
- 반면 깊은 페이지는 게시물 수와 OFFSET이 커질수록 급격히 느려졌다.
- 120만 건 게시판은 1,000페이지에서 4.8~10.4초, 2,000페이지부터 30초 안에 응답하지 못했다.
- 결론적으로 첫 페이지 동시 처리보다 OFFSET 기반 깊은 페이지네이션이 현재의 핵심 병목이다.
## 테스트 환경
### 부하 발생기
| 항목 | 사양 |
|---|---|
| 장비 | Mac mini (Mac16,11) |
| CPU | Apple M4 Pro 12코어 |
| 메모리 | 48GB |
| OS | macOS 26.5.2 |
| 도구 | k6 v1.6.1, darwin/arm64 |
### 대상 서버
| 항목 | 사양 |
|---|---|
| 인스턴스 | AWS EC2 t3.small, burstable |
| CPU | Intel Xeon Platinum 8259CL 2 vCPU |
| 메모리 | 1.9GB |
| Swap | 1.9GB, 테스트 전 약 467MB 사용 |
| 디스크 | EBS 60GB, 루트 58GB 중 13GB 사용 |
| OS | Ubuntu 24.04.4 LTS |
| 웹 서버 | Nginx 1.24.0 |
| PHP | PHP-FPM 8.5.8, memory_limit 256MB, max_execution_time 120초 |
| PHP-FPM | dynamic, max_children 6 |
| 애플리케이션 | 그누보드7 7.0.3, Laravel 12.62.0 |
| DB | MySQL 8.4.10 |
| InnoDB buffer pool | 384MB |
| MySQL max_connections | 20 |
테스트 중 서버 CPU는 전체 구간 평균 약 30%, 순간 최대 93%였다. run queue는 최대 8이었으며, active swap in/out과 유의미한 I/O wait는 관찰되지 않았다.
## 테스트 데이터
| 게시판 | 원글 | 댓글 | 마지막 페이지 |
|---|---:|---:|---:|
| gallery | 약 20만 건 | 약 25만 건 | 10,000 |
| test40 | 약 40만 건 | 약 50만 건 | 20,000 |
| freebd | 약 120만 건 | 약 150만 건 | 60,000 |
댓글은 별도 테이블에 저장되고 목록 쿼리는 게시글의 `comments_count`를 읽으므로, 댓글 행 수가 목록 OFFSET 비용에 직접 합산되지는 않는다. 다만 전체 DB 크기와 상세·댓글 API 부하는 증가시킨다.
## K6 측정 방법
- 게시판 3개를 동시에 때리지 않고 게시판별로 독립 실행했다.
- 실행 순서: gallery 5 VU, test40 5 VU, freebd 5 VU, gallery 10 VU, test40 10 VU, freebd 10 VU.
- 각 테스트는 첫 페이지 API를 30초 동안 호출했다.
- VU는 응답을 받은 뒤 1초 대기하고 다음 요청을 보냈다.
- 매 응답에서 HTTP 200, 첫 페이지 여부, 게시글 배열 존재 여부를 검증했다.
- 브라우저의 JavaScript 렌더링 시간은 제외되며, 맥미니에서 서버까지의 네트워크와 API 처리시간은 포함된다.
## 첫 페이지 K6 결과
| 게시판 | VU | 평균 | 중앙값 | p95 | p99 | 최대 | 처리량 | 오류율 |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| gallery 20만 | 5 | 134ms | 128ms | 222ms | 291ms | 343ms | 4.37 req/s | 0% |
| gallery 20만 | 10 | 162ms | 141ms | 273ms | 524ms | 631ms | 8.45 req/s | 0% |
| test40 40만 | 5 | 127ms | 106ms | 230ms | 280ms | 347ms | 4.34 req/s | 0% |
| test40 40만 | 10 | 164ms | 133ms | 347ms | 515ms | 622ms | 8.44 req/s | 0% |
| freebd 120만 | 5 | 126ms | 105ms | 226ms | 287ms | 341ms | 4.36 req/s | 0% |
| freebd 120만 | 10 | 152ms | 129ms | 349ms | 622ms | 675ms | 8.52 req/s | 0% |
`p95 349ms`는 전체 요청의 95%가 349ms 이내에 완료됐다는 뜻이다. 10 VU에서 처리량은 약 2배로 증가했고 오류는 없었지만, p95와 최대 응답시간은 증가했다.
## 깊은 페이지 결과
| 게시판 | 첫 페이지 | 느려지는 구간 | 깊은 페이지 | 판정 |
|---|---:|---:|---:|---|
| 20만 건 | 약 0.12초 | 1,000페이지 약 1.55초 | 10,000페이지 약 2.0초 | 사용 가능하지만 깊은 페이지는 느림 |
| 40만 건 | 약 0.12초 | 2,000페이지 약 2.78초 | 20,000페이지 약 3.72초 | 깊은 페이지 사용성이 낮음 |
| 120만 건 | 약 0.12초, cold outlier 2.52초 | 1,000페이지 4.8~10.4초 | 2,000페이지부터 30초 초과 | 사실상 사용 곤란 |
## 원인
목록은 `simplePaginate()`를 사용해 매 요청의 전체 COUNT는 피하지만, 페이지 이동은 여전히 OFFSET 방식이다.
- `modules/_bundled/sirsoft-board/src/Repositories/PostRepository.php`
- `modules/_bundled/sirsoft-board/src/Services/PostService.php`
- `modules/_bundled/sirsoft-board/src/Http/Resources/PostCollection.php`
OFFSET이 커지면 DB는 앞의 행을 읽고 버린 뒤 필요한 20건을 반환한다. 첫 페이지는 LIMIT 20과 캐시된 전체 건수 덕분에 빠르지만, 마지막 페이지로 갈수록 수십만~백만 행을 스캔·정렬하게 된다.
공개 게시판 API에는 `throttle:600,1`이 적용돼 있다. 대기 시간 없는 단일 IP K6 테스트는 분당 600건 이후 429를 반환하므로, 이번 VU 비교에는 사용자당 1초 대기를 적용했다.
## 결론 및 개선 방향
5 VU는 PHP-FPM worker 6개 범위의 기준선이고, 10 VU는 소형 서버에서 동시 사용자 증가 영향을 확인하기에 적절하다. 따라서 하나만 고르기보다 5 VU를 기준선, 10 VU를 1차 부하 기준으로 함께 유지하는 것이 맞다.
현재 서버에서도 첫 페이지는 10 VU를 오류 없이 처리했다. 서버 증설은 처리량과 캐시 여유를 늘릴 수 있지만, OFFSET 증가 비용 자체는 해결하지 못한다.
우선순위는 다음과 같다.
1. 일반 구간은 기존 페이지 번호를 유지하고, 일정 페이지 이후 커서 방식으로 전환한다.
2. 깊은 페이지가 꼭 필요하면 ID만 먼저 찾고 실제 20개 행을 다시 조회하는 deferred join을 적용한다.
3. 게시판·삭제 상태·정렬 컬럼 순서에 맞는 복합 인덱스를 검토한다.
4. 5/10 VU 첫 페이지와 깊은 페이지 쿼리를 별도 성능 회귀 테스트로 운영한다.
총 1명이 반응했습니다
|
댓글을 작성하시려면 로그인이 필요합니다.
댓글 4개
제보해주신 내용 포함하여 게시판, 커머스 모듈 등 성능이 중요한 부분은 꾸준하게 개선하도록 하겠습니다.
다만 이 현상이 G7만의 문제인지, 일반적인 OFFSET 기반 페이지네이션의 한계인지는 구분해서 볼 필요도 있을 거같습니다.
G7 너무 무겁습니다.