QueryDSL : 예제

이진욱·2024년 12월 25일

이론

목록 보기
17/24

아래는 QueryDSL을 사용한 간단한 예제입니다. 이 예제는 User 엔티티를 기반으로 하며, 특정 조건을 가진 유저를 조회하는 기본적인 QueryDSL 사용법을 보여줍니다.

1. 프로젝트 설정

build.gradle 파일에 QueryDSL 관련 의존성을 추가합니다:

dependencies {
    implementation 'com.querydsl:querydsl-jpa:5.0.0'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jpa'
}

annotationProcessor를 추가했으므로, IDE의 빌드 옵션에서 annotation processing을 활성화해야 합니다.

2. 엔티티 생성

User 엔티티를 정의합니다.

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private int age;

    private String city;
}

빌드를 실행하면 QueryDSL의 코드 생성 도구에 의해 QUser라는 클래스가 자동으로 생성됩니다.

3. QueryDSL 예제

  • Repository 인터페이스 생성
    Spring Data JPA를 사용하여 사용자 정의 Repository를 만듭니다.
public interface UserRepositoryCustom {
    List<User> findUsersByCityAndAge(String city, int age);
}
  • Repository 구현
    QueryDSL을 사용하여 UserRepositoryCustom 인터페이스를 구현합니다.
import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;

import java.util.List;

import static com.example.demo.entity.QUser.user; // QUser import

@RequiredArgsConstructor
public class UserRepositoryImpl implements UserRepositoryCustom {
    private final JPAQueryFactory queryFactory;

    @Override
    public List<User> findUsersByCityAndAge(String city, int age) {
        return queryFactory
                .selectFrom(user)
                .where(user.city.eq(city)
                        .and(user.age.goe(age)))
                .fetch();
    }
}

JPA Repository와 Custom Repository 연결
UserRepository를 생성하고, Custom Repository를 확장합니다.

import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
}

Spring Data JPA의 자동 연결


Spring은 UserRepository를 구현체로 생성할 때 JpaRepository의 기본 메서드와 함께 UserRepositoryCustom의 구현체(UserRepositoryImpl)를 결합합니다.
즉, UserRepository는 기본 메서드와 사용자 정의 메서드를 모두 사용할 수 있습니다.

기본 규칙

  • 사용자 정의 인터페이스 이름은 자유롭게 정할 수 있습니다.
    예: UserRepositoryAdvanced, UserExtraRepository

  • 구현체 클래스 이름은 사용자 정의 인터페이스 이름에 Impl을 붙여야 합니다.
    예: 인터페이스 이름이 UserRepositoryAdvanced라면, 구현체 이름은 UserRepositoryAdvancedImpl이어야 합니다.
    인터페이스 이름이 UserExtraRepository라면, 구현체 이름은 UserExtraRepositoryImpl이어야 합니다.

  • 사용자 정의 인터페이스 이름에 접미사 Custom을 권장하는 이유

    • 관례적 이해: Custom 접미사를 사용하면 사용자 정의 Repository라는 점을 한눈에 알아볼 수 있어 코드 가독성이 높아집니다.
    • 일관성 유지: 팀 개발 환경에서 관례적으로 이름을 정하면 유지 보수와 코드 이해도가 높아집니다.

만약 구현체가 2개 이상이라면 어떻게 구별하고 이를 자동으로 인식을 하는가?

Spring Data JPA에서 사용자 정의 Repository 구현체가 Impl로 끝나는 클래스가 2개 이상인 경우, Spring은 다음 규칙에 따라 구현체를 인식합니다:

1. 인터페이스 이름과 클래스 이름의 조합을 기반으로 매핑

Spring은 특정 Repository 인터페이스와 결합할 구현체를 인터페이스 이름과 구현 클래스 이름의 패턴을 기준으로 결정합니다.

예를 들어:

UserRepository 인터페이스
UserRepositoryImpl 구현체
이 경우 UserRepositoryImplUserRepository에 연결됩니다.

만약 UserRepositoryImpl 외에 다른 Impl로 끝나는 클래스가 존재하더라도 이름 패턴이 맞지 않으면 Spring은 이를 무시합니다.

2. 중복 매핑 발생 시 예외

동일한 이름 패턴으로 매핑할 수 있는 구현체가 두 개 이상 존재하는 경우, Spring은 매핑 불가능한 중복으로 인식하여 애플리케이션 시작 시 예외를 발생시킵니다.

