<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Querydsl on kastori</title><link>http://blog.kastori.dev/tags/querydsl/</link><description>Recent content in Querydsl on kastori</description><generator>Hugo -- gohugo.io</generator><language>ko-kr</language><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="http://blog.kastori.dev/tags/querydsl/index.xml" rel="self" type="application/rss+xml"/><item><title>[Spring 완전 정복 #8] QueryDSL이 실무 표준인 이유 — JPQL의 한계와 타입 안전한 동적 쿼리</title><link>http://blog.kastori.dev/tech/2026-05-19-spring-08-querydsl/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>http://blog.kastori.dev/tech/2026-05-19-spring-08-querydsl/</guid><description>&lt;h2 id="jpql의-불편함--오타가-런타임에-터진다"&gt;&lt;a href="#jpql%ec%9d%98-%eb%b6%88%ed%8e%b8%ed%95%a8--%ec%98%a4%ed%83%80%ea%b0%80-%eb%9f%b0%ed%83%80%ec%9e%84%ec%97%90-%ed%84%b0%ec%a7%84%eb%8b%a4" class="header-anchor"&gt;&lt;/a&gt;JPQL의 불편함 — 오타가 런타임에 터진다
&lt;/h2&gt;&lt;p&gt;JPA Repository의 쿼리 메서드로 해결되지 않는 복잡한 조회는 &lt;code&gt;@Query&lt;/code&gt;로 JPQL을 직접 작성한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Query&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;SELECT o FROM Order o WHERE o.member.id = :memberId AND o.status = :status&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findByMemberAndStatus&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;@Param&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;memberId&amp;#34;&lt;/span&gt;) Long memberId,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Param&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;) OrderStatus status);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이 방식의 문제가 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;컴파일 타임에 오류를 잡을 수 없다.&lt;/strong&gt; &lt;code&gt;o.member.id&lt;/code&gt;를 &lt;code&gt;o.member.idx&lt;/code&gt;로 오타를 내도 컴파일은 된다. 런타임에 쿼리가 실행되는 순간에야 오류가 난다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;동적 쿼리가 불편하다.&lt;/strong&gt; 조건이 있을 수도 없을 수도 있는 검색 기능을 JPQL로 만들려면 조건마다 쿼리 문자열을 조합해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;리팩터링에 취약하다.&lt;/strong&gt; 엔티티 필드명을 바꾸면 문자열 안의 JPQL도 직접 찾아서 수정해야 한다.&lt;/p&gt;
&lt;p&gt;QueryDSL은 이 문제를 해결한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="querydsl이란"&gt;&lt;a href="#querydsl%ec%9d%b4%eb%9e%80" class="header-anchor"&gt;&lt;/a&gt;QueryDSL이란
&lt;/h2&gt;&lt;p&gt;QueryDSL은 Java 코드로 SQL처럼 읽히는, 타입 안전한 쿼리를 작성하게 해주는 라이브러리다.&lt;/p&gt;
&lt;p&gt;빌드 시 엔티티 클래스로부터 &lt;strong&gt;Q클래스를 자동 생성&lt;/strong&gt;한다. &lt;code&gt;Order&lt;/code&gt; 엔티티가 있으면 &lt;code&gt;QOrder&lt;/code&gt;가 생성된다. Q클래스의 필드는 엔티티 필드와 1:1 대응한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;QOrder order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QOrder.&lt;span style="color:#a6e22e"&gt;order&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;QMember member &lt;span style="color:#f92672"&gt;=&lt;/span&gt; QMember.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 자바 코드로 쿼리를 작성 — 컴파일 타임 타입 체크&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;selectFrom&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;, member).&lt;span style="color:#a6e22e"&gt;fetchJoin&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(OrderStatus.&lt;span style="color:#a6e22e"&gt;COMPLETED&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;orderBy&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;createdAt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;desc&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;order.status.eq(OrderStatus.COMPLETED)&lt;/code&gt; — &lt;code&gt;order.status&lt;/code&gt;는 Q클래스의 타입 안전한 필드다. 오타를 내면 컴파일 오류가 발생한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="기본-설정"&gt;&lt;a href="#%ea%b8%b0%eb%b3%b8-%ec%84%a4%ec%a0%95" class="header-anchor"&gt;&lt;/a&gt;기본 설정
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.querydsl&lt;span style="color:#f92672"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;querydsl-jpa&lt;span style="color:#f92672"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;classifier&amp;gt;&lt;/span&gt;jakarta&lt;span style="color:#f92672"&gt;&amp;lt;/classifier&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;QueryDslConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@Bean&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; JPAQueryFactory &lt;span style="color:#a6e22e"&gt;jpaQueryFactory&lt;/span&gt;(EntityManager em) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; JPAQueryFactory(em);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="동적-쿼리--querydsl의-핵심-강점"&gt;&lt;a href="#%eb%8f%99%ec%a0%81-%ec%bf%bc%eb%a6%ac--querydsl%ec%9d%98-%ed%95%b5%ec%8b%ac-%ea%b0%95%ec%a0%90" class="header-anchor"&gt;&lt;/a&gt;동적 쿼리 — QueryDSL의 핵심 강점
&lt;/h2&gt;&lt;p&gt;검색 조건이 선택적으로 적용되는 동적 쿼리가 QueryDSL의 진짜 강점이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Repository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@RequiredArgsConstructor&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderQueryRepository&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; JPAQueryFactory queryFactory;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findByCondition&lt;/span&gt;(OrderSearchCondition cond) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;selectFrom&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;, member).&lt;span style="color:#a6e22e"&gt;fetchJoin&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; statusEq(cond.&lt;span style="color:#a6e22e"&gt;getStatus&lt;/span&gt;()),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; memberIdEq(cond.&lt;span style="color:#a6e22e"&gt;getMemberId&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// BooleanExpression — null 반환 시 WHERE 조건에서 자동 제외&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; BooleanExpression &lt;span style="color:#a6e22e"&gt;statusEq&lt;/span&gt;(OrderStatus status) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; status &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;?&lt;/span&gt; order.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(status) : &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; BooleanExpression &lt;span style="color:#a6e22e"&gt;memberIdEq&lt;/span&gt;(Long memberId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; memberId &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt; &lt;span style="color:#f92672"&gt;?&lt;/span&gt; order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;eq&lt;/span&gt;(memberId) : &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;BooleanExpression&lt;/code&gt;을 반환하는 메서드로 분리하면 두 가지 이점이 있다. null을 반환하면 WHERE 조건에서 자동으로 제외된다. 그리고 이 조건 메서드를 여러 쿼리에서 &lt;strong&gt;재사용&lt;/strong&gt;할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="dto-직접-조회--필요한-필드만"&gt;&lt;a href="#dto-%ec%a7%81%ec%a0%91-%ec%a1%b0%ed%9a%8c--%ed%95%84%ec%9a%94%ed%95%9c-%ed%95%84%eb%93%9c%eb%a7%8c" class="header-anchor"&gt;&lt;/a&gt;DTO 직접 조회 — 필요한 필드만
&lt;/h2&gt;&lt;p&gt;엔티티 전체가 아닌 필요한 컬럼만 DTO로 조회할 때 &lt;code&gt;@QueryProjection&lt;/code&gt;을 사용한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@QueryProjection&lt;/span&gt; &lt;span style="color:#75715e"&gt;// DTO 생성자에 붙이면 QOrderSummary 자동 생성&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderSummary&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; Long orderId;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; String memberName;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; OrderStatus status;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;@QueryProjection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderSummary&lt;/span&gt;(Long orderId, String memberName, OrderStatus status) { ... }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderSummary&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findSummary&lt;/span&gt;(OrderStatus status) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;select&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; QOrderSummary(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;from&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;, member)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(statusEq(status))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;DTO 생성자 파라미터도 타입 안전하게 체크된다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="페이징-최적화"&gt;&lt;a href="#%ed%8e%98%ec%9d%b4%ec%a7%95-%ec%b5%9c%ec%a0%81%ed%99%94" class="header-anchor"&gt;&lt;/a&gt;페이징 최적화
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Page&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderSummary&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findSummaryPage&lt;/span&gt;(OrderStatus status, Pageable pageable) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderSummary&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; content &lt;span style="color:#f92672"&gt;=&lt;/span&gt; queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;select&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; QOrderSummary(order.&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;, order.&lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;from&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;join&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;member&lt;/span&gt;, member)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(statusEq(status))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;offset&lt;/span&gt;(pageable.&lt;span style="color:#a6e22e"&gt;getOffset&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;limit&lt;/span&gt;(pageable.&lt;span style="color:#a6e22e"&gt;getPageSize&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// count 쿼리 분리 (최적화)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; JPAQuery&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Long&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; countQuery &lt;span style="color:#f92672"&gt;=&lt;/span&gt; queryFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;select&lt;/span&gt;(order.&lt;span style="color:#a6e22e"&gt;count&lt;/span&gt;())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;from&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;where&lt;/span&gt;(statusEq(status));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; PageableExecutionUtils.&lt;span style="color:#a6e22e"&gt;getPage&lt;/span&gt;(content, pageable, countQuery::fetchOne);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;PageableExecutionUtils.getPage()&lt;/code&gt;는 마지막 페이지이거나 첫 페이지에서 결과가 pageSize보다 적으면 count 쿼리를 실행하지 않는다. 불필요한 COUNT 쿼리를 줄이는 최적화다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="repository-구조-패턴"&gt;&lt;a href="#repository-%ea%b5%ac%ec%a1%b0-%ed%8c%a8%ed%84%b4" class="header-anchor"&gt;&lt;/a&gt;Repository 구조 패턴
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 1. 기본 JPA Repository + 커스텀 인터페이스&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderRepository&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;extends&lt;/span&gt; JpaRepository&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;Order, Long&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; OrderRepositoryCustom { }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 2. 커스텀 인터페이스 선언&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderRepositoryCustom&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; List&lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt;OrderSummary&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;findByCondition&lt;/span&gt;(OrderSearchCondition cond);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// 3. QueryDSL 구현체&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@Repository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderRepositoryImpl&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;implements&lt;/span&gt; OrderRepositoryCustom {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;final&lt;/span&gt; JPAQueryFactory queryFactory;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// QueryDSL 쿼리 구현&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;단순 CRUD는 &lt;code&gt;JpaRepository&lt;/code&gt;가, 복잡한 조회는 &lt;code&gt;OrderRepositoryImpl&lt;/code&gt;이 담당하는 역할 분리다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="언제-무엇을-쓸까"&gt;&lt;a href="#%ec%96%b8%ec%a0%9c-%eb%ac%b4%ec%97%87%ec%9d%84-%ec%93%b8%ea%b9%8c" class="header-anchor"&gt;&lt;/a&gt;언제 무엇을 쓸까
&lt;/h2&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;단순 조회 → JPA 쿼리 메서드 (findByStatus, findByMemberId)
조건 1~2개 → @Query (JPQL)
동적 쿼리 / 복잡한 조인 / DTO 조회 → QueryDSL ← 실무 표준
DB 특화 함수 / 성능 힌트 → Native Query
대용량 통계·배치 → MyBatis (별도 모듈)
&lt;/code&gt;&lt;/pre&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;기준&lt;/th&gt;
 &lt;th&gt;JPQL&lt;/th&gt;
 &lt;th&gt;QueryDSL&lt;/th&gt;
 &lt;th&gt;Native Query&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;타입 안전성&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;❌ 문자열&lt;/td&gt;
 &lt;td&gt;✅ Q클래스&lt;/td&gt;
 &lt;td&gt;❌ 문자열&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;동적 쿼리&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;불편&lt;/td&gt;
 &lt;td&gt;✅ BooleanExpression&lt;/td&gt;
 &lt;td&gt;불편&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;DB 특화 기능&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;❌&lt;/td&gt;
 &lt;td&gt;❌&lt;/td&gt;
 &lt;td&gt;✅&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;DTO 조회&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;new 키워드 (불편)&lt;/td&gt;
 &lt;td&gt;✅ @QueryProjection&lt;/td&gt;
 &lt;td&gt;수동 매핑&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="마치며"&gt;&lt;a href="#%eb%a7%88%ec%b9%98%eb%a9%b0" class="header-anchor"&gt;&lt;/a&gt;마치며
&lt;/h2&gt;&lt;p&gt;QueryDSL이 실무 표준이 된 이유는 명확하다. JPQL의 문자열 기반 한계를 타입 안전한 Java 코드로 대체하고, 동적 쿼리를 &lt;code&gt;BooleanExpression&lt;/code&gt; 패턴으로 깔끔하게 처리한다.&lt;/p&gt;
&lt;p&gt;다음 편에서는 Spring Security — 인증/인가와 JWT 구현을 정리한다.&lt;/p&gt;</description></item></channel></rss>