아래는 QueryDSL을 사용한 간단한 예제입니다. 이 예제는 User 엔티티를 기반으로 하며, 특정 조건을 가진 유저를 조회하는 기본적인 QueryDSL 사용법을 보여줍니다.
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을 활성화해야 합니다.
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라는 클래스가 자동으로 생성됩니다.
public interface UserRepositoryCustom {
List<User> findUsersByCityAndAge(String city, int age);
}
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을 권장하는 이유
만약 구현체가 2개 이상이라면 어떻게 구별하고 이를 자동으로 인식을 하는가?
Spring Data JPA에서 사용자 정의 Repository 구현체가 Impl로 끝나는 클래스가 2개 이상인 경우, Spring은 다음 규칙에 따라 구현체를 인식합니다:
Spring은 특정 Repository 인터페이스와 결합할 구현체를 인터페이스 이름과 구현 클래스 이름의 패턴을 기준으로 결정합니다.
예를 들어:
UserRepository 인터페이스
UserRepositoryImpl 구현체
이 경우 UserRepositoryImpl은 UserRepository에 연결됩니다.
만약 UserRepositoryImpl 외에 다른 Impl로 끝나는 클래스가 존재하더라도 이름 패턴이 맞지 않으면 Spring은 이를 무시합니다.
동일한 이름 패턴으로 매핑할 수 있는 구현체가 두 개 이상 존재하는 경우, Spring은 매핑 불가능한 중복으로 인식하여 애플리케이션 시작 시 예외를 발생시킵니다.
예를 들어:
UserRepositoryImpl
UserRepositoryImpl_v2
두 구현체가 모두 UserRepository와 연결될 수 있다면, Spring은 어떤 구현체를 사용할지 알 수 없으므로 BeanCreationException과 같은 오류를 발생시킵니다.
만약 동일 인터페이스에 대해 여러 구현체가 필요하다면, Spring에게 명시적으로 어떤 구현체를 사용할지 지정해야 합니다. 방법은 다음과 같습니다:
@Primary로 우선순위 지정@Primary 어노테이션을 사용합니다.@Component
@Primary
public class UserRepositoryImpl implements UserRepositoryCustom {
// 기본 구현
}
@EnableJpaRepositories 설정에서 구현체를 수동으로 연결합니다.@EnableJpaRepositories(
repositoryBaseClass = CustomRepositoryImpl.class // 특정 구현체 지정
)
@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 없이 사용자 정의 메서드를 사용하려면, 다음과 같은 대안이 필요합니다:
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을 사용하므로 별도의 구현체가 필요 없습니다.
간단한 로직은 JpaRepository를 확장하여 직접 구현할 수 있습니다. 그러나 이 방식은 코드가 분리되지 않으므로 유지보수성이 떨어집니다.
구현체가 2개 이상일 때는 이름 패턴에 따라 하나만 자동으로 매핑됩니다.
매핑 중복 시 예외가 발생하며, 명시적으로 매핑을 지정해야 합니다.
@Primary, @Qualifier, 또는 Spring 설정을 활용하여 여러 구현체를 관리할 수 있습니다.
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);
}
}
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);
}
}
/users?city=Seoul&age=25와 같은 URL로 요청을 보내면 해당 조건에 맞는 유저 목록이 반환됩니다.