예를 들어:

UserRepositoryImpl
UserRepositoryImpl_v2
두 구현체가 모두 UserRepository와 연결될 수 있다면, Spring은 어떤 구현체를 사용할지 알 수 없으므로 BeanCreationException과 같은 오류를 발생시킵니다.

3. 여러 구현체를 명시적으로 연결하는 방법

만약 동일 인터페이스에 대해 여러 구현체가 필요하다면, Spring에게 명시적으로 어떤 구현체를 사용할지 지정해야 합니다. 방법은 다음과 같습니다:

  • 방법 1: @Primary로 우선순위 지정
    하나의 구현체를 기본 구현체로 설정하려면 @Primary 어노테이션을 사용합니다.
@Component
@Primary
public class UserRepositoryImpl implements UserRepositoryCustom {
    // 기본 구현
}
  • 방법 2: 특정 구현체를 명시적으로 지정
    Spring이 자동 매핑하지 않도록 하고, @EnableJpaRepositories 설정에서 구현체를 수동으로 연결합니다.
@EnableJpaRepositories(
    repositoryBaseClass = CustomRepositoryImpl.class // 특정 구현체 지정
)
  • 방법 3: Bean 이름으로 수동 지정
    Spring 컨텍스트에서 @Qualifier를 사용하여 명시적으로 구현체를 주입합니다.

동일타입으로 Bean을 여러 개 지정
ex)@Configuration
public class AppConfig {
@Bean
public Service serviceA() {
return new ServiceA();
}


@Bean
public Service serviceB() {
return new ServiceB();
}
}

Qualifier("메소드명") 객체타입 객체명

@Repository
public class UserRepositoryImpl implements UserRepositoryCustom {
    // 첫 번째 구현체
}

@Repository("customUserRepositoryImpl")
public class UserRepositoryImpl_v2 implements UserRepositoryCustom {
    // 두 번째 구현체
}

@Service
public class UserService {
    private final UserRepositoryCustom userRepositoryCustom;

    public UserService(@Qualifier("customUserRepositoryImpl") UserRepositoryCustom userRepositoryCustom) {
        this.userRepositoryCustom = userRepositoryCustom;
    }
}

- 만약 UserRepositoryImpl 없이 사용할 수 있는 경우

UserRepositoryImpl 없이 사용자 정의 메서드를 사용하려면, 다음과 같은 대안이 필요합니다:

3.1. QueryDSL 사용 없이 @Query로 처리

Spring Data JPA에서 제공하는 @Query를 사용하면 별도의 구현체 없이 JPQL로 동적 쿼리를 작성할 수 있습니다.

public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.city = :city AND u.age >= :age")
    List<User> findUsersByCityAndAge(@Param("city") String city, @Param("age") int age);
}

이 경우, QueryDSL이 아닌 JPQL을 사용하므로 별도의 구현체가 필요 없습니다.

3.2. JpaRepository에 직접 구현

간단한 로직은 JpaRepository를 확장하여 직접 구현할 수 있습니다. 그러나 이 방식은 코드가 분리되지 않으므로 유지보수성이 떨어집니다.

결론

구현체가 2개 이상일 때는 이름 패턴에 따라 하나만 자동으로 매핑됩니다.
매핑 중복 시 예외가 발생하며, 명시적으로 매핑을 지정해야 합니다.
@Primary, @Qualifier, 또는 Spring 설정을 활용하여 여러 구현체를 관리할 수 있습니다.

4. QueryDSL Config 설정

JPAQueryFactory 빈을 등록합니다.

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDslConfig {
    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) {
        return new JPAQueryFactory(entityManager);
    }
}

5. 서비스 및 컨트롤러

  • 서비스 계층
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;

    public List<User> getUsersByCityAndAge(String city, int age) {
        return userRepository.findUsersByCityAndAge(city, age);
    }
}
  • 컨트롤러 계층
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @GetMapping("/users")
    public List<User> getUsers(@RequestParam String city, @RequestParam int age) {
        return userService.getUsersByCityAndAge(city, age);
    }
}

6. 실행 및 테스트

  • 서버를 실행합니다.
    /users?city=Seoul&age=25와 같은 URL로 요청을 보내면 해당 조건에 맞는 유저 목록이 반환됩니다.
profile
열심히 하는 신입 개발자

0개의 댓글