Spring boot

JPQL 핵심 개념과 대표 예시 코드 (1)

mynote6676 2025. 6. 23. 20:10

JPQL이란 ?

JPQL(Java Persistence Query Language)은 엔티티 객체를 대상으로 하는 객체지향 쿼리 언어입니다.

 

엔티티 객체란?

엔티티 객체는 데이터 베이스 테이블과 매핑된 자바 객체를 의미합니다.

================================================================

 

기본 문법 이해

 

더보기

SQL vs JPQL 비교

 // SQL (테이블 중심)
 select * from board_tb where username = 'ssar'
 
 //JPQL (엔티티 중심)
 SELECT b FROM b WHERE b.username = 'ssar'
 
 
 핵심 차이점:
 -SQL : 테이블명(board_tb), 컬럼면 사용
 - JPQL: 엔티티명(Board_tb), 컬러명 사용
 
 핵심 차이점
 SQL : 테이블 (Board), 컬럼명 사용
 - JPQL : 엔티티명(Board), 필드명

 

 

기본 구조

// JPQL 기본 구조
SELECT [별칭] FROM [엔티티명] [별칭] WHERE [조건]

// 실제 예시
"SELECT b FROM Board b WHERE b.id = :id"
//  ↑      ↑     ↑       ↑       ↑      ↑
// SELECT 별칭  엔티티명  별칭   필드명  파라미터

 

2. 기본 조회 패턴

 

더보기

전체 조

// 모든 게시글 조회
public List<Board> findAll() {
	String jpql = "SELECT b FROM Board B';
    
    return em.createQuery(jpql, Board.class)
    			.getResultList();
}

// 정렬 추가
public List<Board> findAllOrderByDate() {
	String jpql = "SELECT b FROM Board b ORDER BY b.createdAt DESE";
    
    return em.createQuery(jpql, Board.class)
    					.getReultList();
}


조건 조회

 

// 특정 사용자의 게시글
public List<Board> findByUsername(String username) {
    String jpql = "SELECT b FROM Board b WHERE b.username = :username";

    return em.createQuery(jpql, Board.class)
             .setParameter("username", username)  // 파라미터 바인딩
             .getResultList();
}

// 제목에 키워드 포함
public List<Board> findByTitleContaining(String keyword) {
    String jpql = "SELECT b FROM Board b WHERE b.title LIKE :keyword";

    return em.createQuery(jpql, Board.class)
             .setParameter("keyword", "%" + keyword + "%")
             .getResultList();
}

// 여러 조건 조합
public List<Board> findByUsernameAndTitle(String username, String titleKeyword) {
    String jpql = "SELECT b FROM Board b " +
                  "WHERE b.username = :username " +
                  "AND b.title LIKE :title";

    return em.createQuery(jpql, Board.class)
             .setParameter("username", username)
             .setParameter("title", "%" + titleKeyword + "%")
             .getResultList();
}

 

3. 집계 함수와 그룹화

더보기

개수 조회

// 전체 게시글 수
public long countAll(){
	String jpql = "SELECT COUNT(b) FROM Board b";
    
    return em.createQuery(jpq;, Long.class)
    		 .getSingleResult();
}

// 특정 사용자의 게시글 수 
public long countByUsername(String username) {
	String jpql = "SELECCT COUNT(b) FROM b WHERE b.username = :username";
    
    return em.createQuery(jpql, Long.class)
    		.setParameter("username",username)
            .getSingleResult();
}

 

통계 조회

// 사용자별 게시글 수
public List<Object[]> countByUsernameGroup() {
	String jpql = "SELECT b.username, COUNT(b)" + 
    "FROM Board b " + 
    "GROUP BY b.username " + 
    "ORDER BY COUNT(b) DESC ";
    return em.createQuery(jpql, Object[].class)
    	.getReslutList();
    // 결과 사용법
    // for (object[] result : result) {
    // 		String username = (String) result[0];
    // 		Long count  =  (Long) result[1];
    // }
}

// 평균 조회수
public Double gwtAverageViewCount(){
	String jpql = "SELECT AVG(b.viewCount) FROM Board b";
    
    return em.createQuery(jpql,Double.class)
    		.getSingleResult();
}

4 .페이징 처리

더보기

기본 페이징

// 페이지별 게시글 조회
public List<Board>fingBoardWithPaging(int page, int size) {
	String jpql - "SELECT b FROM Board b ORDER BY b. createdAt DESC";
    
    return em.createQuery(jpql, Board.class)
    		.setFirstResult(page * size) //OFFSET: 시작 위치
            .setMaxResults(size)		 // LIMIT: 조회할 개수
            .getResultList();
}

// 예시: 2페이지, 10개씩 조회
// page=1,size=10 -> setFirstResult(10),setMaxResultes(10)
// 결과 : 11~20번째 게시글 조회

 

페이징 + 검색

// 검색 조건이 있는 페이징
public List<Board> searchWithPaging(String keyword, int page, int size) {
    String jpql = "SELECT b FROM Board b " +
                  "WHERE b.title LIKE :keyword " +
                  "ORDER BY b.createdAt DESC";

    return em.createQuery(jpql, Board.class)
             .setParameter("keyword", "%" + keyword + "%")
             .setFirstResult(page * size)
             .setMaxResults(size)
             .getResultList();
}

5. 날짜 조건 조회

더보기

날짜 범위 조회

