Spring JPA와 QueryDsl-JPA
- 왜 ORM 인가?
- ORM(Object-Relational Mapping)은 객체와 관계형 데이터베이스 간의 설정을 의미
📌 만약 ORM을 사용하지 않는다면, 관계형 데이터베이스의 테이블에 접근해서 CRUD 기능을 구현하기 위해 데이터베이스에 종속적인 SQL 작성이 필연적입니다. 또한, 데이터베이스에서는 객체 지향의 추상화, 상속, 다형성 같은 개념이 없어 데이터베이스와 객체 지향 사이의 기능과 표현 방법의 차이가 있어 패러다임의 불일치가 생깁니다.
- 정리하자면 ORM이 등장하게 된 이유는 다음과 같습니다.
- 데이터 베이스에 종속적인 코드 작성이 필연적
- 데이터베이스와 객체 지향 프로그래밍의 패러다임 불일치
- ORM을 활용할 경우 다음과 같은 문제점을 해결할 수 있습니다.
Spring ORM 구현 방법
- Spring에서 ORM을 구현하는 방법
- JPA Interface
- JPQL
- QueryDsl
- JPA Interface
- Spring Data JPA에서 제공하는JpaRepository 인터페이스를 사용 JpaRepository를 상속받은 인터페이스를 생성하여 기본적인 CRUD 작업을 간단하게 처리
@Repository
public interface UserRepository extends JpaRepository<User, Long> {}
// MemberService에서 회원 조회 하는 로직 중
private final UserRepository userRepository;
userRepository.findById(id).orElseThrow(EntityNotFoundException::new);
- JPA Inteface의 장단점
- 장점: 간단한 CRUD 구현의 생산성을 높여준다.
- 단점: 복잡한 쿼리는 제한될 수 있고, 동적인 쿼리 작성은 어렵거나 가독성이 떨어 질수 있다.
- JPQL (Java Persistence Query Language)
- JPA에서 사용하는 객체 지향 쿼리 언어
- SQL과 유사한 문법, 엔티티 객체를 대상으로 쿼리를 작성 가능
@Repository
public class UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u from User u where u.email = :email")
Optional<User> findByEmail(@Param("email") String email);
}
* JPQL의 장단점 * 장점: JPA의 일부로 제공되는 쿼리 언어이므로 JPA 인터페이스와의 통합이 용이하다. * 단점: 복잡한 조인이나 집계 함수등을 다루기에는 제한적일 수 있다.
- QueryDsl
- QueryDsl은 JPQL을 타입 안전하게 작성하기 위한 라이브러리
import static ...user.entity.QUser.user;
@Repository
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class UserQueryRepositoryImpl {
JPAQueryFactory queryFactory;
public UserDto getUserInfo(String email) {
return queryFactory
.select(new QUserDto(
user.name,
user.email,
user.age
))
.from(user)
.where(email.eq(user.email))
.fetchOne();
}
}
- QueryDsl의 장점
- 장점: 컴파일 시점에 오류를 발견할 수 있어 쿼리 작성 중 발생하는 오타나 잘못된 필드 접근 등의 오류를 방지하여 안정성을 확보
- 단점: 의존성 추가 작업이 필요하며, JPQL 기능을 완전히 지원하지는 않을 수 있다.
왜 QueryDsl 인가?
- 저희는 QueryDsl을 알기전 JPQL을 사용해 코드를 작성하였습니다. 그러던중 다음과 같은 문제 상황을 직면하게되었습니다.
@Query("SELECT u from User u where u.email= :emali")
Optional<User> findByEmail(@Param("email") String email);
org.hibernate.QueryException: could not resolve property: emali
of: com.example.User
u.emali 같이 잘못된 필드 접근 → 다음과 같은 오류 발생
- QueryDsl의 장점
- 타입 안정성 ```java import static …user.entity.QUser.user;
@Repository @RequiredArgsConstructor @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) public class UserQueryRepositoryImpl {
JPAQueryFactory queryFactory;
public UserDto getUserInfo(String email) {
return queryFactory
.select(new QUserDto(
user.name,
user.email,
user.age
))
.from(user)
.where(email.eq(user.email))
.fetchOne();
} } ```
- JPQL → 컴파일 시점에 오류 발견 ❌ 런타임 시점에 오류 발견 ⭕️
-
QueryDsl → 컴파일 시점에 오류 발견 ⭕️
- 컴파일 시점에서 오류를 발견할 수 있어 쿼리 작성 중 발생하는 오타나 잘못된 필드 접근 등의 오류를 방지하여 안정성을 확보
- 동적 쿼리 작성
- QueryDsl은 동적 쿼리 작성에 강점
- 조건 문과 조합하여 런타임 시점에 쿼리를 동적으로 생성하고 실행 → 복잡한 검색 조건에 유연하게 대응할 수 있고 다양한 검색 조건을 처리하는 데 용이
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import java.util.List;
@Repository
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class UserRepository {
JPAQueryFactory queryFactory;
public List<User> getUsersByUsernameAndEmail(String username, String email) {
return queryFactory.select(
user.username,
user.email,
user.age)
.from(user)
.where(eqUsername(username),
eqEmail(email))
.fetchOne();
}
private BooleanExpression eqUsername(String username) {
if(StringUtils.isEmpty(username) {
return null;
}
return user.username.eq(username);
}
private BooleanExpression eqEmail(String email) {
if(StringUtils.isEmpty(email) {
return null;
}
return user.email.eq(email);
}
}
위의 예시 코드는 UserRepository 클래스에서 getUsersByUsernameAndEmail 메서드를 통해 사용자의 이름과 이메일을 조건으로 받아 동적으로 쿼리를 작성하는 코드입니다. 이 예시에서는 QueryDsl을 용하여 조건을 동적으로 추가한 후, QueryDsl의 메서드를 사용하여 쿼리를 작성하고 실행합니다. 이를 통해 필요한 조건에 따라 다양한 쿼리를 동적으로 작성할 수 있습니다.
QueryDsl 사용하기
- Configuration - QueryDsl 설정
@Configuration
public class QueryDslConfig {
@Bean
public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
return new JPAQueryFactory(entityManager);
}
}
-
JPAQueryFactory 란?
JPAQueryFactory는 JPA 기반 애플리케이션에서 QueryDsl과 함께 사용 되는 유틸리티 클래스로, 쿼리 작성과 실행을 간편하게 지원하는 역할을 한다.
-
EntityManager 란?
EntityManager는 JPA에서 Entity 와 상호작용하는 데 사용 되는 핵심 인터페이스로, 영속성 관리, 트랜잭션 관리, Entity 의 CRUD 작업 등을 수행합니다.
- Entity 설계
@Entity public class Club { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; private String name; private Integer capacity; }
- JPA Interface + QueryDsl
3-1. ClubService
public List<ClubInUserListResponse> selectClubInUsers(Long clubId) {
return Optional.ofNullable(clubRepository.findClubInUsers(clubId))
.orElse(Collections.emptyList());
}
3-1. ClubRepository
@Repository
public interface ClubRepository extends JpaRepository<Club, Long>, ClubQueryDslRepository{
}
3-2. ClubQueryDslRepository
public interface ClubQueryDslRepository {
List<ClubInUserListResponse> findClubInUsers(Long clubId);
}
3-3. ClubQueryDslRepositoryDsl
import static gdsc.netwalk.domain.activity.entity.QActivity.activity;
import static gdsc.netwalk.domain.club.entity.QParticipant.participant;
import static gdsc.netwalk.domain.user.entity.QUser.user;
public class ClubQueryDslRepositoryImpl implements ClubQueryDslRepository {
JPAQueryFactory queryFactory;
@Override
public List<ClubInUserListResponse> findClubInUsers(Long clubId) {
return queryFactory
.select(new QClubInUserListResponse(
user.id,
user.name,
user.email,
participant.isActive,
queryFactory
.select(activity.totalActDistance.sum())
.from(activity)
.where(activity.user.id.eq(user.id)),
queryFactory
.select(activity.totalActTime.sum())
.from(activity)
.where(activity.user.id.eq(user.id))
))
.from(participant)
.innerJoin(participant.user, user)
.where(participant.club.id.eq(clubId))
.fetch();
}
}
- 주목할 점은 import static … .QUser.user 로 되어 있는 import문 과 QueryDsl 조회 결과를 DTO에 Mapping하는 작업입니다.
-
QueryDsl에서 사용하는 인스턴스는 @Entity 어노테이션이 붙은 엔티티와, DTO 생성자에 @QueryProjection 을 붙여 QueyDsl용 인스턴스로 사용할 수 있습니다. 그리고 gradle이나 maven build를 하게 되면 아래 그림처럼 구현객체가 생성되고 QueryDsl에서 사용할 수 있습니다.
- 그럼 굳이 왜 Entity에 매핑하지 않고 DTO 객체에 쿼리 결과를 매핑해서 사용할 까요?
@QueryProjection public ClubInUserListResponse( Long userId, String name, String email, Boolean isActive, Double totalActDistance, Double totalActTime) { this.userId = userId; this.name = name; this.email = email; this.isActive = isActive; this.totalActDistance = totalActDistance; this.totalActTime = totalActTime; }
-
이유는 API의 응답을 더욱 세밀하게 제어할 수 있기 때문입니다.
- 엔티티는 일반적으로 DB구조와 밀접하게 연관되어 있지만 DTO는 비즈니스 요구사항에 맞게 구성될 수 있어 유연합니다.
- API 응답에서 필요한 데이터만 포함하기에 데이터의 구조를 클라이언트에 노출시키지 않는등 보안적인 측면에서도 장점이 있습니다
QueryDsl 성능 개선 (feat 우아한 테크 콘서트)
해당 내용은 2020 우아한 테크콘서트 내용 중 일부를 소개합니다.
-
핵심은 Entity 가 필요한게 아니라면 QueryDsl Dto을 통해 딱 필요한 항목들만 조회하고 업데이트 하는 것!
- Entity로 조회 시 다음과 같은 문제가 발생합니다.
- Hibernate 캐시 불가
- 불필요한 컬럼 조회
- OneToOne N+1 쿼리
- OneToOne은 Lazy Loding이 안되기 때문에, 무조건 N+1 문제가 발생할 수 있다.
-
정리하자면 Entity를 사용한다면 단순 조회 기능에서도 성능 이슈 요소가 많이 발생한다. 따라서 DTO를 사용해 꼭 필요한 컬럼 만을 조회하여 결과를 저장하는 방식을 사용하는 것이 성능을 높일수 있는 방법이다.
- 추가적으로 항상 QueryDsl + DTO가 진리인 것인가? 에 대한 고민이 따른다.
- 우아한 테크 콘서트 이동욱님의 발표에서는 다음과 같이 정리한다.
- Entity 조회: 실시간으로 Entity 변경이 필요한 경우 적합
- DTO 조회: 고강도 성능 개성이나 대량의 데이터 조회가 필요한 경우에 적합
- 우아한 테크 콘서트 이동욱님의 발표에서는 다음과 같이 정리한다.
- 정리하자면 비즈니스 로직의 요구사항을 먼저 파악하고 어떤 방법을 적용하는것이 요구에 가장 부합하며 성능을 높일 수 있는지 충분한 고민을 하는 것이 중요하다고 생각합니다. 저희는 다음과 같이 상황에 맞게 ORM을 활용하는 것을 추천 드립니다.
🔥 JPA Interface는 이럴때 사용하자!
- 복잡한 쿼리 보다는 CRUD 작업에 집중하고자 할 때
🔥 JPQL 는 이럴때 사용하자!
- 동적인 쿼리보다는 정적인 쿼리 작성에 더 초점을 둘 때.
- JPA 인터페이스와의 통합이 필요할 때
🔥 QueryDsl 는 이럴때 사용하자!
- 타입 안정성을 강조하고 싶은 경우.
- 복잡한 동적 쿼리를 다루어야 할 때.