OPEN HYPER STEP
← 목록으로 (Java+Spring)
JAVA · 73 / 99
java
CHAPTER 73 / 99
읽기 약 2
FUNCTION

Spring Data JPA 심화


핵심 개념

QueryDSL·Specification·Projection·N+1 fetch join·페이지네이션 — 복잡한 검색 필터.

본문

N+1 문제

JAVA📋 코드 (19줄)
// ❌ N+1 — 1번 + N번 추가 쿼리
@Transactional(readOnly = true)
public List<UserDto> getAll() {
    List<User> users = userRepository.findAll();  // 1 query
    return users.stream()
        .map(u -> new UserDto(u.getId(), u.getName(), u.getOrders().size()))
        // 각 user마다 orders 조회 — N queries
        .toList();
}


// ✅ Fetch Join — 1번 쿼리
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders();


// ✅ EntityGraph — 어노테이션 기반
@EntityGraph(attributePaths = {"orders"})
List<User> findAll();

페이지네이션

JAVA📋 코드 (17줄)
@RestController
@RequiredArgsConstructor
public class PostController {
    private final PostRepository repo;

    @GetMapping("/posts")
    public Page<PostDto> getPosts(
        @PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC)
        Pageable pageable
    ) {
        return repo.findAll(pageable)
            .map(PostDto::from);
    }
}

// 호출:
// GET /posts?page=0&size=20&sort=createdAt,desc

QueryDSL — 동적 쿼리

JAVA📋 코드 (38줄)
// build.gradle: implementation 'com.querydsl:querydsl-jpa:5.0.0'

@Repository
@RequiredArgsConstructor
public class PostQueryRepository {
    private final JPAQueryFactory queryFactory;

    public List<Post> search(PostSearchDto search) {
        QPost post = QPost.post;

        return queryFactory
            .selectFrom(post)
            .where(
                titleContains(search.title()),
                authorIdEq(search.authorId()),
                createdAtBetween(search.from(), search.to())
            )
            .orderBy(post.createdAt.desc())
            .limit(search.limit())
            .fetch();
    }

    // null 안전 동적 조건
    private BooleanExpression titleContains(String title) {
        return title != null ? QPost.post.title.contains(title) : null;
    }

    private BooleanExpression authorIdEq(Long id) {
        return id != null ? QPost.post.author.id.eq(id) : null;
    }

    private BooleanExpression createdAtBetween(LocalDate from, LocalDate to) {
        if (from == null && to == null) return null;
        if (from == null) return QPost.post.createdAt.before(to.atStartOfDay());
        if (to == null) return QPost.post.createdAt.after(from.atStartOfDay());
        return QPost.post.createdAt.between(from.atStartOfDay(), to.atStartOfDay());
    }
}

Projection — DTO 직접 매핑

JAVA📋 코드 (22줄)
public interface PostSummary {
    Long getId();
    String getTitle();
    String getAuthorName();
    int getCommentCount();
}


@Repository
public interface PostRepository extends JpaRepository<Post, Long> {

    // 인터페이스 기반 — 자동 매핑
    @Query("SELECT p.id as id, p.title as title, p.author.name as authorName, " +
           "       SIZE(p.comments) as commentCount " +
           "FROM Post p")
    List<PostSummary> findAllSummaries();


    // Class 기반 — DTO에 생성자 필요
    @Query("SELECT new com.example.PostSummaryDto(p.id, p.title) FROM Post p")
    List<PostSummaryDto> findAllAsDto();
}

Specification — 동적 조건 결합

JAVA📋 코드 (29줄)
@Repository
public interface UserRepository extends JpaRepository<User, Long>,
                                          JpaSpecificationExecutor<User> {
}


public class UserSpecs {
    public static Specification<User> nameContains(String name) {
        return (root, query, cb) ->
            name == null ? null : cb.like(root.get("name"), "%" + name + "%");
    }

    public static Specification<User> hasRole(Role role) {
        return (root, query, cb) ->
            role == null ? null : cb.equal(root.get("role"), role);
    }

    public static Specification<User> activeOnly() {
        return (root, query, cb) -> cb.equal(root.get("active"), true);
    }
}


// 사용 — 조건 조합
List<User> users = userRepository.findAll(
    Specification.where(UserSpecs.activeOnly())
        .and(UserSpecs.nameContains("alice"))
        .and(UserSpecs.hasRole(Role.USER))
);

다음 챕터

CH.5 "트랜잭션 관리" — @Transactional의 동작 원리.


AI 프롬프트
🤖 AI에게 잘 물어보는 법 — 모델·전략별 프롬프트
Claude

무료: Sonnet 4.6 / Pro $20/mo: Opus 4.6

내 Spring 코드의 JPA 쿼리 부분을 분석해서
N+1 문제 발견와 개선 우선순위를 알려줘.
ChatGPT

무료: GPT-5.5 / Plus $20/mo: GPT-5.5 Pro

JPA 쿼리 vs 다른 패턴 비교를
실전 사례 5개로 보여주고 JPQL vs QueryDSL 트레이드오프를 알려줘.
Gemini

무료: 2.5 Flash / Pro $19.99/mo: 3.1 Pro

내 코드베이스 전체를 분석해서
JPA 쿼리 관련 슬로우 쿼리 + N+1 위치를 보고해줘.
Grok

무료: Grok 4.1 / SuperGrok $30/mo

2026년 한국 기업의 JPA 쿼리 채택률과
한국 백엔드의 ORM 채택 트렌드를 솔직히 알려줘.

⭐ 이것만 기억하세요
Spring Data JPA 심화 이 3가지만 확실히 잡으세요
1.N+1은 JPA의 가장 흔한 함정 — fetch join 또는 @EntityGraph로 해결
2.QueryDSL은 타입 안전한 동적 쿼리 + Specification은 표준 API 기반 동적 조건
3.다음 챕터 CH.5에서 트랜잭션 — 격리 수준과 전파 옵션


공유하기
진행도 73 / 99