fix(board): 반응 처리 락 획득 시점 게시글 삭제 레이스를 404로 처리

스코프 검증 통과 직후·Post 락 획득 시점 사이에 게시글이 삭제되면
lockForUpdate 의 findOrFail 이 ModelNotFoundException 을 던지는데, 컨트롤러가
이 예외를 모르므로 일반 500 으로 새어나가 사용자가 "반응 처리에 실패했습니다"
라는 원인 불명 메시지만 보게 되던 문제를 수정. 이미 존재를 확인한 게시글이
사라진 것과 같은 의미이므로 PostNotFoundException 으로 변환해 404 로 일관 처리.
This commit is contained in:
chym1217
2026-08-07 18:03:04 +09:00
parent 442474817d
commit 46ff9c036b
3 changed files with 48 additions and 1 deletions
@@ -107,7 +107,17 @@ class ReactionService
// 판단하도록 보장한다. 잠금 없이 existing 을 먼저 읽으면 다른 트랜잭션이
// 그 사이 상태를 바꿔도 반영되지 않아, 이미 지워진 반응을 다시 지우거나
// 캐시 카운트가 실제 반응 행 수와 어긋나는 결함으로 이어진다.
$this->reactionRepository->lockPostForReaction($post->id);
//
// 스코프 검증 통과 직후·락 획득 시점 사이에 게시글이 삭제되는 레이스에서는
// findOrFail() 이 ModelNotFoundException 을 던진다. 컨트롤러는 이 예외를
// 모르므로 그대로 두면 일반 500 으로 새어나가 사용자가 원인을 알 수 없다 —
// 이미 위에서 존재를 확인한 게시글이 사라진 것과 같은 의미이므로 동일하게
// PostNotFoundException 으로 변환한다.
try {
$this->reactionRepository->lockPostForReaction($post->id);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
throw new PostNotFoundException($post->id);
}
$existing = $this->reactionRepository->findByUserAndTarget(
$userId,
@@ -2,12 +2,14 @@
namespace Modules\Sirsoft\Board\Tests\Unit\Services;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\DB;
use Modules\Sirsoft\Board\Database\Seeders\BoardReactionTypeSeeder;
use Modules\Sirsoft\Board\Exceptions\PostNotFoundException;
use Modules\Sirsoft\Board\Exceptions\ReactionNotAllowedException;
use Modules\Sirsoft\Board\Models\Reaction;
use Modules\Sirsoft\Board\Models\ReactionType;
use Modules\Sirsoft\Board\Repositories\Contracts\ReactionRepositoryInterface;
use Modules\Sirsoft\Board\Services\ReactionService;
use Modules\Sirsoft\Board\Tests\BoardTestCase;
@@ -237,4 +239,38 @@ class ReactionServiceTest extends BoardTestCase
$this->expectException(PostNotFoundException::class);
$this->service->react($reactor->id, $this->board, 999999, $this->likeId);
}
/**
* 스코프 검증 통과 직후·락 획득 시점 사이에 게시글이 삭제되는 레이스 상황에서도
* PostNotFoundException 으로 일관되게 처리된다.
*
* lockPostForReaction() 은 findOrFail() 을 사용해 락을 거는데, 이 시점에 게시글이
* 없으면 Eloquent 의 ModelNotFoundException 이 던져진다. 이를 감싸지 않으면 컨트롤러의
* catch 블록 어디에도 걸리지 않아 일반 \Exception 으로 떨어져 500 에러가 되고, 사용자는
* "반응 처리에 실패했습니다"만 보게 되어 게시글이 없다는 실제 원인을 알 수 없다.
*
* @scenario case=post_deleted_between_scope_check_and_lock
* @effects post_not_in_board_returns_404
*/
public function test_react_wraps_model_not_found_during_lock_as_post_not_found(): void
{
$author = $this->createUser();
$reactor = $this->createUser();
$postId = $this->createTestPost(['user_id' => $author->id]);
$repository = $this->mock(ReactionRepositoryInterface::class);
$repository->shouldReceive('lockPostForReaction')
->once()
->with($postId)
->andThrow(new ModelNotFoundException());
$service = new ReactionService(
$repository,
app(\Modules\Sirsoft\Board\Repositories\Contracts\ReactionTypeRepositoryInterface::class),
app(\Modules\Sirsoft\Board\Repositories\Contracts\PostRepositoryInterface::class),
);
$this->expectException(PostNotFoundException::class);
$service->react($reactor->id, $this->board, $postId, $this->likeId);
}
}
@@ -30,6 +30,7 @@ axes:
- inactive_type_react_blocked
- use_reaction_off_react_blocked
- post_not_in_board
- post_deleted_between_scope_check_and_lock
- detail_shows_active_types_even_zero
- detail_hidden_when_no_active_types
- detail_hidden_when_use_reaction_off