// 최근 7일 내 게시글
public List<Board> findRecentBoards(int days) {
    String jpql = "SELECT b FROM Board b " +
                  "WHERE b.createdAt >= :cutoffDate " +
                  "ORDER BY b.createdAt DESC";

    LocalDateTime cutoff = LocalDateTime.now().minusDays(days);

    return em.createQuery(jpql, Board.class)
             .setParameter("cutoffDate", cutoff)
             .getResultList();
}

// 특정 날짜 범위
public List<Board> findBoardsBetweenDates(LocalDateTime startDate, LocalDateTime endDate) {
    String jpql = "SELECT b FROM Board b " +
                  "WHERE b.createdAt BETWEEN :startDate AND :endDate " +
                  "ORDER BY b.createdAt DESC";

    return em.createQuery(jpql, Board.class)
             .setParameter("startDate", startDate)
             .setParameter("endDate", endDate)
             .getResultList();
}

 

6. 조인 쿼리(연관관계 활용)

 

더보기

기본 조인 

// 게시글과 작성자 정보 함께 조회 (N+1 문제 해결)
public List<Board> findBoardsWithUser() {
    String jpql = "SELECT b FROM Board b JOIN FETCH b.user u";

    return em.createQuery(jpql, Board.class)
             .getResultList();
}

// 조건이 있는 조인
public List<Board> findBoardsByUserStatus(String status) {
    String jpql = "SELECT b FROM Board b " +
                  "JOIN b.user u " +
                  "WHERE u.status = :status";

    return em.createQuery(jpql, Board.class)
             .setParameter("status", status)
             .getResultList();
}

 

7.서브쿼리 활용

 

평균보다 높은 조회수

// 평균 조회수보다 높은 게시글
public List<Board> findPopularBoards() {
    String jpql = "SELECT b FROM Board b " +
                  "WHERE b.viewCount > (SELECT AVG(b2.viewCount) FROM Board b2)";

    return em.createQuery(jpql, Board.class)
             .getResultList();
}

// 댓글이 많은 게시글
public List<Board> findBoardsWithManyComments(int minComments) {
    String jpql = "SELECT b FROM Board b " +
                  "WHERE b.id IN (SELECT c.board.id FROM Comment c " +
                  "               GROUP BY c.board.id " +
                  "               HAVING COUNT(c) >= :minComments)";

    return em.createQuery(jpql, Board.class)
             .setParameter("minComments", minComments)
             .getResultList();
}

 

 

8. 성능을 고려한 JPQL

더보기

필요한 필드만 조회(DTO 프로젝션 쿼리)

// DTO 클래스 정의
public class BoardSummaryDTO {
    private Long id;
    private String title;
    private String username;
    private LocalDateTime createdAt;

    public BoardSummaryDTO(Long id, String title, String username, LocalDateTime createdAt) {
        this.id = id;
        this.title = title;
        this.username = username;
        this.createdAt = createdAt;
    }
    // getter 생략
}

------------------------------------------------------------------------------

// 엔티티 객체 대신 DTO로 조회 (메모리 효율적)
public List<BoardSummaryDTO> findBoardSummaries() {
    String jpql = "SELECT new com.tenco.blog.dto.BoardSummaryDTO(" +
                  "b.id, b.title, b.username, b.createdAt) " +
                  "FROM Board b " +
                  "ORDER BY b.createdAt DESC";

    return em.createQuery(jpql, BoardSummaryDTO.class)
             .getResultList();
}

| 예시

 

// 전체 엔티티 조회 (10개 게시글)
List<Board> boards = findAll();  

메모리 사용량:
- id: 8 bytes × 10 = 80 bytes
- title: 평균 50 bytes × 10 = 500 bytes  
- content: 평균 5000 bytes × 10 = 50,000 bytes ← 불필요한 데이터!
- username: 평균 20 bytes × 10 = 200 bytes
- createdAt: 16 bytes × 10 = 160 bytes
- 기타 필드들: 2000 bytes × 10 = 20,000 bytes ← 불필요한 데이터!
총 메모리: 약 70KB

// DTO 프로젝션 조회 (10개 게시글)
List<BoardSummaryDTO> summaries = findBoardSummaries();

메모리 사용량:
- id: 8 bytes × 10 = 80 bytes
- title: 평균 50 bytes × 10 = 500 bytes
- username: 평균 20 bytes × 10 = 200 bytes  
- createdAt: 16 bytes × 10 = 160 bytes
총 메모리: 약 1KB (70배 절약!)

 

프로젝션이란?

프로젝션(projextion)은 원래 수학/기하학 용어로, 고차원 데이터를 저차원으로 투영(변환)하는 것을 의미합니다.

데이터베이스에서는 전체 데이터 중에서 필요한 부분만 선택해소 가져오는 것을 프로젝션이라 합니다.

 

 

주의사항과 Best Practice

좋은 예시 

// 파라미터 바인딩 사용(SQL Injection 방지)
String jpql = "SELECT b FROM Board b WHERE b.username = :username";
query.setParamerter("username",userInput);

// 타입 안정성 확보
List<Board> boards = em.createQuery(jpql, Board.class).getResultList();

// 적절한 페이징 처리
query.setFirstResult(offset).setMaxResults(limit);

 

 

피해야할 예시

// 문자열 연결 (SQL Injection 위험)
String jpql = "SELECT b FROM Board b WHERE b.username = '" + userInput + "'";

// 타입 미지정 (런타임 에러 위험)
List boards = em.createQuery(jpql).getResultList();

// 페이징 없는 전체 조회 (성능 문제)
List<Board> allBoards = em.createQuery("SELECT b FROM Board b", Board.class).getResultList();
728x90