[JPA + QueryDSL] 이해하기

2025. 10. 26. 21:10·Project/Team
728x90

 

서론 

해당 글에서는 JPA 와 QueryDSL 에 대한 개념보단 프로젝트를 진행하면서

마주한 문제와 알게된 내용을 기록하기 위해 작성하였습니다. 

 

JPA (Java Persistence API)

자바 객체(클래스) 와 데이터베이스 테이블을 자동으로 매핑해주는 ORM 표준 인터페이스이다. 
인터페이스 이기 때문에 Hibernate, OpenJPA 등이 JPA를 구현한다. 

 

ORM (Object-Relational Mapping) 

애플리케이션의 Class 와 RDB (Relational Database) 의 테이블을 매핑(연결) 한다는 뜻이다. 

기술적으로는 어플리케이션의 객체를 RDB 테이블에 자동으로 영속화 해주는 것이라고 보면 된다. 

출처 : 구글검색

 

QueryDSL 을 적용하게 된 계기 

JPA 를 통해서 쿼리를 작성하려다가, 디테일한 쿼리를 작성해야하는 것이 필요해서 

문자열 기반으로 되어 가독성이 떨어지는 @Query 어노테이션의 JPQL 만 작성하는 것보다
QueryDSL 이라는 빌더를 사용하는 것이 복잡한 쿼리를 작성하는 데 가독성이 높을 것 같아 사용하게 되었다. 

QueryDSL 

Java에서 안전하고 가독성 있는 쿼리를 작성할 수 있게 해주는 프레임워크다.

QueryDSL 은 이러한 특징을 갖고 있다. 

  • 타입 안정성 : 컴파일 시점에 쿼리의 타입을 검증하여 런타임 에러 방지 
  • 가독성 : 코드 형태로 쿼리를 작성하므로 가독성이 높아지고 유지보수가 용이하다. 
  • 동적 쿼리 지원 : 조건에 따른 동적 쿼리를 유연하게 생성할 수 있다. 
  • 통합성 : JPA, Hibernate, SQL 과 쉽게 통합 가능하다. 

 

아래 직접 QueryDSL 을 사용하면서 이해해보도록 하자! 

팔로우 리스트 조회하는 쿼리 

select
    f.id
     , f.from_user_id as user_id
     , u.email
     , u.nickname
     , IF(targetF.id is not null
              and targetF.deleted_at is null, TRUE, FALSE) as isFollowingBack
    -- , avatar_file_path
from follows f join user u
                    on f.from_user_id = u.id
               left join follows targetF
                         on f.from_user_id = targetF.to_user_id
                             and f.to_user_id = targetF.from_user_id
where f.to_user_id = 'userId' -- 로그인한 사용자의 id(pk) : 임시 데이터
  and f.deleted_at is null
#   and u.nickname like '%유저닉네임%' -- 임시 데이터 
order by f.created_at desc

 

위 쿼리를 QueryDSL 로 표현했을 때

전체코드로는 아래와 같다. 위의 실제 쿼리와 QueryDSL 의 구문은 대체로 유사하여 이해하는데 어렵지 않았다. 

하지만 처음보는 구문도 있기에 한번 하나씩 뜯어보도록 하자. 

