한 상품에 딸린 channel_products 십여 건이 전부 이미지가 깨져 있는 상태였다. 코드를 읽으며 원인을 추정했지만 후보가 여럿이라 하나로 좁혀지지 않았다.
Table of contents
Open Table of contents
조사 — 코드가 낸 두 후보
첫째 후보는 파일 복사가 실패하는 경우였다. media_files 레코드 복제와 S3 copyObject 가 분리돼 있어서 복사가 실패하면 레코드는 있고 파일은 없는 상태가 되는데 그것이 정확히 깨진 이미지의 모양이다.
둘째 후보는 syncToChannels() 전체가 DB::transaction() 하나로 묶여 있다는 것이었다. 이미지 처리에서 예외가 나면 그 앞의 상품 정보 갱신까지 함께 롤백된다. 두 후보 다 그럴듯해서 코드만 읽어서는 어느 쪽인지 갈릴 수 없었다.
검증 — 데이터가 가른 것
그래서 코드를 덮고 데이터베이스 쪽 사실을 봤다. 십여 건 전부 created_at 과 updated_at 이 같아서 만들어진 뒤로 한 번도 안 바뀐 상태였다.
같은 대상의 product_histories 를 세어 보니 그것도 0건이었다. 두 사실을 합치면 동기화가 한 번도 성공한 적이 없다는 결론이 나오고 첫째 후보였다면 상품 정보는 갱신됐어야 하므로 둘째로 확정됐다.
상위의 수정 이력
확인차 상위 상품을 보니 그 뒤로 두 번이나 수정된 이력이 있었다. 수정할 때마다 동기화가 돌았을 텐데 자식은 처음 만들어진 시각 그대로였다.
두 번의 동기화가 다 롤백됐다는 뜻이 된다. 첫째 후보를 완전히 배제하려고 media_files.size 도 비교했는데 같은 파일명인데 크기가 달라서 복제 시점의 원본과 지금 원본이 다른 파일이었고 그 사이에 마스터 이미지가 바뀐 것이었다.
원인 — 복구를 막는 순환
이 진단의 결론이 생각보다 무거웠는데 상위를 아무리 수정해도 복구가 되지 않는다는 것이었다.
상위를 수정하면 동기화가 시작되고 이미지 처리에서 같은 예외가 나서 전체가 롤백되기 때문이다. 깨져 있어서 못 고치고 못 고쳐서 깨져 있는 순환이라 한 트랜잭션에 묶여 있는 한 부분 성공이 불가능했다.
보고에는 두 가지 방법이 있었는데 이 십여 건의 이미지를 손으로 복사해 넣고 끝내는 것과 syncToChannels() 의 이미지 처리와 상품 정보 갱신을 분리하는 것을 선행 조건으로 적는 것이다. 뒤쪽을 골랐다. 이유는 손으로 넣어도 상위를 다음에 수정하는 순간 같은 롤백이 또 나서다. 대신 복구가 코드 배포 뒤로 미뤄지는 것을 감수했다. 나누면 상품 정보는 갱신되고 이미지 처리는 별도로 실패하며 다음 시도에서 이미지만 다시 하면 된다.
판단 기준 — 두 지표와 순서
이 건에서 다른 조사에도 쓸 진단 지표 둘을 얻었는데 created_at = updated_at 이라는 것과 product_histories 가 0건이라는 것이다.
둘이 같이 나오면 이 레코드에 아무 일도 안 일어났다는 강한 증거이고 무언가 실패했나 보다가 아니라 성공한 적이 없다로 확정된다. 조사 순서는 코드로 후보를 뽑고 데이터로 판별한 뒤 배제한 것도 따로 확인하는 것인데 코드는 이럴 수 있다까지만 말하고 실제로 그랬다는 것은 데이터가 말한다.
정리
- 코드로 후보가 좁혀지지 않으면 데이터베이스 사실을 본다
created_at = updated_at이고 이력이 0건이면 성공한 적이 없다는 뜻이다- 파일 크기 비교로 같은 파일인지 판별할 수 있다
- 전체가
DB::transaction()하나면 부분 성공이 불가능하다 - 깨져서 롤백되고 롤백돼서 못 고치는 순환이 생긴다
- 복구에 선행 조건이 있으면 그것부터 보고한다
- 코드는 이럴 수 있다까지만 말한다
- 실제로 그랬다는 것은 데이터가 말한다