JPA
, QueryDSL
를 처음 도입하면서 같이 개발하는 후임들의 질문 사항을 바탕으로 작성했습니다.
- JPA, QueryDSL 너무 어려워요
- Query는 대체 어디다 짜는건가요?
- QueryDSL을 왜 쓰는건가요?
JPA 및 QueryDSL 설정 및 개발 가이드라인에 대해 작성하겠습니다.
먼저 jpa
와 querydsl
관련 의존성과 플러그인을 설정합니다.
pom.xml
에서 아래 코드를 추가해줍니다.<!-- JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- QueryDSL APT Config -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<scope>provided</scope>
</dependency>
<!-- QueryDSL JPA Config -->
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>${querydsl.version}</version>
</dependency>
QueryDSL은 Entity에 대해 자신 만의 클래스(QClass)를 생성하여 객체로 사용하기 때문에 아래와 같은 설정을 반드시 해주어야 합니다.
<plugins>
....
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
....
<plugins>
JPAQueryFactory 전역 Bean을 생성합니다.
@Configuration
public class QueryDslConfiguration {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
Entity User
생성합니다.
@Entity
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
@Column(name = "rec_key")
private Long recKey;
@Column(name = "user_id", length = 20, nullable = false)
private String userId;
@Column(name = "user_name", length = 20, nullable = false)
private String userName;
@Column(name = "create_date")
@Builder.Default
private LocalDateTime createDate = LocalDateTime.now();
@Column(name = "edit_date")
private LocalDateTime editDate = LocalDateTime.now();
}
Entity를 추가했으면, QueryDSL을 이용하기 위해 소스 폴더를 생성해야 합니다.
Maven
> 소스 및 업데이트 폴더 생성
target
> classes
> 패키지 경로
에서 QClass 생성 유무를 확인합니다.User
, QUser
클래스 확인CrudRepository<Entity, ID> 인터페이스를 상속 받아, Repository 기능을 이용합니다.
public interface UserRepository extends CrudRepository<User, Long> {
@Override
List<User> findAll();
}
해당 레포지토리에서 Custom 기능을 이용하려면 커스텀 인터페이스를 생성하여 추가적으로 상속해주면 됩니다.
public interface UserRepository extends CrudRepository<User, Long>, UserRepositoryCustom {
@Override
List<User> findAll();
}
QueryDSL을 이용하여 구현할 기능들을 Custom Interface로 추상화합니다. 저는 사용자 조회 Request시 받은 UserSearchParam 객체를 이용하여 검색하는 기능을 예시로 구현했습니다.
public interface UserRepositoryCustom {
Page<User> findAllBy(UserSearchParam param);
}
페이징 구현 예시를 위해 페이징 옵션을 넣었습니다.
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserSearchParam {
@NotNull
private int pageIdx = 1;
@NotNull
private int pageSize = 25;
private String userId;
private String userName;
}
이 구현체에서 실제 QueryDSL
을 사용한 코드를 작성합니다.
@RequiredArgsConstructor
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public Page<User> findAllBy(UserSearchParam param) {
PageRequest request = PageRequest.of(param.getPageIdx() - 1, param.getPageSize());
return new PageImpl<>(getBy(param), request, countBy(param));
}
/**
* [User] 검색 조건문 생성
* @param param 사용자 조회 파라미터
* @return 검색 조건
*/
public BooleanBuilder getCondition(UserSearchParam param) {
QUser qUser = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
if (param.getUserId() != null && !param.getUserId().isEmpty()) {
builder.and(qUser.userId.like("%" + param.getUserId() + "%"));
}
if (param.getUserName() != null && !param.getUserName().isEmpty()) {
builder.and(qUser.userId.like("%" + param.getUserName() + "%"));
}
return builder;
}
/**
* [User] 조회 - Count
* @param param 사용자 조회 파라미터
* @return 사용자 수
*/
public Long countBy(UserSearchParam param) {
QUser qUser = QUser.user;
return queryFactory
.select(qUser.count())
.from(qUser)
.where(getCondition(param))
.fetchOne();
}
/**
* [User] 조회 - List
* @param param 사용자 조회 파라미터
* @return 사용자 목록
*/
public List<User> getBy(UserSearchParam param) {
QUser qUser = QUser.user;
return queryFactory
.selectFrom(qUser)
.where(getCondition(param))
.offset(param.getPageIdx() - 1)
.limit(param.getPageSize())
.fetch();
}
}
UserRepository가 UserRepositoryCustom을 상속받았기 때문에, 따로 메소드를 추가해주지 않아도 원래 Repository의 메소드인 것처럼 사용할 수 있습니다.
@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
@Override
public PageVO<User> findAll() {
return PageVO.builder(userRepository.findAll()).build();
}
@Override
public PageVO<User> findAllBy(UserSearchParam param) {
return PageVO.builder(userRepository.findAllBy(param)).build();
}
}