5줄 아키텍처 테스트로 코드 리뷰를 놓친 데이터 유출 적발

발행: (2026년 6월 14일 PM 06:20 GMT+9)
10 분 소요
원문: Dev.to

출처: Dev.to

TL;DR: Pest PHP는 코드의 구조만 아니라 행동을 테스트할 수 있습니다. 팀 규칙을 아키텍처 테스트로 작성하고 CI가 매 커밋 시 적용합니다. 이러한 테스트 중 하나는 인간이 검토를 놓친 멀티테넌트 데이터 유출을 잡았습니다.

우리는 규칙이 있었습니다. 테넌트 전용 데이터를 보유하는 모든 모델은 우리 BelongsToTenant 특성을 사용해야 합니다. 그 특성은 한 클리닉이 다른 클리닉의 데이터를 볼 수 있도록 하는 글로벌 스코프를 추가합니다.

규칙은 온보딩 과정에 있었고, 코드 리뷰 체크리스트에도 포함되어 있었으며, 모두가 알고 있었습니다.

개발자가 팀에 합류했습니다. 세 주가 지난 뒤에 새로운 모델을 추가하면서 특성을 잊어버렸습니다. 리뷰어는 비즈니스 로직에 집중했으며, 그 로직이 실제로 잘 작성되어 있었기에 누락된 특성을 눈치채지 못했고, 모델은 출시되었습니다.

두 날 동안 한 클리닉이 특정 보고서에서 다른 클리닉의 데이터 조각을 볼 수 있었습니다. 지원 티켓이 이를 포착했습니다. 우리의 테스트는 그렇지 못했습니다.

그날은 아키텍처 테스트가 프로젝트에 도입된 날이었습니다.

대부분의 테스트는 동작을 검증합니다. 주어진 입력이 있으면 함수는 해당 출력을 반환합니다. 아키텍처 테스트는 대신 구조를 검증합니다. 이는 코드가 무엇을 계산하는지에 대한 것이 아니라 코드 조직 방식을 ASSERT(검증)한다는 의미입니다.

Pest에는 이를 위해 arch 함수가 있습니다.

// tests/Architecture/ArchTest.php
arch('tenant models must use the BelongsToTenant trait')
     ->expect('App\Models')
     ->toUseTrait('App\Traits\BelongsToTenant')
     ->ignoring('App\Models\SystemSetting');

arch(‘controllers may not touch the DB facade directly’) ->expect(‘App\Http\Controllers’) ->not->toUse(‘Illuminate\Support\Facades\DB’);

arch(‘services may not depend on the HTTP request’) ->expect(‘App\Services’) ->not->toUse(‘Illuminate\Http\Request’);

arch(‘no env calls outside config files’) ->expect(‘App’) ->not->toUse(‘env’);


이들은 CI 파이프라인에서 매 커밋마다 실행됩니다. 규칙을 위반하면 빌드가 해당 규칙과 깨진 파일을 명시하는 오류 메시지로 실패합니다.

특성 테스트는 이후 몇 달 동안 네 가지 더 모델이 특성을 누락하고 있음을 포착했습니다. 각각은 데이터 유출의 잠재적 원인이었습니다. 그 한 개의 테스트만으로 전체 노력을 자체적으로 상쇄했습니다.

컨트롤러 테스트는 “이 정도면 금방 넣어보자”식의 행동을 포착합니다. 이는 사람들이 인정하는 것보다 더 자주 발생하며, 대체로 마감 압박 하에 일어납니다.

// This fails the architecture test
class ReportController extends Controller
{  
    public function index()  
    {  
         $total  = DB::table('orders')->sum('total');  

        return response()->json($total);  
     }  
}  

env 테스트는 우리가 처음부터 추가했으면 좋겠다는 생각이 듭니다. 직접 config 파일을 벗어나 env를 호출하면 개발 환경에서는 정상 작동하지만, 설정이 캐시된 후 생산 환경에서는 null을 반환합니다. 이는 클래식한 Laravel 함정입니다.

