유령 레코드가 나왔다.
하위 상품은 저장됨
이미지는 없음
Table of contents
Open Table of contents
증상 — 유령 레코드와 후보
중간에 죽은 흔적이라 v2CreateChildrenProduct 가 의심스러웠다.
public function v2CreateChildrenProduct($master)
{
// 하위 상품 저장
// 이미지 복사
}
v2CreateChildrenProduct 가 저장 뒤에 복사를 한다.
copy 가 실패하면 저장만 남아 유령 레코드가 되는 구조여서 여기에 트랜잭션을 넣어야 하나 싶었다.
판단 기준 — 실패가 무엇으로 오는가
트랜잭션을 넣기 전에 하나를 먼저 확인했다.
$disk->copy($from, $to);
copy 가 거짓을 반환하는지 예외를 던지는지가 갈린다.
[거짓 반환] 조용히 실패 → 다음 줄로 넘어감 → 유령 레코드
[예외] 중단 → 호출처로 전파
copy 의 이 한 가지로 결론이 정반대가 된다.
검증 — 소스를 직접 읽었다
의존 라이브러리의 copy 소스를 직접 열었다.
public function copy($path, $newpath)
{
$this->assertPresent($path); // 여기
...
}
assertPresent 로 존재 검사를 하고 없으면 예외를 던진다.
assertPresent 가 켜져 있는지도 확인했는데 설정 파일에 검사 비활성 항목이 아예 없었다. 항목이 없으면 기본값이 적용되고 그 기본값은 검사 활성이었다.
문서에는 copy 의 그 부분이 명확하지 않았고 설정에 따라 달라진다는 언급만 있었다. 그래서 동작을 확정하려면 소스를 직접 읽는 수밖에 없었다.
결과 — 호출처 넷 중 셋
copy 의 예외가 어디로 가는지 봤다.
grep -rn "v2CreateChildrenProduct" app/
v2CreateChildrenProduct 를 부르는 자리가 넷 나왔다.
// ProductController:2061
DB::beginTransaction();
$result = $service->v2CreateChildrenProduct($master);
if (!$result['result']) {
DB::rollBack();
}
트랜잭션으로 감싸고 실패면 rollBack 을 부른다.
ProductController:2061 감쌈
ProductImportController:136 감쌈
ProductImportController:204 감쌈
ProductMigrationService:124 안 감쌈 ← 하나
넷 중 셋이 이미 감싸고 있었다.
결론 — 안 넣기로 했다
롤백 경로가 이미 성립하고 있었다.
복사 실패
↓ 예외
함수 안에서 catch → false 반환
↓
호출처가 rollback
여기서 함수 안에 트랜잭션을 새로 넣었으면 중첩을 만들 뻔했다.
[전] 이 함수가 유령 레코드의 원인일 수 있음
[후] 호출처 네 곳 중 한 곳만 미보호
봐야 할 곳이 ProductMigrationService 한 곳으로 좁혀졌다.
교훈 — 용의자를 지우는 것
이번 유령 레코드가 ProductMigrationService 경로에서 나온 것인지도 확인했다.
아니었고 다른 경로에서 온 것이어서 이 함수는 이번 건의 원인이 아니었다.
[안 확인했으면] 여기에 트랜잭션 넣고 → 유령 레코드 또 나옴
[확인했으니] 여기를 지우고 다른 데를 봄
원인을 찾는 것과 원인이 아닌 것을 지우는 것이 둘 다 필요하다.
여기가 의심스러우니 일단 고쳐 두자로 가면 안 고쳐졌을 때 처음부터 다시 본다. 원인을 못 찾았어도 용의자를 지우는 것은 분명한 진전이었다.
정리
- 실패가 거짓 반환인지 예외인지가 롤백 성립을 가른다
- 라이브러리 소스를 직접 읽어 확인한다
- 관련 설정이 없으면 기본값이 적용된다
- 기본값이 무엇인지를 확인한다
- 호출처를 전부 세어 각각의 보호 여부를 본다
- 이미 성립하면 안쪽에 트랜잭션을 새로 넣지 않는다
- 미보호 구간이 한 곳으로 좁혀진다
- 용의자를 지우는 것도 진전이다
- 의심만으로 고치면 안 고쳐졌을 때 처음부터 다시 본다