java
CHAPTER 73 / 99
읽기 약 2분
FUNCTION
Spring Data JPA 심화
핵심 개념
QueryDSL·Specification·Projection·N+1 fetch join·페이지네이션 — 복잡한 검색 필터.
본문
N+1 문제
// ❌ 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();페이지네이션
@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,descQueryDSL — 동적 쿼리
// 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 직접 매핑
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 — 동적 조건 결합
@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