이 테스트는 다시 그 함정에 빠지는 것을 불가능하게 합니다.

대규모 구시된 코드베이스에 엄격한 아키텍처 테스트를 추가하면 한 번에 수백 개의 실패가 발생할 것입니다. 팀은 이를 resent하고 작업이 중단됩니다.

다음은 우리에게 실제로 효과적이었던 롤아웃 방법입니다.

첫 번째 규칙을 하나만 시작합니다. 가장 중요한 규칙 하나입니다. 우리에게는 특성 트레이트였습니다. 그_rule_의 몇 안 되는 위반 사항을 수정하고 바로 병합합니다.

그다음에 매 주 하나씩 규칙을 추가합니다. 팀이 각 규칙을 흡수하고 해당 규칙이 드러내는 기존 위반 사항을 수정할 수 있을만큼 충분히 느립니다.

많은 레거시 위반이 있는 규칙에 대해서는 ignoring 메서드를 사용해 구식 코드를 제외합니다.

```php
arch('controllers may not use the DB facade')
     ->expect('App\Http\Controllers')
     ->not->toUse('Illuminate\Support\Facades\DB')
     ->ignoring('App\Http\Controllers\Legacy');

이것은 새 코드에 규칙을 적용하면서 구식 코드를 자체 일정에 맞춰 리팩터링하게 합니다. 이후 무시 예외의 개수를 추적합니다. 그 수는 매 스프린트마다 감소해야 합니다. 수가 증가하면 레거시 예외 안으로 새로운 코드가 작성되고 문제가 발생했음을 의미합니다.

한 번에十规则을 추가하는 풀 리퀘스트를 시도했습니다. 빌드가 전체 코드베이스에 위반 사항으로 불타올랐습니다. 그것은 압도적이었고 팀은 강하게 반발했습니다. 나는 아이디어를 포기할 뻔했습니다.

규칙을 하나씩 추가하는 것은 선택 사항이 아니라 실제 프로젝트에서는 유일한 방법입니다. 빨간 실패壁(벽)이 사람들을 테스트 삭제하게 만들게 합니다. 단 하나의 새로운 실패 규칙만으로도人们는 한 가지만 수정하고자 합니다.

새 프로젝트의 첫 커밋에 아키텍처 테스트를 추가합니다. 기존 코드베이스에 추가하는 것은Catch-up 작업일 뿐입니다. 처음부터 시작하면 규칙이 초기부터 적용되고 위반 사항의 백로그가 전혀 생기지 않습니다.

또한 일반적인 테스트를 공유할 수 있는 작은 패키지를 구축했습니다. 우리는 세 프로젝트에서 거의 동일한 아키텍처 테스트를 작성했습니다. 재사용 가능한 패키지는 그 중복을 방지했을 것입니다.

아키텍처 테스트는 자체적으로 엄격함을 목표로 하는 것이 아닙니다. 그것은 코드베이스를 예측 가능하게 만드는 데 있습니다.

모든 테넌트 모델이 확실히 특성을 가지고 있을 때, 격리는 희망에 머무는 것이 아니라 확정된 사실이며, 빌드가 이를 검증합니다. 컨트롤러가 데이터베이스에 손을 대지 못하면 서비스 레이어는 진정한 단일 경로가 됩니다. 이러한 보장은 팀이 바뀌고 코드베이스가 성장함에도 불구하고, 기계가 매 커밋에서 검증하고 인간이 기억할 필요 없이 유지됩니다.

팀 규칙은 테스트 스위트에 소속되어야 합니다. 위키에 있는 규칙은 제안일 뿐이며, CI에 있는 규칙은 실제입니다.

만약 가능하다면 가장 먼저 어떤 규칙을 테스트로 전환하고 싶으실까요?

0 조회
Back to Blog

관련 글

더 보기 »

넷라 보안

🔱 Building Netra Security: Creating a Python-Based Static Application Security Testing SAST Tool As a cybersecurity student, I've always been curious about how...