기록하기

[사이드 프로젝트 - 비사이드] Spring Boot + JPA + Querydsl 적용 본문

Server/Spring Boot

[사이드 프로젝트 - 비사이드] Spring Boot + JPA + Querydsl 적용

jjungdev 2022. 12. 28. 21:02

비사이드 13기에 참여하게 되면서 프로젝트 세팅에서 매번 헷갈리는 내용인 Querydsl 과 애플 로그인 구현, 네이버 클라우드와 관련된 내용을 정리해보려고 한다.

  1. Spring Boot + JPA + Querydsl 적용(현재글)
  2. 네이버 클라우드 Global DNS, SSL 설정
  3. 애플 로그인 구현

Spring Boot + JPA + Querydsl 적용과 관련해서는 이미 다른 분들께서 블로그 정리를 너무나도 잘해주셨다. 그래서 블로그 작성을 고민하다가 나중에 또 세팅할 때 헷갈릴 것 같아 추후 확인을 위해 다시 정리해보려고 한다.

 

build.gradle 설정

설정은 다음과 같이 진행을 했다.

참고로 프로젝트 환경은 아래와 같다.

  • Spring Boot 2.7.7
  • Java 17
  • gradle 7.6
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.7'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    //querydsl 설정 추가
    implementation 'com.querydsl:querydsl-jpa'
    implementation 'com.querydsl:querydsl-apt'

    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
    //querydsl 설정 추가    

    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

//querydsl 설정 추가
//QClass 도 같이 build 되기 위해 src/main/generated 위치 명시
def querydslSrcDir = 'src/main/generated'
sourceSets {
    main {
        java {
            srcDirs += [ querydslSrcDir ]
        }
    }
}

//다른 Java 버전에서도 java.annotation.Generated 로 import 하도록 설정
compileJava {
    options.compilerArgs << '-Aquerydsl.generatedAnnotationClass=javax.annotation.Generated'
}

//위에서 선언한 src/main/generated 위치에 QClass 저장
tasks.withType(JavaCompile) {
    options.generatedSourceOutputDirectory = file(querydslSrcDir)
}

//build.clean 시 QClass 모두 삭제
clean {
    delete file(querydslSrcDir)
}
//querydsl 설정 추가
  • implementation 'com.querydsl:querydsl-jpa' : Querydsl 을 사용하기 위한 라이브러리
  • implementation 'com.querydsl:querydsl-apt' : QClass 생성하기 위한 라이브러리
  • annotationProcessor 'com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa' : @Entity 클래스를 찾고, QClass 생성
  • annotationProcessor 'jakarta.persistence:jakarta.persistence-api' : java 에서 jakarta 로 변경됨.
  • annotationProcessor 'jakarta.annotation:jakarta.annotation-api' : java 에서 jakarta 로 변경됨.

이렇게 설정을 하고 build or compileJava 를 실행했을 때 QClass 들이 생성이 되면 성공한 것이다.

 

Querydsl 사용

QuerydslConfig 설정

