SQL 쿼리 빌더의 두 가지 유형
요약
본 글은 파이프 구문(Pipe Syntax)을 활용한 쿼리 빌더의 장점과 두 가지 유형에 대해 논합니다. DSL 기반 쿼리 빌더는 동적 쿼리 조합 및 공통 로직 재사용에 용이하며, 일반 SQL 작성보다 개발 편의성을 높여줍니다. 다만, 기존 SQL 지식 활용을 제한하거나 추상화가 내부 구조를 드러내는 등 근본적인 가치에 대한 의문도 제기합니다.
핵심 포인트
- 파이프 구문은 쿼리 조합 및 테스트 용이성을 극대화한다.
- DSL의 핵심 장점은 동적 쿼리 생성과 로직 재사용이다.
- 순서 기반 DSL(declarative-dsls) 구현 시 파서 단순화가 가능하다.
- 최종적으로는 사용 편의성과 개발자 경험을 모두 고려한 API 설계가 필요하다.
파이프 구문을 보고 정말 깨달음을 얻었음. SQL에 꽤 능숙한 편이지만, 파이프라인을 쓰면 쿼리를 훨씬 쉽게 조합할 수 있음. 빠른 테스트를 위해 LIMIT이나 WHERE를 넣었다 빼는 작업도 다른 부분을 고치지 않고 주석 처리만으로 가능함.
Google에서 일하지만, 언어 구문에 대한 열정은 직장과 무관함.
declarative-dsls에서 인수 순서에 의미를 부여하는 방식, 즉 파이프 기호 없는 파이프 구문을 고민 중임. 지금은 인수를 거의 어떤 순서로든 넣을 수 있는데, 순서를 의미 있게 만들면 오히려 파서가 단순해짐. 현재도 df/select에 :from, :where, :sort, :limit를 함께 지정할 수 있음. tech.ml이나 Qi처럼 ->를 쓰는 접근도 있지만, 개인적으로 크게 좋아하지는 않음. 매크로로 필터·조인·계산 열·정렬 등을 효율적으로 연결할 수 있겠지만, 개별 연산 대부분이 아직 비공개 apply- 함수여서 제시한 형태 그대로는 작동하지 않음.
대신 ->>로 여러 df/select를 연결하는 방식은 이미 지원함. 하나의 df/select 안에서도 여러 :where가 순서대로 작동하며, 예시에서는 조인 이후 계산한 나이로 필터링하려면 새 select가 필요함. 개별 함수를 공개해 직접 연결하는 API를 제공하는 편이 좋을까? 인수 순서를 의미 있게 만들되, 사용 편의를 위해 이를 알아서 처리하는 기본 select도 제공하는 쪽이 가장 좋아 보임.
「SQL Has Problems. We Can Fix Them: Pipe Syntax In SQL (2024)」를 읽고 이해한 바로는, 파이프 구문은 SQL 자체에 내장된 데이터 지향 쿼리 빌더임. 관련 논의는 previously (3 comments)와 previouslyier (31 comments)에 있음.
논문의 첫 예시는 고객별 주문 수를 집계한 서브쿼리를 다시 집계하는 SQL을, FROM customer에서 시작해 LEFT OUTER JOIN, 두 번의 AGGREGATE, ORDER BY를 차례로 연결하는 형태로 바꿈.
블로그의 중첩 쿼리와 From(:person) |> Where(...) |> Order(...) |> Limit(100) |> Select(...) 형태의 FunSQL 예시도 비슷한 대비를 보여줌. 펼쳐진 SQL에서 서브쿼리가 어떻게 생기는지 눈여겨볼 만함.
예시의 쿼리 빌더들이 일반 SQL과 너무 비슷하다는 점이 눈에 띔. 그렇다면 굳이 쓸 이유가 무엇일까? SQL 지식은 여전히 필요한데, CTE·윈도 함수·RETURNING 같은 고급 기능은 사용하지 못하게 함. 일부 예시의 WHERE 조합 방식만 봐도 추상화가 내부 구조를 상당히 드러냄. SQL에 흠이 있더라도, SELECT를 끝에 놓는 것 외에 이런 DSL이 주는 가치는 무엇일까?
이 주제에 대한 생각을 더 정리해 둠. https://noteflakes.com/articles/2026-09-05-beyond-orms
지난 몇 년간 SQL을 직접 작성해 왔으며, SQL 키워드를 메서드로 옮겨 적을 이유를 모르겠음. 변수가 있을 때만 WHERE 조건을 추가하는 동적 쿼리가 조금 까다롭지만, WHERE $1 IS NULL OR foo = $1처럼 정적 쿼리로 작성함.
덕분에 SQL 실력이 훨씬 좋아졌고, 데이터베이스 쿼리 도구에 그대로 복사해 테스트하기도 쉬움. 최종 쿼리를 이미 직접 작성했으니 예상 밖의 SQL이 나올 일도 없음.
DSL의 장점은 동적 쿼리 조합과 공통 로직 재사용에 있음. 문자열을 이어 붙이거나 일반 사용자용·관리자용 쿼리를 통째로 복제하지 않고도 조건에 따라 WHERE admin = true 같은 절을 추가할 수 있으며, 보조 함수로 쿼리 로직을 분리하기도 쉬움.
Forgejo처럼 여러 데이터베이스를 지원하는 애플리케이션에서는 같은 쿼리를 서로 다른 데이터베이스에 실행할 수 있음. 다만 실제 호환 범위는 생성되는 SQL과 각 데이터베이스의 CTE 등 기능 지원에 달려 있음. 값을 그대로 끼워 넣지 않는 DSL이라면 SQL 삽입 공격도 훨씬 어려워짐. 정적 타입 언어에서는 어느 정도 타입 안전성도 얻지만, 컴파일 시점에 데이터베이스 스키마를 읽지 않는 한 한계가 있음.
이는 쿼리 빌더의 장점이지 ORM 전반에 대한 얘기는 아님. ORM도 유용할 수 있지만 구현에 따라 불필요한 복잡성을 더하고, 특정 작업 방식을 강제하는 경우가 많음.
코드에 이미 있는 모델을 바탕으로 조인 생성 보조 함수를 만들거나, 자주 쓰는 where 조각을 추상화할 수 있지 않을까? 적어도 내가 clsql 위에 구축한 프로젝트에서는 그렇게 활용함.
취향에 따라 평가가 갈리는 문법 차이를 제외하면, 가장 큰 장점은 공통 로직을 추상화하기 쉽다는 것임. 여러 SQL 방언에서는 이런 분리가 어색해지기 쉽고, 특히 성능에 영향을 줄 수 있음.
예전에 T-SQL 환경의 큰 쿼리를 뷰와 프로시저로 나눠 재사용하려다가 성능이 크게 떨어졌음. 한동안 SQL을 많이 다루지 않아 그사이 최적화기가 개선됐을 수도 있지만, 내가 보는 쿼리 빌더의 가치는 여기에 있음.
ActiveRecord처럼 기존 SQL 구조에 그대로 대응하는 DSL보다, FunSQL처럼 파이프라인 추상화를 제공하는 DSL이 더 유용하다고 봄. 각 단계가 하나의 스키마를 다른 스키마로 바꾸는 변환이어서, 프로그램으로 조합하거나 보조 함수를 만들기 훨씬 쉬움.
주 개발 언어가 쿼리를 이해하면 상호 참조 같은 기능도 활용할 수 있음. SQL 생성을 위한 문자열 연결을 줄이면 실수로 SQL 삽입 취약점을 만드는 일도 줄어듦.
제시된 쿼리 빌더들이 SQL로 더 깔끔하게 표현할 수 없는 무언가를 해 주는지는 확신하기 어려움. 그중 Django 방식은 프로그래밍 언어로 SQL 쿼리를 표현하는 이점을 실제로 살리는 편이라 가장 좋다고 봄. 같은 예시는 Person.objects.filter(gender_concept_id=8507).order_by('year_of_birth').values_list('person_id', flat=True)[:100]처럼 작성할 수 있음.
복잡한 OR/AND 조합 대신 키워드 인수로 필터를 표현하므로 중복 코드 없이 동적 쿼리를 만들기 쉬움. SELECT에 해당하는 values_list는 생략하면 모델 객체를 받으므로 대개 선택적인 최적화에 가깝고, 결과 개수 제한에는 함수를 더 연결하는 대신 Python 슬라이스를 쓸 수 있음.
물론 필터 요소를 구분하는 이중 밑줄은 적응이 필요하고, Sum·Count·Min·Max보다 복잡한 계산을 쿼리에 붙이면 금방 골치 아파짐. 하지만 그런 쿼리나 CTE는 내가 본 다른 쿼리 빌더·ORM도 깔끔하게 처리하지 못하므로, 그때는 SQL을 직접 쓰면 됨. 데이터베이스 작업 대부분을 차지하는 CRUD에는 대체로 잘 맞음.
또 하나 주의할 점은 filter와 exclude의 조건 결합 차이임. filter는 여러 번 호출하든 한 번에 키워드 인수를 여러 개 주든 대체로 예상대로 범위를 좁힘. 반면 exclude에 여러 키워드 인수를 주면 조건들을 AND로 묶은 뒤 제외하므로, 각 조건을 따로 제외하려면 보통 호출을 나눠야 함. 조금 특이하지만 익숙해질 수 있음.
Django ORM 설계를 하나만 바꿀 수 있다면 이중 밑줄 방식을 바꾸고 싶음. 가장 큰 이유는 타입 힌트와 맞지 않기 때문임. 다만 Django ORM이 설계된 당시에는 타입 힌트가 등장하기까지 10년도 넘게 남아 있었음.
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기