JPA를 사용하다 보면 join을 할 때가 많아진다. join을 어떠한 방법으로 하느냐에 따라서 수행되는 쿼리가 달라지고 성능에 문제가 발생하는 경우도 종종있다.
그래서 다양한 방식의 join 방식을 알아보고 방식에 따라 작업을 진행해 보자.
우선 사용될 entity 두 개를 설명하면 다음과 같다.
@Getter
@Entity
@Table(name = "wedul_classes")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Builder
public class WedulClasses extends CommonEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long wedulClassesId;
@OneToMany(mappedBy = "wedulClasses", fetch = FetchType.LAZY)
private Set<WedulStudent> wedulStudentList = new LinkedHashSet<>();
private String classesName;
private String classesAddr;
}
@Getter
@Entity
@Table(name = "wedul_student")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor
@Builder
public class WedulStudent extends CommonEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long wedulStudentId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "wedul_classes_id")
@JsonBackReference
private WedulClasses wedulClasses;
private String studentName;
private int studentAge;
@Enumerated(value = EnumType.STRING)
private StudentType studentType;
}
이 두 엔티티는 class와 student의 관계로 1대 N의 관계를 가지고 있다.
이 과정에서 사용될 데이터는 임의로 추가했고 다음과 같다.
wedul_classe 테이블의 데이터
wedul_student 테이블의 데이터
사용 쿼리 툴) tadpole docker version
그럼 이 entity를 이용해서 데이터를 조인하여 조회해보자.
1. 단순 조회
우선 첫 번째로 JpaRepository 인터페이스 사용 시 기본적으로 제공하는 findAll을 사용해보자.
@Repository
public interface WedulClassesRepository extends JpaRepository<WedulClasses, Long> {
Optional<WedulClasses> findByClassesName(String classesName);
}
이를 사용하여 데이터를 조회해보면 사용 되는 쿼리는 다음과 같다.
-- classes 목록을 조회하는 쿼리
select
wedulclass0_.wedul_classes_id as wedul_cl1_0_
,wedulclass0_.create_at as create_a2_0_
,wedulclass0_.update_at as update_a3_0_
,wedulclass0_.classes_addr as classes_4_0_
,wedulclass0_.classes_name as classes_5_0_
from
wedul_classes wedulclass0_
;
-- 아래 쿼리들은 wedul_classes_id 개수별로 조회되는 쿼리
select
wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
,wedulstude0_.wedul_student_id as wedul_st1_1_0_
,wedulstude0_.wedul_student_id as wedul_st1_1_1_
,wedulstude0_.create_at as create_a2_1_1_
,wedulstude0_.update_at as update_a3_1_1_
,wedulstude0_.student_age as student_4_1_1_
,wedulstude0_.student_name as student_5_1_1_
,wedulstude0_.student_type as student_6_1_1_
,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
from
wedul_student wedulstude0_
where
wedulstude0_.wedul_classes_id = ?
;
select
wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
,wedulstude0_.wedul_student_id as wedul_st1_1_0_
,wedulstude0_.wedul_student_id as wedul_st1_1_1_
,wedulstude0_.create_at as create_a2_1_1_
,wedulstude0_.update_at as update_a3_1_1_
,wedulstude0_.student_age as student_4_1_1_
,wedulstude0_.student_name as student_5_1_1_
,wedulstude0_.student_type as student_6_1_1_
,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
from
wedul_student wedulstude0_
where
wedulstude0_.wedul_classes_id = ?
;
select
wedulstude0_.wedul_classes_id as wedul_cl7_1_0_
,wedulstude0_.wedul_student_id as wedul_st1_1_0_
,wedulstude0_.wedul_student_id as wedul_st1_1_1_
,wedulstude0_.create_at as create_a2_1_1_
,wedulstude0_.update_at as update_a3_1_1_
,wedulstude0_.student_age as student_4_1_1_
,wedulstude0_.student_name as student_5_1_1_
,wedulstude0_.student_type as student_6_1_1_
,wedulstude0_.wedul_classes_id as wedul_cl7_1_1_
from
wedul_student wedulstude0_
where
wedulstude0_.wedul_classes_id = ?
;
쿼리를 자세히 보면 알겠지만 wedul_classes를 조회하는 쿼리와 그 wedul_classes 개수만큼 쿼리가 실행되는것을 볼 수 있다.
많이 들어 봤을 법한 N+1 문제가 발생한 것이다.
이 방식으로 쿼리 수행 시 N번의 쿼리가 발생해야 하기에 데이터 수만큼 쿼리가 실행되는 안좋은 부담을 안고 가야해서 좋지 않다.
2. left fetch join
위의 1번의 N+1 문제 해결로 고안된 방법 중 하나가 fetch join이다. 나는 left join을 하고자 하기에 left fetch join을 시도해보자. 우선 사용된 코드는 다음과 같다.
@Repository
public interface WedulClassesRepository extends JpaRepository<WedulClasses, Long> {
@Query(value = "select DISTINCT c from WedulClasses c left join fetch c.wedulStudentList")
List<WedulClasses> findAllWithStudent();
}
distinct가 붙은 이유는 카티션곱에 의해서 여러개의 결과값이 발생해 버리기 때문에 추가하였다.
그럼 사용된 쿼리도 확인해보자.
select
distinct wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
,wedulstude1_.wedul_student_id as wedul_st1_1_1_
,wedulclass0_.create_at as create_a2_0_0_
,wedulclass0_.update_at as update_a3_0_0_
,wedulclass0_.classes_addr as classes_4_0_0_
,wedulclass0_.classes_name as classes_5_0_0_
,wedulstude1_.create_at as create_a2_1_1_
,wedulstude1_.update_at as update_a3_1_1_
,wedulstude1_.student_age as student_4_1_1_
,wedulstude1_.student_name as student_5_1_1_
,wedulstude1_.student_type as student_6_1_1_
,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
,wedulstude1_.wedul_student_id as wedul_st1_1_0__
from
wedul_classes wedulclass0_
left outer join wedul_student wedulstude1_
on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id
left join을 해서 한번에 데이터를 가져올 수 있는 걸 확인 할 수 있지만 아쉽게도 Lazy로 데이터를 가져오지 못하고 Eager로 가져와야 한다.
3. EntityGraph
이제 3번째 방식으로 entity graph를 사용하여 실행시켜보자. 코드는 아래와 같다.
@EntityGraph(attributePaths = "wedulStudentList")
@Query("select c from WedulClasses c")
Page<WedulClasses> findEntityGraph(Pageable pageable);
실행되는 쿼리는 다음과 같아서 2번과 동일하다. (page를 사용한 것만 차이)
select
wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
,wedulstude1_.wedul_student_id as wedul_st1_1_1_
,wedulclass0_.create_at as create_a2_0_0_
,wedulclass0_.update_at as update_a3_0_0_
,wedulclass0_.classes_addr as classes_4_0_0_
,wedulclass0_.classes_name as classes_5_0_0_
,wedulstude1_.create_at as create_a2_1_1_
,wedulstude1_.update_at as update_a3_1_1_
,wedulstude1_.student_age as student_4_1_1_
,wedulstude1_.student_name as student_5_1_1_
,wedulstude1_.student_type as student_6_1_1_
,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
,wedulstude1_.wedul_student_id as wedul_st1_1_0__
from
wedul_classes wedulclass0_
left outer join wedul_student wedulstude1_
on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id
order by
wedulclass0_.update_at desc;
4. QueryDSL
Querydsl은 정적 타입을 이용해서 SQL과 같은 쿼리를 사용할 수 있도록 해주는 프레임워크로 HQL쿼리를 실행하게 도와준다.
설정 방식은 gradle 5 기준으로 다음과 같다.
plugins {
id 'org.springframework.boot' version '2.2.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
id 'java'
}
group = 'com.wedul'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compile group: "org.flywaydb", name: "flyway-core", version: '5.2.4'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.0'
annotationProcessor 'org.projectlombok:lombok'
testCompile group: 'org.mockito', name: 'mockito-all', version:'1.9.5'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// query dsl
compile("com.querydsl:querydsl-apt")
compile("com.querydsl:querydsl-jpa")
}
// querydsl 적용
def querydslSrcDir = 'src/main/generated'
querydsl {
library = "com.querydsl:querydsl-apt"
jpa = true
querydslSourcesDir = querydslSrcDir
}
compileQuerydsl{
options.annotationProcessorPath = configurations.querydsl
}
configurations {
querydsl.extendsFrom compileClasspath
}
sourceSets {
main {
java {
srcDirs = ['src/main/java', querydslSrcDir]
}
}
}
그리고 QueryDsl 사용을 위해 QueryDslRepositorySupport를 상속받아서 사용할 수 있는데 마지막에 distinct를 사용한 것은 2번 fetch 조인의 이유와 동일하다.
@Repository
public class WedulClassesQueryDsl extends QuerydslRepositorySupport {
public WedulClassesQueryDsl() {
super(WedulClasses.class);
}
public List<WedulClasses> findAllWithStudent() {
QWedulClasses wedulClasses = QWedulClasses.wedulClasses;
QWedulStudent wedulStudent = QWedulStudent.wedulStudent;
return from(wedulClasses)
.leftJoin(wedulClasses.wedulStudentList, wedulStudent)
.fetchJoin()
.distinct()
.fetch();
}
}
그럼 마찬가지로 실행되는 쿼리를 확인해보자.
select
distinct wedulclass0_.wedul_classes_id as wedul_cl1_0_0_
,wedulstude1_.wedul_student_id as wedul_st1_1_1_
,wedulclass0_.create_at as create_a2_0_0_
,wedulclass0_.update_at as update_a3_0_0_
,wedulclass0_.classes_addr as classes_4_0_0_
,wedulclass0_.classes_name as classes_5_0_0_
,wedulstude1_.create_at as create_a2_1_1_
,wedulstude1_.update_at as update_a3_1_1_
,wedulstude1_.student_age as student_4_1_1_
,wedulstude1_.student_name as student_5_1_1_
,wedulstude1_.student_type as student_6_1_1_
,wedulstude1_.wedul_classes_id as wedul_cl7_1_1_
,wedulstude1_.wedul_classes_id as wedul_cl7_1_0__
,wedulstude1_.wedul_student_id as wedul_st1_1_0__
from
wedul_classes wedulclass0_
left outer join wedul_student wedulstude1_
on wedulclass0_.wedul_classes_id = wedulstude1_.wedul_classes_id
애도 2번, 3번과 동일한 쿼리가 작성되는 걸 확인할 수 있다.
기본적으로 단순하게 다대일 데이터를 가져오려고 하면 N+1 문제가 발생할 수 있기 때문에 조심해야하고 이를 해결하기 위해서는 다양한 방식의 문제 해결 방식이 있는걸 확인할 수 있었다.
무엇이 가장 좋은지는 본인이 판단하거나 상황에 맞게 사용하면 좋을 거 같다.
'web > Spring' 카테고리의 다른 글
spring cloud resilience4j 사용시 CircuitBreakerConfiguration 에러 (0) | 2020.02.23 |
---|---|
RestHighLevelClient를 사용하여 search after 기능 구현하기 (1) | 2019.11.14 |
데이터 베이스 버전 컨트롤 Flyway (0) | 2019.09.28 |
Redis에서 Pub/Sub 방식 소개 및 Spring Boot에서 구현해보기 (2) | 2019.08.21 |
Spring5 리액티브 스트림 정리 및 api 전달 방식 정리 (0) | 2019.08.16 |