public List<FollowSearchListResponseDTO> findFollowerList(String userId, @Nullable String keyword, @Nullable Long lastId, int size) {
    // follows 와 user 조인
    // keyword 가 존재시 nickname Like 검색 조건 추가
    return jpaQueryFactory
            .select(Projections.constructor(FollowSearchListResponseDTO.class,
                    qFollows.id,
                    qFollows.fromUserId.as("userId"),
                    qUser.email,
                    qUser.nickname,
                    Expressions.booleanTemplate(
                            "CASE WHEN {0} IS NOT NULL AND {1} IS NULL " +
                                    "THEN TRUE ELSE FALSE END",
                            targetF.id,
                            targetF.deletedAt
                    ).as("isFollowingBack")
                    ))
            .from(qFollows)
            .join(qUser).on(qFollows.fromUserId.eq(qUser.id))
            .leftJoin(targetF)
                .on(qFollows.fromUserId.eq(targetF.toUserId)
                .and(qFollows.toUserId.eq(targetF.fromUserId)))
            .where(qFollows.toUserId.eq(userId)
                    .and(qFollows.deletedAt.isNull())
                    .and(keywordCondition(keyword))
                    .and(cursorCondition(lastId))
                    )
            .orderBy(qFollows.createdAt.desc())
            .limit(size)
            .fetch();

 

  • QFollows 라는 Q클래스가 이미  생성되어 있고 주 테이블이기 때문에 객체를 생성한 상태에서 
    같은 테이블끼리 Join 이 필요할 때  또 다른 Q클래스를 생성해야 할 경우 
    • 전역 변수로 만들었기 때문에 위 코드에는 생성과정이 보이지 않아, 따로 넣어주었다. 
// 기본 인스턴스를 사용하여 Q클래스 사용 
private final QFollows qFollows = QFollows.follows;
// 원하는 별칭("targetF") 으로 Q클래스 사용  
private final QFollows targetF = new QFollows("targetF");

 

  • CASE WHEN THEN 을 표현할 경우
    • Expressions.booleanTemplate("SQL 표현식", 파라미터들...)
      • 복잡하거나 QueryDSL에서 직접 지원하지 않는 SQL 구문을 그대로 표현할 때 사용하는
        SQL 템플릿 기반 Boolean 표현식 생성 메서드이다. 
      • 아래 코드에서 {0} 과 {1} 은 placeholder로,
        뒤에 오는 파라미터를 SQL로 치환해주는데 순서대로 0에는 targetF.id 가 들어가고
        1 에는 targetF.deletedAt 이 들어가게 된다. 
Expressions.booleanTemplate(
    "CASE WHEN {0} IS NOT NULL AND {1} IS NULL THEN TRUE ELSE FALSE END",
    targetF.id,
    targetF.deletedAt
)

 

  • QueryDSL 을 DTO 로 결과를 매핑하는 방식 
    • Projections.constructor() : 생성자를 통해 매핑 (생성자 기반이라 불변객체가 가능하여 Builder 패턴에 적합함) 
    • Projections. fields() : 필드 이름으로 매핑 
    • Projections.bean() : Setter 로 매핑 

 

JPA 의 더티체킹 (Dirty Checking) 

JPA 는 DB 에서 엔티티를 조회하거나 저장 하는 시점에 

그 엔티티의 @Id 의 값을 알게 된다. 

  • 엔티티를 생성 할 때 @Id 값을 DB 에서 받아오고 
  • 조회 시 쿼리 결과에서 id 컬럼을 함께 읽어 오기 때문이다. 

이 시점에서 JPA 는 영속성 컨텍스트(1차 캐시) 에 등록을 한다. 

 

내가 이 부분을 깨닫게 된 시점은 아래 QueryDSL 의 수정 부분의 동작을 이해하게 되었을 때이다. 

 

@Override
    public void addFollow(String fromUserId, String toUserId) {
        Follows existing = jpaQueryFactory
                .selectFrom(qFollows)
                .where(qFollows.fromUserId.eq(fromUserId)
                        .and(qFollows.toUserId.eq(toUserId))
                        .and(qFollows.deletedAt.isNotNull()))
                .fetchOne();

        // 객체가 비어있다면 첫 팔로우 추가
        if(ObjectUtils.isEmpty(existing)) {
            jpaQueryFactory
                    .insert(qFollows)
                    .columns(qFollows.fromUserId, qFollows.toUserId)
                    .execute();
        } else if(existing.getDeletedAt() != null) {
            existing.restore();
        }

}

 

여기서 중요하게 봐야할 부분은 existing.restore() 메서드를 호출하는 구간이다. 

이 구간은 deletedAt 컬럼이 null 이냐 아니냐 를 통해서 

  • 기존에 팔로우 했다가 언팔한 사람을 다시 팔로우 하는 시점인지
  • 새로운 사람을 팔로우하는 시점인지 

를 파악하는 곳이었고, deletedAt 이 null 이 아닐 경우 전자 시점에 해당하여 

이미 생성된 row 에서 deletedAt 의 컬럼 데이터만 null 로 바꾸어주면 다시 팔로우 하게 되게 하는 쿼리문이었다. 

 

본래라면 아래코드와 같이 다시 수정 쿼리를 날려야 할 것 같지만, JPA 의 더티 체킹 이라는 기능으로 인해 

달라진 필드만 update 가 된다는 것을 알게 되었다. 

 

jpaQueryFactory
            .update(qFollows)
            .setNull(qFollows.deletedAt)
            .where(qFollows.fromUserId.eq(fromUserId)
                    .and(qFollows.toUserId.eq(toUserId))
                    .and(qFollows.deletedAt.isNull()))
            .execute();

 

저 restore 메서드는 Follows 엔티티에 작성된 메서드인 아래와 같다. 

public void restore() {
        this.deletedAt = null;
        this.createdAt = LocalDateTime.now();
    }

 

이렇게 deletedAt 컬럼과 createdAt 컬럼이 변경됨으로써 JPA 의 더티체킹에 걸리게 되면서 

따로 update 쿼리를 생성할 필요 없이 조회 시 저장하고 있던 id 값을 통해 update 를 자동으로 요청하게 되는 것이다. 

 

 

참고 

[Spring JPA] JPA 란 ?

 

[Spring JPA] JPA 란?

이번 글에서는 JPA(Java Persistence API)가 무엇인지 알아보려고한다. JPA는 자바 진영에서 ORM(Object-Relational Mapping) 기술 표준으로 사용되는 인터페이스의 모음이다. 그 말은 즉, 실제적으로 구현된것이

dbjh.tistory.com

[Java Spring Boot] QueryDSL 사용하기

 

[Java Spring Boot] QueryDSL 사용하기.

QueryDSL의 특징QueryDSL은 Java에서 안전하고 가독성 있는 쿼리를 작성할 수 있게 해주는 프레임워크이다.타입 안전성: 컴파일 시점에 쿼리의 타입을 검증하여 런타임 에러 방지가독성: 코드 형태로

kangth97.tistory.com

 

728x90

'Project > Team' 카테고리의 다른 글

[Spring Security] CurrentUser 구현체 + ArgumentResolver  (0) 2025.10.29
[Git] Branch 와 checkout 의 헤프닝  (2) 2025.10.22
'Project/Team' 카테고리의 다른 글
  • [Spring Security] CurrentUser 구현체 + ArgumentResolver
  • [Git] Branch 와 checkout 의 헤프닝
is낫널
is낫널
  • is낫널
    아직은 NULL NULL 합니다
    is낫널
  • 전체
    오늘
    어제
    • 분류 전체보기 (52)
      • Computer Science (12)
        • 운영체제 (3)
        • Java (4)
        • Spring (0)
        • 네트워크 (2)
        • 자료구조 및 알고리즘 (0)
        • 데이터베이스 (1)
      • Algorithm (10)
        • BOJ & SWEA (8)
        • Programmers (0)
        • 이론 (2)
      • Project (7)
        • Team (3)
        • Personal & Toy (4)
      • 사회인 준비생 (22)
        • SSAFY (5)
        • 이직 (1)
        • TIL (14)
      • 무작정 따라해보기 (1)
        • 블로그 (1)
  • 블로그 메뉴

    • 홈
    • 글쓰기
    • 태그
    • 방명록
    • 블로그 관리
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    알고리즘
    인터럽트핸들러
    빈 라인
    개발자
    transiant
    it세계의 괴물들
    카카오톡API
    파이썬
    Roy Fielding
    개발
    백엔드 면접지식
    BOJ
    그림자 문제
    HTTP
    백준
    LAMBDA
    14510 나무 높이
    sw적성진단
    딩코딩코
    코딩테스트
    AWS
    CS지식
    Java
    비전공자
    문자열
    자바
    backend
    개발공부
    백엔드
    CPU의 구성요소
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
is낫널
[JPA + QueryDSL] 이해하기
상단으로

티스토리툴바