쿼리를 여기저기 작성하는 것을 멈추세요: 당신의 Laravel 코드베이스에 필요한 Atomic Habit
Source: Dev.to
위의 링크에 있는 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다.
주제
- 원자 쿼리 구성 (AQC)
- 백엔드 개발
- 소프트웨어 아키텍처
- 클린 코드
You Have a Query Problem. You Just Haven’t Admitted It Yet.
일반적인 Laravel 코드베이스를 설명해 보겠습니다.
- 컨트롤러는 활성 사용자를 가져옵니다
- 작업(job)은 거의 동일한 사용자를 가져오지만 하나의 추가 조건이 있습니다
- 서비스는 다시 가져오지만 조건이 누락되었습니다
- 테스트는 처음부터 쿼리를 다시 구성합니다
같은 의도. 다른 쿼리. 다른 결과.
아무도 문제가 발생할 때까지 눈치채지 못합니다. 그러면 모두가 어떤 버전이 **“정확한”**지 추측하기 시작합니다.
그게 문제입니다.
복잡성도 아니고, 규모도 아닙니다.
불일치입니다.
The Problem Isn’t Duplication. It’s Drift.
People love saying “don’t repeat yourself,” yet they still write this everywhere:
User::where('active', true)->get();
Then somewhere else:
User::where('active', true)
->whereNotNull('email_verified_at')
->get();
Then somewhere else:
User::where('active', true)
->where('role', 'admin')
->get();
Now you don’t have duplication.
You have different definitions of the same thing.
That’s worse, because your system behaves differently depending on which file you opened.
규칙: 모든 쿼리를 하나의 레이어가 소유한다
이를 막는 방법은 단 하나다: 모든 데이터베이스 쿼리는 하나의 레이어에 존재해야 한다. 그 레이어는 AQC이다.
한 번 존재하게 되면:
- 컨트롤러는 쿼리를 작성하지 않는다
- 서비스는 쿼리를 작성하지 않는다
- 잡은 쿼리를 작성하지 않는다
- 테스트는 쿼리를 작성하지 않는다
AQC 외부에 쿼리가 존재한다면 잘못된 것이다. 논의의 여지가 없다. AQC를 위반한다.
Before (controller)
public function index(Request $request)
{
$users = User::where('active', true)
->whereNotNull('email_verified_at')
->get();
return view('users.index', compact('users'));
}
After (controller)
public function index(Request $request)
{
$users = (new GetUsers())->handle(['active' => true]);
return view('users.index', compact('users'));
}
이제 쿼리는 하나의 집을 갖는다.
모든 곳에서 동일한 규칙
컨트롤러
$users = (new GetUsers())->handle([
'role' => $request->role,
]);
서비스
$users = (new GetUsers())->handle([
'digest_enabled' => true,
]);
잡
$users = (new GetUsers())->handle([
'active' => false,
]);
테스트
$users = (new GetUsers())->handle([
'active' => true,
]);
이제는 아무도 쿼리를 작성하지 않습니다. 그저 사용할 뿐이죠. 그리고 놀라운 사실은? 여러분에게는 컴포지션이 있습니다.
AQC 작동 예시
where('active', $params['active']);
}
if (!empty($params['digest_enabled'])) {
$query->where('digest_enabled', $params['digest_enabled']);
}
if (!empty($params['role'])) {
$query->where('role', $params['role']);
}
// Apply sorting when requested; otherwise sort by id
if (isset($params['sortBy']) && isset($params['type'])) {
$query->orderBy($params['sortBy'], $params['type']);
}
return isset($params['paginate'])
? $query->paginate(User::PAGINATE)
: $query->get();
}
}
무슨 일이 일어나고 있나요?
- 조건부 필터 – 활성 사용자, 다이제스트 활성 사용자, 역할 기반 필터링 – 모두 호출자에 의해 제어됩니다. 여러 곳에 하드코딩된 것이 없습니다.
- 유연한 정렬 – 쿼리는 동적 정렬을 지원하지만, 지정되지 않은 경우 예측 가능한 동작으로 기본 설정됩니다.
- 페이지네이션 또는 전체 컬렉션 – 하나의 클래스가 UI용 페이지네이션된 리스트와 작업 또는 내보내기를 위한 전체 컬렉션을 모두 처리합니다.
이것이 바로 컴포지션이 실제로 작동하는 모습입니다. 하나의 쿼리 클래스가 여러 소비자에게 서비스를 제공하며, 각 소비자는 자체 파라미터를 전달합니다. 기본 조건과 로직은 한 곳에 존재합니다. 컨트롤러, 서비스, 혹은 작업이 필터를 다시 구현하거나 실수로 조건을 놓치는 일이 없습니다.
아름다움은? 모든 소비자가 중복하거나 흐트러진 쿼리 로직 없이 정확히 필요한 것을 얻습니다. 사용자 조회에 대한 단일 진실의 원천을 유지하게 됩니다.
얻는 것 (그리고 멈추는 것)
쿼리가 한 곳에 존재할 때:
- 비즈니스 규칙을 다시 정의하는 일을 멈춥니다
- 조건을 잊어버리는 일을 멈춥니다
- 파일을 가로질러 버그를 추적하는 일을 멈춥니다
- 어떤 쿼리가 올바른지 추측하는 일을 멈춥니다
변경은 한 번만 발생합니다. 동작은 모든 곳에서 업데이트됩니다. 끝입니다.
위반으로 간주되는 경우
AQC 외부 어디에서든 이것을 작성하면:
User::where(...);
당신은 방식을 깨뜨렸으며, AQC 디자인‑패턴 규칙을 위반한 것입니다.
- “조건 하나뿐”이라고 해도 상관없다
- “임시”라고 해도 상관없다
- “이렇게 하면 더 빠르다”라고 해도 상관없다
그것은 잘못된 것입니다. AQC는 강제될 때만 작동하며, 단순히 제안만으로는 안 됩니다.
습관
쿼리를 작성하기 전에, 선택지는 두 가지만 있습니다:
- 이미 존재합니다 → 사용하세요
- 존재하지 않습니다 → AQC에서 생성하세요
ist → AQC에서 만들기
세 번째 옵션은 없습니다.
최종 생각
Atomic Query Construction은 모든 쿼리에 단일 위치를 제공함으로써 반복적인 쿼리를 없앱니다:
- 한 레이어.
- 작업당 하나의 클래스.
- 하나의 public 메서드.
- 하나의 파라미터 시그니처.
모든 소비자는 매번 같은 쿼리를 사용합니다.
- 컨트롤러는 AQC 클래스를 호출합니다.
- 서비스는 AQC 클래스를 호출합니다.
- 작업은 AQC 클래스를 호출합니다.
- 테스트는 AQC 클래스를 호출합니다.
데이터베이스는 한 레이어 — 그리고 오직 한 레이어에만 응답합니다.
추측을 멈추세요.
중복을 멈추세요.
파일 간 미묘한 차이를 추적하는 것을 멈추세요.
레이어를 구축하고, 규칙을 강제하며, 쿼리 규율을 습관화하세요.
이것은 선택 사항이 아닙니다. 유지 보수 가능하고 예측 가능한 Laravel 코드베이스의 기반입니다.