설정 파일은 다음과 같이 설정을 해주었다.

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

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class QuerydslConfig {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

 

Entity 클래스 생성

이후 Repository 클래스들을 생성하여 실제로 데이터를 잘 조회해오는지 확인을 해봐야하는데, 이를 위해 Diary, BaseEntity 라는 객체를 생성했다.

Diary 는 일기를 작성하는 곳인데 프로젝트 주제가 '술일기' 였기에 술 마신 날을 기록하는 엔티티라고 볼 수 있고, 구체적인 컬럼은 삭제하고 초기 설계 내용으로 대신해보겠다.

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.TimeZone;

@Getter
@Entity
@NoArgsConstructor
@Table(name = "diary")
public class Diary {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private LocalDateTime diaryDt;
    private String drinkName;
    private Long drinkCount;
    private String content;
}

 

그리고 BaseEntity 는 생성일, 업데이트일과 같이 공통적으로 처리되는 날짜 관련 컬럼을 이곳에서 관리하도록 설계해, 각 엔티티가 BaseEntity 를 상속 받도록 구현했다.

아 그리고 추후에 테스트를 하면서 발견한 오류인데 createdAt 에 @Column(updatable = false) 를 하지 않으면 추후에 update 가 될 때 null 이 된다. 그렇기 때문에 이 설정을 꼭 해줘야한다.

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {

    @Column(updatable = false)
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

 

Repository 클래스 생성

자 그러면 이제 Entity 클래스도 생성이 되었으니 관련 Repository 를 생성하면 된다.

Repository 구조를 잡는 것에는 여러 방법이 있다. 필자가 선택한 방법은 Spring Data JPA Custom Repository 사용 방법이다. 

Spring Data JPA 공식 문서에 나와 있는 내용을 참고했는데, 요약을 하자면

  1. CustomRepository 인터페이스 객체를 생성한다.
  2. 1번에서 생성한 CustomRepository 인터페이스의 구현체를 생성한다.
  3. JPARepository 인터페이스 객체를 생성한다. 이때, JpaRepository 나 CrudRepository 를 상속받는데 이것 뿐만 아니라 1번에서 작성한 CustomRepository 인터페이스도 상속받는다.

위 내용으로 정리할 수 있다.

실제 코드로 구현을 해본다면 아래와 같다.

 

1번 CustomRepository 인터페이스 객체 생성

public interface DiaryQuerydslRepository {
}

 

2번 CustomRepository 인터페이스의 구현체 생성

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class DiaryQuerydslRepositoryImpl implements DiaryQuerydslRepository {

    private final JPAQueryFactory queryFactory;
}

 

3번 JPARepository 인터페이스 객체 생성

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

public interface DiaryJpaRepository extends JpaRepository<Diary, Long>, DiaryQuerydslRepository {
}

 

이렇게 구성하면 Repository 클래스 구성도 끝난 것이다! 아 여기서 주의할 것은, 구현체 파일명이 interface 명 + impl 이어야 한다. 이유는 DiaryJpaRepository 에 객체를 주입할 때 구현체 클래스를 삽입해주기 때문이다. 이 점만 주의해서 따라온다면 문제 없이 구성할 수 있을 것이다.

 

테스트

이제 마지막으로 테스트를 해보려고 한다. 일단 현재까지 구성한 상황에서 build 나 compildJava 를 실행한다면 다음과 같은 파일이 생긴 것을 확인할 수 있다.

 

그러면 비즈니스 로직을 구성할 때 Querydsl 을 어떻게 사용해야하는걸까? 그리고 실제로 Repository DI 는 어떻게 해야하는 걸까?

이에 대한 답변은, Service Layer 에서 DiaryJpaRepository 하나만 의존성 주입을 받아서 사용하면 된다는 것이다.

 

실제로 로직을 구현해서 잘 동작하는지 확인을 해보자

위 Diary 클래스에서 drinkName 으로 조회를 해오는데 이를 Querydsl 로 구현을 해야한다고 가정해보자

 

Controller

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/diary")
public class DiaryController {

    private final DiaryService diaryService;

    @GetMapping("/test/querydsl/{drinkName}")
    public ResponseEntity testQuerydsl(@PathVariable String drinkName) {
        Diary diary = diaryService.findByDrinkName(drinkName);
        return ResponseEntity.ok(diary);
    }
}

간단한 endpoint 를 만들어보았다. 

 

Service

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class DiaryService {

    private final DiaryJpaRepository diaryJpaRepository;

    public Diary findByDrinkName(String drinkName) {
        return diaryJpaRepository.findByDrinkName(drinkName);
    }
}

여기서 DiaryJpaRepository 만 주입을 받고, findByDrinkName 메소드를 생성하려고 할 때 아래와 같이 2개의 클래스에서 생성할 수 있다고 나오게 된다. 여기서 우리는 DiaryQuerydslRepository 를 선택한 뒤 그 구현체인 DiaryQuerydslRepositoryImpl 에서 메소드를 오버라이딩하여 설계하면 된다.

 

DiaryQuerydslRepositoryImpl

로직을 구현하면 아래와 같다.

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;

import static com.example.querydsl.QDiary.diary;

@RequiredArgsConstructor
public class DiaryQuerydslRepositoryImpl implements DiaryQuerydslRepository {

    private final JPAQueryFactory queryFactory;

    @Override
    public Diary findByDrinkName(String drinkName) {
        return queryFactory
                .selectFrom(diary)
                .where(diary.drinkName.eq(drinkName))
                .fetchFirst();
    }
}

 

실제로 잘 동작하는지 확인해보자

http 테스트 파일을 만들어서 실행해보니 원하는 데이터를 잘 조회해오는 것을 확인했다.

 

아래 블로그 내용을 참고해서 구현해보았는데, 더 구조가 복잡한 프로젝트를 설계할 때 기본이 되는 내용인 것 같아 정리해보았다.

 

 

참고

https://velog.io/@soyeon207/QueryDSL-Spring-Boot-%EC%97%90%EC%84%9C-QueryDSL-JPA-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

Spring Boot 에서 QueryDSL JPA 사용하기

QueryDSL 을 본격적으로 사용해보자

velog.io