Files
Gnuboard5/docs/object-cache.md
whitedot e4bd69ab82 fix: 객체 캐시의 타입별 저장소 분리로 값 충돌 방지
기존 public 멤버를 유지하면서 기타 타입의 저장소에 타입 차원을 추가하고 캐시 접근 경로를 공통화한다. API와 clone 동작을 유지하고 직접 멤버 접근 코드의 이전 방법을 문서화한다.

검증: PHP 5.2.17 및 PHP 8.4.22에서 구문 검사와 객체 캐시 회귀 테스트 통과.
2026-09-09 07:58:16 +00:00

108 lines
5.7 KiB
Markdown

# 객체 캐시의 타입 격리와 호환성
`G5_object_cache`는 한 요청 안에서 사용하는 메모리 캐시다. `common.php`에서
요청마다 객체를 생성하며 요청 간 값 공유, TTL, Redis/Memcached 연동은 제공하지 않는다.
## 메서드 계약
캐시 주소는 `(type, group, key)`로 결정된다. 타입, 그룹, 키 중 하나라도 다르면
별도의 값을 저장한다. `bbs`, `content`, `shop`과 플러그인에서 정한 타입 모두에 적용된다.
인자 순서와 기본값은 기존과 같다.
```php
$cache->set($type, $key, $data = array(), $group = 'default');
$cache->get($type, $key, $group = 'default');
$cache->exists($type, $key, $group = 'default');
$cache->delete($type, $key, $group = 'default');
```
- `type`, `group`, `key`는 문자열 또는 정수 식별자를 사용한다. 배열과 객체를
식별자로 사용하는 것은 지원하지 않는다. PHP 배열 키 규칙에 따라 정수 `123`과
문자열 `'123'`은 같은 키다. 타입에도 같은 규칙이 적용된다.
- 빈 문자열은 각 차원에서 유효하다. 빈 그룹 `''`은 생략 시 사용하는 `'default'`와 다르다.
식별자에 별도의 정규화나 구분자 연결을 하지 않는다.
- `set()`은 값을 저장하고 명시적인 반환값 없이 `NULL`을 반환한다.
- `get()`은 저장한 값을 반환하며, 없는 값은 `false`를 반환한다.
저장한 `false`와 cache miss는 `exists()`로 구분한다.
- `exists()`는 `NULL`, `false`, `0`, `'0'`, `''`, 빈 배열을 저장한 경우에도 `true`다.
- `delete()`는 해당 주소만 제거하며 성공 시 `true`, 없는 값이면 `false`를 반환한다.
- 객체는 저장 시와 조회 시 각각 `clone`한다. 기존과 같은 얕은 복사이며,
객체 내부의 중첩 객체나 배열 내부 객체까지 재귀적으로 복사하지 않는다.
- 빈 타입·그룹 버킷은 정리하지 않는다. 기타 타입을 처음 조회할 때도 빈 타입 버킷이
만들어질 수 있으며 요청 종료 시 함께 해제된다.
```php
$cache->set('shop', 'same-key', 'shop-value', 'same-group');
$cache->set('plugin', 'same-key', 'plugin-value', 'same-group');
$cache->get('shop', 'same-key', 'same-group'); // 'shop-value'
$cache->delete('plugin', 'same-key', 'same-group');
$cache->exists('shop', 'same-key', 'same-group'); // true
```
## public 멤버 호환성과 업그레이드
세 public 멤버는 제거하지 않고 직접 접근을 deprecated로 표시했다.
`$writes[$group][$key]`와 `$contents[$group][$key]` 구조는 유지한다.
기타 타입의 충돌을 해결하기 위해 `$etcs`에는 타입 차원을 추가했다.
| 타입 | 기존 저장 구조 | 변경 후 저장 구조 |
| --- | --- | --- |
| `bbs` | `$writes[$group][$key]` | 동일 |
| `content` | `$contents[$group][$key]` | 동일 |
| 그 외 | `$etcs[$group][$key]` | `$etcs[$type][$group][$key]` |
메서드와 public 멤버는 같은 저장소를 사용한다. 호환용 데이터를 별도로 복제하거나
기존 `$etcs` 주소로 fallback하지 않는다. 기존 주소에는 타입 정보가 없어 어느 타입의
값인지 복원할 수 없기 때문이다. 메서드만 사용하는 호출부는 변경할 필요가 없다.
외부 테마·플러그인이 `$etcs`를 직접 읽거나 쓰거나 삭제한다면 다음과 같이 수정해야 한다.
기존의 서로 다른 타입 간 캐시 공유에 의존했다면 공유할 호출부에서 동일한 타입을
명시적으로 사용해야 한다.
```php
// 변경 전: shop 등 모든 기타 타입이 같은 주소를 사용했다.
$cache->etcs[$group][$key] = $item;
$item = $cache->etcs[$group][$key];
unset($cache->etcs[$group][$key]);
// 변경 후 권장: 메서드로 타입과 존재 여부를 명시한다.
$cache->set('shop', $key, $item, $group);
if ($cache->exists('shop', $key, $group)) {
$item = $cache->get('shop', $key, $group);
}
$cache->delete('shop', $key, $group);
// 직접 접근이 불가피한 기존 코드의 새 주소 (deprecated)
$cache->etcs['shop'][$group][$key] = $item;
```
2026-09-09 저장소 PHP 코드 검색에서는 클래스 외부의 캐시 public 멤버 직접 접근을
확인하지 못했다. 코어의 `bbs`, `content`, `shop` 호출은 메서드를 사용한다.
[공개 구현](https://github.com/gnuboard/gnuboard5/blob/master/lib/Cache/obj.class.php)과
[원 제보](https://sir.kr/boards/g5_issues/3)도 검토했다. 공개 웹에서
`g5_object->writes`, `g5_object->contents`, `g5_object->etcs`를 검색했으나
외부 플러그인의 직접 접근 사례는 확인하지 못했다. 검색에 잡히지 않는 코드의
호환성까지 보장하는 결과는 아니므로 public 멤버 제거는 이번 변경에 포함하지 않는다.
## 검증
```sh
php -l lib/Cache/obj.class.php
php -l tests/object_cache.php
php tests/object_cache.php
```
회귀 테스트는 타입·그룹·키 격리, 삭제 범위, 빈 값과 기본값, 객체 clone,
public 멤버와 메서드의 저장소 일치, 인스턴스 간 격리를 확인한다.
DB 대역을 사용해 실제 `get_shop_item()`의 빈 그룹 및 `shop_{query hash}` 그룹,
`get_content_by_field()`의 `bbs`/`content` 적재와 `get_content_db()`의 재사용도 확인한다.
실제 DB나 웹 세션은 필요하지 않다.
구현과 테스트는 PHP 5.2.17에서 지원하는 문법으로 작성했다. PHP 8.4.22와 PHP 5.2.17
CLI에서 변경 파일의 구문 검사와 회귀 테스트가 모두 통과했다. PHP 5.2.17은
[공식 소스 배포본](https://museum.php.net/php5/php-5.2.17.tar.gz)을 임시 디렉터리에서
CLI와 PCRE를 포함하도록 빌드해 검증했다. 시스템 PHP는 변경하지 않았다.
이 검증은 객체 캐시와 위 코어 호출부에 한정하며, 사이트 전체의 PHP 5.2 호환성을
검증한 것은 아니다.