LLM Wiki가 커질 때 어떻게 운영할까: 3만 문서·100만 링크에서 배운 확장 전략
LLM Wiki는 처음에는 Markdown을 모으고 검색하는 작은 도구로 시작할 수 있다. 나도 처음에는 구현을 단순하게 만드는 데 집중했다. 갱신할 때 모든 Markdown을 읽고, 파싱 결과를 JavaScript 객체에 모으고, 메모리 안에서 SQLite를 완성한 뒤 파일로 내보냈다. 자료가 적을 때는 이 방식이 빠르게 만들기 좋았고 실제로 잘 동작했다.
내가 놓친 것은 자료가 계속 쌓일 때도 같은 전체 재생성 구조를 그대로 사용한 것이었다. 문서가 몇천 개일 때의 편리한 구현이 몇만 개에서도 괜찮을 것이라고 생각했고, 어느 시점에 증분 처리로 전환할지 기준을 정하지 않았다. 검색 본문과 파싱 결과, SQLite 생성 버퍼가 동시에 메모리에 존재하는 것도 작은 데이터에서는 눈에 띄지 않았다.
검색 대상이 6,495개에서 30,882개로 늘고 문서 사이 링크가 100만 개를 넘자 이 선택의 비용이 드러났다. 전체 인덱싱은 1.1 GiB를 넘어서도 메모리가 계속 증가했고 결국 완료되지 못했다.
1GB가 서버에서는 아주 큰 숫자로 보이지 않을 수 있다. 하지만 이 시스템은 전용 인덱싱 서버가 아니라 내가 작업하는 개인 로컬 PC에서 돌아간다. 같은 메모리를 IDE, 브라우저, MCP 서버, Python 작업과 다른 개발 도구가 함께 사용한다. Wiki 인덱스 하나를 갱신하는 동안 1GB 이상이 추가로 필요하고, 자료가 늘수록 더 커진다면 매일 실행하기 부담스럽다. 백그라운드 도구는 작업을 방해하지 않고 조용히 갱신돼야 하는데 당시 구조는 그 조건을 만족하지 못했다.
그래서 질문을 바꿨다. “메모리를 조금 줄이는 방법”보다 문서가 계속 늘어도 메모리가 전체 자료량과 함께 증가하지 않게 만들 수 있는가를 먼저 봤다. 답은 모든 것을 한 번에 만드는 구조에서 벗어나, 변경 파일만 읽고 일정한 배치로 디스크 SQLite에 쓰는 것이었다.
구조를 SQLite 배치 처리와 변경 파일 중심의 증분 갱신으로 바꾼 뒤 전체 전환은 358.6 MiB로 완료됐다. 검색 캐시 갱신 메모리도 594.7 MiB에서 159.8 MiB로 줄었다. 이 경험을 바탕으로 LLM Wiki가 커질 때 어떤 실수를 피하고, 어느 구조를 바꾸며, 이후 어떻게 운영해야 하는가를 정리한다.
처음에 한 실수
문제는 SQLite를 썼다는 데 있지 않았다. SQLite를 메모리에서 전체 생성하는 방식으로 사용한 것이 문제였다. 여기에 모든 파싱 결과를 배열에 모으고 검색 본문을 여러 계층에 중복 보관하면서, 자료가 늘수록 메모리 사용량도 함께 늘어나는 구조가 됐다.
처음 설계에서 빠진 것은 다음 네 가지였다.
- 전환 기준이 없었다. 문서 수, 링크 수, 최대 메모리가 어느 수준에 도달하면 전체 재생성을 중단할지 정하지 않았다.
- 변경량을 보지 않았다. 하루에 바뀐 문서는 일부인데도 매번 전체 문서를 같은 비용으로 처리했다.
- 로컬 환경의 메모리 예산을 정하지 않았다. 기능이 완료되는지만 확인했고 다른 개발 도구와 함께 실행할 때의 부담을 측정하지 않았다.
- 무변경 실행을 설계하지 않았다. 바뀐 파일이 없는 날에도 전체 작업을 다시 수행했다.
이 네 가지는 데이터가 작을 때는 드러나지 않는다. 그래서 작은 Wiki에서 시작할수록 현재 성능보다 자료가 5배, 10배가 됐을 때 비용이 무엇을 따라 증가하는지를 먼저 봐야 한다.
핵심 결론
- 원문은 보존하고 SQLite, FTS, 그래프, 캐시는 다시 만들 수 있는 파생 데이터로 둔다.
- 메모리 사용량을 전체 문서 수가 아니라 한 문서와 한 배치의 크기에 묶는다.
- 매일 전체를 다시 만들지 않고 신규·수정·삭제 파일만 처리한다.
- 갱신 중에는 기존 인덱스를 유지하고 성공한 결과만 원자적으로 교체한다.
- 수집 완료, Wiki 반영, 검색 노출을 서로 다른 운영 상태로 관리한다.
- 문서 수뿐 아니라 링크 수, 갱신 지연, 최대 메모리, 실패 질의를 함께 본다.
규모가 커지면 문제의 성격이 달라진다
작은 Wiki에서는 구현 단순성이 가장 중요하다. 모든 파일을 읽어 메모리에서 인덱스를 다시 만들어도 실행 시간이 짧고 실패 비용도 작다. 자료가 누적되면 비용의 기준이 달라진다.
| 관점 | 작은 Wiki | 커지는 Wiki에서 생기는 문제 |
|---|---|---|
| 파일 처리 | 전체 재처리도 부담이 작다 | 바뀌지 않은 문서까지 매번 읽는다 |
| 메모리 | 전체 파싱 결과를 보관할 수 있다 | 원문·파싱 객체·DB 생성 버퍼가 동시에 쌓인다 |
| 링크 | 문서 수와 비슷한 규모다 | 한 문서가 여러 링크를 가지며 edge가 더 빠르게 늘 수 있다 |
| 실패 | 다시 실행하면 된다 | 긴 작업이 마지막에 실패하면 시간과 메모리를 모두 잃는다 |
| 최신성 | 사람이 갱신 여부를 기억할 수 있다 | 원본, 인덱스, 검색 캐시의 시점이 서로 달라질 수 있다 |
| 삭제 | 파일만 지우면 된다 | FTS, 그래프, 캐시에서도 흔적을 제거해야 한다 |
이번 사례에서는 문서가 약 4.75배 늘어나는 동안 링크는 17만 개에서 102만 개로 증가했다. LLM Wiki의 확장 비용은 문서 개수 하나만으로 설명되지 않는다. 헤딩, 링크, citation, 검색 본문, 그래프 edge가 각각 얼마나 늘어나는지 봐야 한다.
구조 전환을 검토할 신호
다음 현상이 보이면 전체 재생성 중심 구조를 다시 검토할 시점이다.
- 전체 빌드가 일상적인 갱신 주기 안에 끝나지 않는다.
- 문서 수가 조금 늘었는데 최대 메모리가 가파르게 증가한다.
- 변경이 없는 날에도 전체 빌드와 캐시 생성을 반복한다.
- 갱신이 실패했을 때 검색 서비스까지 함께 중단된다.
- 원본 최신 날짜와 검색 결과의 최신 날짜가 다르다.
- 삭제한 문서가 검색 결과나 그래프에 계속 남는다.
- 운영자가 현재 갱신 단계를 로그 파일을 뒤져야만 알 수 있다.
첫 번째 대응: 메모리를 전체 자료량에서 분리한다
기존 인덱서는 모든 Markdown을 읽고 문서, 제목, 링크, 검색 본문을 JavaScript 객체에 누적했다. 이어서 메모리 기반 SQLite가 전체 DB를 만든 뒤 마지막에 파일로 내보냈다. 이 과정에서는 원문 문자열, 파싱 결과, SQLite 페이지, export 버퍼가 같은 시간대에 존재한다.
그림 1. 자료가 늘수록 파싱 결과와 DB 생성 상태가 함께 커지는 구조.
새 구조에서는 경로, 수정 시각, 파일 크기만 먼저 비교한다. 바뀐 파일을 한 개씩 읽어 파싱한 뒤 SQLite에 바로 저장하고, 50개 단위로 커밋한다. 현재 문서 처리가 끝나면 메모리에서 놓기 때문에 작업 메모리는 전체 Wiki 크기보다 현재 문서와 배치 크기에 가깝게 유지된다.
그림 2. 한 문서씩 처리하고 배치 커밋해 작업 메모리의 상한을 관리한다.
여기서 중요한 선택은 Python이나 SQLite라는 제품명이 아니다. 입력을 스트리밍하고, 중간 결과를 디스크에 기록하며, 전체 객체를 한 번에 보관하지 않는 구조가 핵심이다.
두 번째 대응: 전체 크기보다 변경량에 비용을 묶는다
LLM Wiki의 전체 문서는 계속 늘어도 하루에 실제로 바뀌는 문서는 일부인 경우가 많다. 따라서 반복 작업의 비용은 전체 문서 수보다 변경량에 가까워야 한다.
새 인덱서는 다음 순서로 동작한다.
- 현재 파일의
path + mtime + size를 기존 문서 테이블과 비교한다. - 신규·수정·삭제 문서 목록을 만든다.
- 신규·수정 문서만 다시 읽고 파싱한다.
- 삭제 문서는 문서 ID를 기준으로 관련 레코드에서 제거한다.
- 임시 SQLite에서 작업을 완료한다.
- 모든 검증을 통과하면 현재 인덱스와 원자적으로 교체한다.
- Source Graph가 바뀐 경우에만 MCP 검색 캐시를 갱신한다.
변경 파일이 0개라면 DB 복사, 본문 파싱, 링크 재계산, 그래프 생성, 캐시 갱신을 모두 생략한다. 증분 시스템에서 가장 자주 실행되는 경로는 대규모 변경이 아니라 아무것도 바뀌지 않은 날일 수 있기 때문이다.
flowchart LR
A[파일 메타데이터 확인] --> B{신규·수정·삭제가 있는가?}
B -- 없음 --> C[현재 인덱스 유지]
B -- 있음 --> D[변경 파일만 파싱]
D --> E[임시 SQLite에 배치 반영]
E --> F[링크·그래프 정합성 확인]
F --> G[성공 결과 원자 교체]
G --> H[MCP 캐시 갱신]
flowchart LR
A[파일 메타데이터 확인] --> B{신규·수정·삭제가 있는가?}
B -- 없음 --> C[현재 인덱스 유지]
B -- 있음 --> D[변경 파일만 파싱]
D --> E[임시 SQLite에 배치 반영]
E --> F[링크·그래프 정합성 확인]
F --> G[성공 결과 원자 교체]
G --> H[MCP 캐시 갱신]
실제 규모에서 확인한 결과
측정 대상은 동일한 워크스페이스와 검색 환경이다. 개선 전 전체 빌드는 OOM으로 끝났으므로 1.1 GiB는 최대값이 아니라 관찰된 하한이다.
| 항목 | 기존 구조 | 전환 후 | 변화 |
|---|---|---|---|
| Source Graph 전체 빌드 | 1.1 GiB 이상에서 증가 후 OOM | 최대 358.6 MiB | 전체 전환 완료 |
| MCP 캐시 갱신 메모리 | 594.7 MiB | 159.8 MiB | 약 73% 감소 |
| MCP 캐시 파일 | 약 1.1 GiB | 719 MiB | 약 35% 감소 |
| 인덱스 문서 수 | 6,495 | 30,882 | 4.75배 규모 반영 |
| 인덱스 링크 수 | 173,208 | 1,028,974 | 100만 링크 반영 |
그림 3. 문서 수가 늘었지만 전체 빌드와 캐시 갱신의 최대 메모리는 낮아졌다.
최초 전환과 일상 운영은 비용이 다르다
첫 전환에서는 기존 자료를 새 구조로 옮겨야 한다. 24,422개의 변경 문서를 읽고 100만 개가 넘는 링크를 다시 계산하는 데 약 20분이 걸렸다. 이는 구조를 바꾸는 초기 마이그레이션 비용이다.
반면 변경이 없는 다음 실행에서는 30,882개 파일의 메타데이터만 확인했다.
- 변경 문서: 0개
- 삭제 문서: 0개
- 최대 메모리: 100.8 MiB
- 소요 시간: 167.32초
- DB 복사와 재생성: 생략
- MCP 캐시 갱신: 생략
규모가 큰 시스템을 평가할 때는 최초 전체 빌드와 평상시 증분 실행을 나눠 측정해야 한다. 일상 운영 비용은 전체 크기와 변경량 중 어느 쪽을 따라가는지가 더 중요하다.
규모가 더 커질 때의 다음 병목
증분 처리가 모든 비용을 없애지는 않는다. 이번 무변경 실행에서도 모든 파일의 메타데이터를 확인하는 데 약 167초가 걸렸다. 또 새 경로나 삭제가 생기면 기존 미해결 링크가 해결되거나 끊어질 수 있어 링크 정합성 계산은 넓은 범위에 영향을 준다.
현재 구조가 더 커질 때는 다음 순서로 개선하는 것이 합리적이다.
| 다음 병목 | 대응 방향 | 적용 시점 |
|---|---|---|
| 전체 디렉터리 메타데이터 스캔 | 수집기가 변경 manifest 또는 journal을 남긴다 | 무변경 검사 시간이 운영 주기를 압박할 때 |
| 전역 링크 재계산 | 변경 문서와 역링크가 연결된 범위만 다시 계산한다 | 링크 수 증가가 인덱싱 시간의 대부분을 차지할 때 |
| 단일 DB 쓰기 경합 | source나 기간 단위로 작업 큐를 나누고 최종 병합한다 | 동시에 여러 수집기가 쓰기 시작할 때 |
| 검색 결과 품질 | FTS를 유지하면서 임베딩과 reranker를 추가한다 | 정확 문자열 검색만으로 실제 질문을 못 찾을 때 |
| 오래된 지식 | claim에 근거와 유효 기간을 연결한다 | 상충하거나 만료된 사실이 누적될 때 |
| 평가 비용 | 대표 질문과 기대 근거를 회귀 테스트로 고정한다 | 검색 방식을 여러 개 비교하기 시작할 때 |
성능 기술을 미리 모두 넣을 필요는 없다. 현재 병목을 숫자로 확인하고 다음 단계 하나를 선택해야 복잡도가 운영 능력보다 빠르게 커지는 일을 막을 수 있다.
수집 규모와 Wiki 품질은 따로 관리한다
파일을 빠르게 색인할 수 있어도 그것만으로 좋은 LLM Wiki가 되지는 않는다. 수집된 원문, 정리된 근거, 장기 지식, 검색 인덱스의 역할을 분리해야 한다.
그림 4. 수집 성공, Wiki 승격, 검색 노출, 답변 품질을 별도 단계로 운영한다.
| 계층 | 역할 | 규모가 커질 때 필요한 원칙 |
|---|---|---|
raw/ai-trends/ | 수집 당시 원문과 메타데이터 | 가공 결과와 분리하고 다시 처리할 수 있게 보존한다 |
wiki/sources/ | 원문 한 건에 대한 근거 페이지 | 출처·발행일·수집일·원문 경로를 유지한다 |
wiki/entities/, wiki/concepts/ | 여러 근거에서 반복되는 장기 지식 | 기사 한 건을 바로 일반 지식으로 승격하지 않는다 |
wiki/trends/, wiki/syntheses/ | 시간 변화와 종합 분석 | 주장별 근거와 관찰 기간을 연결한다 |
processed/ai-trends/ | 독자용 보고서와 메일 | 시점별 결과물로 보존하고 canonical 원천으로 사용하지 않는다 |
| SQLite·FTS·MCP cache | 빠른 검색을 위한 파생 데이터 | 손상되거나 오래되면 원문에서 다시 만든다 |
자료가 들어왔다는 것, 검색할 수 있다는 것, 신뢰할 수 있는 Wiki 지식이 됐다는 것은 서로 다른 상태다. 이 상태를 한꺼번에 “완료”로 표시하면 수집량은 늘지만 근거가 약한 Wiki가 된다.
수집과 인덱싱을 분리해 운영한다
수집기는 새 자료를 가져오는 생산자이고, Wiki 인덱서는 완성된 파일을 읽는 소비자다. 둘을 같은 프로세스나 같은 실행 시각에 묶을 필요는 없다.
수집기는 임시 파일에 쓴 뒤 본문과 메타데이터 기록이 끝난 파일만 최종 경로로 이동한다. 인덱서는 정해진 시간에 완성된 파일만 읽는다. 읽는 도중 크기나 수정 시각이 바뀐 파일은 다음 실행으로 미룬다.
flowchart LR
A[자료 수집] --> B[임시 파일 작성]
B --> C[본문·메타데이터 검증]
C --> D[최종 경로로 이동]
D --> E[일일 증분 인덱싱]
E --> F[원본·DB·캐시 건수 확인]
F --> G[대표 질의로 검색 검증]
B -->|실패| H[오류 기록과 재시도]
flowchart LR
A[자료 수집] --> B[임시 파일 작성]
B --> C[본문·메타데이터 검증]
C --> D[최종 경로로 이동]
D --> E[일일 증분 인덱싱]
E --> F[원본·DB·캐시 건수 확인]
F --> G[대표 질의로 검색 검증]
B -->|실패| H[오류 기록과 재시도]
이 경계가 있으면 수집이 계속되는 동안에도 인덱서는 완성된 데이터만 안전하게 반영할 수 있다. 수집 실패와 검색 갱신 실패도 서로 다른 원인과 재시도 정책으로 다룰 수 있다.
검색도 한 가지 방식으로 키우지 않는다
LLM Wiki라고 해서 모든 검색을 벡터 검색으로 바꿀 필요는 없다. 이번 Source Graph에서 SQLite FTS는 모델명, 버전, 함수명, 오류 코드처럼 정확한 문자열을 찾는 데 유용했다.
규모와 질문 유형에 따라 검색 계층을 나누는 편이 낫다.
- 정확한 명칭과 날짜: SQLite FTS와 필드 필터
- 표현이 다른 유사 개념: FTS와 임베딩 검색 결합
- 문서 간 링크와 변경 영향: Source Graph
- 기업·모델·주장 사이 의미 관계: 별도의 지식 그래프
- 지난 실행의 실패 원인: 실행 로그와 오류 기록
- 반복 작업 방법: skill과 운영 문서
새 검색 기술을 추가하기 전에 실제 사용자 질문과 기대 source 문서를 평가 세트로 만든다. 그래야 임베딩이나 지식 그래프가 복잡도만 늘렸는지, 실제 검색 품질을 개선했는지 비교할 수 있다.
커지는 Wiki의 운영 주기
매일 자동화
- 완성된 신규·수정·삭제 파일을 확인한다.
- 변경분만 임시 SQLite에 반영한다.
- 검증된 인덱스를 원자적으로 교체한다.
- Source Graph가 바뀐 경우에만 MCP 캐시를 갱신한다.
- 원본·인덱스·캐시 문서 수와 마지막 갱신 시각을 비교한다.
- 신규 자료의 고유 키를 검색해 실제 노출을 확인한다.
- 실패·보류·재시도 이유를
errors/에 남긴다.
매주 검토
- Wiki로 승격되지 못하고 쌓인 source 문서
- 근거가 하나뿐이거나 서로 충돌하는 주장
- 끊어진 링크와 어디에도 연결되지 않은 문서
- 0건 검색과 근거가 부족했던 사용자 질문
- 문서·링크·DB 크기·최대 메모리의 증가 추세
매월 정리
- 중복 canonical 문서 통합
- 오래됐거나 더 이상 유효하지 않은 주장 정리
- 대표 질문 평가 세트 재실행
- 많이 찾지만 설명이 부족한 entity와 concept 보강
- 다음 용량 병목과 구조 변경 필요성 검토
반드시 노출할 운영 지표
규모가 커지면 최신성을 사람의 기억으로 관리할 수 없다. MCP의 get_source_graph_status와 update_source_graph도 이 문제를 해결하기 위해 추가했다.
| 지표 | 필요한 이유 |
|---|---|
| 원본 Markdown 수 | 수집 결과의 전체 규모를 확인한다 |
| 인덱스 문서 수 | 원본과 색인 사이의 누락을 찾는다 |
| MCP 캐시 문서 수 | 검색 제공 계층까지 반영됐는지 확인한다 |
| 변경·삭제·보류 파일 수 | 이번 실행이 실제로 처리한 범위를 확인한다 |
| 마지막 성공 시각과 최신 자료 날짜 | 실행 성공과 데이터 최신성을 구분한다 |
| 실행 시간과 최대 메모리 | 다음 확장 시점을 판단한다 |
| 문서·링크·DB 증가량 | 저장소와 그래프의 성장 속도를 확인한다 |
| 현재 단계와 실패 원인 | 재시작과 복구 지점을 결정한다 |
상태 조회는 단순 편의 기능이 아니다. 데이터 규모가 커질수록 운영자가 잘못된 검색 결과를 정상 결과로 믿지 않게 만드는 품질 장치다.
구현에서 효과가 컸던 선택
path + mtime + size기반 변경 감지로 읽어야 할 본문을 줄였다.- 한 문서씩 파싱하고 50개마다 커밋해 메모리 상한을 관리했다.
- 임시 DB와 원자 교체로 갱신 중에도 이전 검색 결과를 제공했다.
- citation 인덱스 추가로 신규 문서마다 전체 citation 테이블을 훑는 병목을 없앴다.
fetchmany()기반 캐시 생성으로 전체 결과를 한꺼번에 메모리에 올리지 않았다.- 검색 본문을 FTS에만 저장해 같은 텍스트의 중복 보관을 줄였다.
- 무변경 단축 경로로 가장 흔한 실행의 비용을 줄였다.
- 상태 조회와 수동 갱신 도구로 자동화 실패 후에도 복구할 수 있게 했다.
아직 남은 한계
- 변경 여부를 판단하려면 현재 모든 Markdown 파일의 메타데이터는 확인해야 한다.
- 새 경로나 삭제는 기존 미해결 링크에 영향을 줄 수 있어 링크 정합성 계산 범위가 넓다.
- Source Graph DB에는 검색 본문뿐 아니라 100만 개 링크, citation, graph node와 edge가 저장되므로 디스크 사용량은 계속 증가한다.
.mpsignore의 일반 glob은 적용하지만!negation 규칙은 아직 구현하지 않았다.- 네이티브 파서는 주요 Markdown 링크 형식을 지원하지만 드문 문법은 추가 호환 검증이 필요하다.
- 현재 개선은 파일·링크 그래프의 확장성에 집중했다. entity·claim 기반 의미 그래프는 별도 품질 계약과 평가가 필요하다.
검증 결과
마무리
LLM Wiki가 커질 때 가장 먼저 필요한 것은 더 복잡한 검색 모델이 아니었다. 전체 자료를 한 번에 다루던 작업을 작은 배치로 나누고, 변경량만 처리하며, 실패해도 이전 결과를 유지하고, 어느 단계까지 갱신됐는지 보여주는 운영 구조였다.
돌이켜보면 처음에 전체 재생성을 선택한 것 자체가 잘못은 아니었다. 작은 데이터에서는 가장 단순한 구조가 좋은 선택이었다. 실제 실수는 그 구조가 언제 한계에 도달하는지 측정하지 않았고, 개인 로컬 환경에 맞는 메모리 예산과 증분 전환 기준을 미리 두지 않은 데 있었다. 1GB를 넘는 메모리 사용은 단순한 숫자가 아니라 이 Wiki를 매일 부담 없이 운영할 수 없다는 신호였다.
이번 전환으로 문서가 4.75배 늘고 링크가 100만 개를 넘어도 전체 인덱스를 완성할 수 있게 됐다. 앞으로 규모가 더 커지면 메타데이터 전체 스캔과 전역 링크 계산이 다음 병목이 된다. 그때도 같은 원칙을 적용할 수 있다. 측정값으로 병목을 찾고, 비용을 전체 규모에서 변경 범위로 옮기며, 원본과 파생 데이터를 분리하는 것이다.
좋은 LLM Wiki는 많은 문서를 담은 저장소에 머물지 않는다. 자료가 계속 늘어도 갱신 가능하고, 실패에서 복구할 수 있으며, 검색 결과가 어떤 원문에서 왔는지 설명할 수 있어야 한다.
'관심있는 주제 > 탐구 노트' 카테고리의 다른 글
| LAYA 딥다이브: BERT와 무엇이 다르고, 한국어에서 얼마나 통할까? - 실험편 (0) | 2026.10.05 |
|---|---|
| 토스증권 AI 백엔드 사례로 배우는 LLM API 설계와 운영 (0) | 2026.10.02 |
| LLM 서비스 개발자는 어떻게 일해야 할까: 요청부터 AS-IS·TO-BE 검증, 배포까지 (0) | 2026.09.19 |
| MCP는 AI용 함수 호출을 넘어 어디로 가는가: Python 개발자가 알아야 할 2026 로드맵 (0) | 2026.08.24 |
| DDD를 공부하다 FDE가 떠올랐다: FDE는 한국의 SI 개발자와 무엇이 다른가 (0) | 2026.08.23 |
