티스토리 뷰

Database

[JPA] JOIN & N+1 문제

Nickolodeon 2022. 11. 26. 23:54

JOIN 의 역할

JPQL 에서 JOIN은 SQL 에서의 JOIN 과 차이가 없다. JOIN 은 항상 FROM 절 뒤에 붙는다. FROM 이 자바의 향상된 반복문에서 콜론 (:) 같은 역할이라고 생각하면 된다. 이 때 JOIN 은 반복하는 구간을 특정하는 역할을 한다. 예를 들어, 다음 두 개는 같은 역할을 하는 쿼리문들이다.

SELECT c1, c2 FROM Country c1 INNER JOIN c1.neighbors c2
SELECT c1, c2 FROM Country c1, Country c2
WHERE c2 MEMBER OF c1.neighbors

즉, INNER JOIN 은 두 항목을 조회하되, 하나는 연관관계에서 주인인 테이블의 항목이고, 다른 하나는 주인에 의해 매핑된 테이블의 항목으로 제한시켜 조회하는 것이다. 매핑된 테이블 항목이 존재하지 않으면 사용할 수 없다.

위의 두 쿼리문들 중에서는 JOIN 을 사용하는 것을 더 권장한다. 짧고 간결해지는 것 외에, DBMS 최적화에 의지하지 않아도 이미 바깥 반복문 (outer loop) 과 안쪽 반복문 (inner loop) 을 사용하는 올바르고 효율적인 방법이기 때문이다.

JOIN, LEFT / RIGHT OUTER JOIN, JOIN FETCH 의 차이

자, 이제 범위를 좀 더 넓혀서 JPQL 에서 사용되는 다양한 JOIN의 종류들에 대해 알아보자. 이름에 따라 역할이 상이하다.

(INNER) JOIN LEFT (OUTER) JOIN RIGHT (OUTER) JOIN JOIN FETCH
두 테이블 중 하나의 테이블의 내용을 다른 테이블 항목들과의 연관관계를 기준으로 조회할 때 사용된다.  왼쪽에 명시된 테이블의
내용을 오른쪽의 테이블에 연관관계가 있는 내용과
함께 조회할 때 사용된다.
오른쪽에 명시된 테이블의 내용을 왼쪽의 테이블에 연관관계가 있는 내용과 함께 조회할 때 사용된다. 하나의 테이블을 조회할 때
연관관계를 가진 테이블을
함께 데이터베이스에서
가져오고 싶을 때 사용된다.
매칭되는 Inner variant 가
없는 Outer variant 를 무시

예) Country 과 Capital 을
INNER JOIN 하면 Captial 이
없는 Country는 무시
INNER JOIN 과 다르게,
매칭되는 Inner variant 가 없다면 NULL 값을 할당

예) Country 중 Capital 이 없다면 무시하지 않고
Capital 에 NULL 을 할당
다른 JOIN들과는 다르게 변수가 아니라 엔티티 클래스명만 명시

JOIN FETCH 의 예시

Country 라는 엔티티 클래스에 Capital 이라는 변수가 있고 이 변수는 @OneToOne 으로 Capital 엔티티 클래스와 연관관계를 나타내고 있다고 가정한다. 이 상황에서 아래 쿼리문을 실행한다:

SELECT c FROM Country c

이 때 Country 클래스의 변수들을 데이터베이스에서 모두 조회하지만, Capital 객체 변수는 주소값만 나올 것이다. 이 쿼리문만으로는 데이터베이스에서 Capital 테이블은 가져오지 못하기 때문이다. 그래서 Country 들의 Capital 을 구하려면 아래와 같이 해야 한다:

  List<Country> results = query.getResultList();
  for (Country c : results) {
      System.out.println(c.getName() + " => " + c.getCapital().getName());
  }

이렇게 되면 Country 내의 Capital 객체를 반복문 내의 리스트를 돌며 한 번에 하나씩 가져온다. 한 번에 모두 가져온 다음에 getName 으로 이름을 조회하는 방법이 효율적인데 말이다. 이런 상황에서 JOIN FETCH 를 사용할 수 있다.

 

아래 쿼리문은 위의 작업을 한 줄로 해결해준다:

SELECT c FROM Country c JOIN FETCH c.capital

 

이 쿼리문 한 줄로 Country 내의 captial 을 모두 한 번에 가져온다. 하나씩 가져오지도, Country 를 가져온 후에 Capital 을 한 번 더 가져오지도 않기 때문에 훨씬 효율적이다.

N+1 문제

N+1 문제는 JPQL 에서 1개의 쿼리를 생성해야 되는 상황에서 N 개의 쿼리를 더 생성하는 문제를 일컫는다.
원인은 Fetchtype.EAGER 상태에서 JOIN FETCH 을 사용하지 않는 것이다.

가장 주요 원인은 연관관계를 설정할 때 붙인 어노테이션에서 fetch = Fetchtype.EAGER 을 명시해준 상태에서 JOIN FETCH 를 사용하지 않는 것이다. Fetchtype.EAGER 은 @ManyToOne 이나 @OneToOne 연관관계를 가진 엔티티에 디폴트로 설정되어 있는 Fetch 방식인데, 모든 것을 다 가져오고 본다. 다음과 같은 엔티티 클래스들이 있다고 가정하자:

@Entity
@Table(name = "country")
public class Country {
 
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
 
    private String name;
 
    //Getter와 Setter 들은 생략했다.
}
 
@Entity
@Table(name = "city")
public class City {
 
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
 
    @ManyToOne
    private Country country;
 
    private Long phoneCode;
 
    //Getter와 Setter 들은 생략했다.
}

여기서 City 엔티티를 조회하고자 하면, 여러 개의 City 객체를 하나 조회할 때마다 City 와 연관관계에 있는 country 항목들이 모두 조회된다. 즉 City 객체가 10개이면 10 x (country 테이블 내의 항목 수) 만큼 조회된다. 실제로 필요한 항목은 그 때마다의 City 객체와 연관 있는 country 테이블 항목 하나 뿐인데, 그 하나를 찾기 전에 일단 모든 연관관계가 있는 항목들을 모두 불러오기 때문에 매우 비효율적이다. 실제로 한 번 조회해보자. API는 다음과 같다:

    @PostMapping("/test")
    public ResponseEntity<List<City>> getAllCities() {
        return ResponseEntity.ok().body(cityService.findAllCities());
    }

다음과 같은 EntityManager 를 JpaRepository 를 상속받도록 해서 생성한다:

import com.springboot.relationship.domain.entity.City;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CityEntityManager extends JpaRepository<City, Long> {

}

서비스에서 컨트롤러에서 사용할 메서드를 정의해 두었다:

import com.springboot.relationship.domain.entity.City;
import com.springboot.relationship.repository.CityEntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class CityService {

    private final CityEntityManager cityEntityManager;

    public List<City> findAllCities() {
        return cityEntityManager.findAll();
    }
}

 

다음은 Fetchtype.EAGER 상태에서 로그에 기록된 Hibernate 가 생성한 쿼리이다:

Hibernate: select city0_.id as id1_2_, city0_.country_id as country_3_2_, city0_.phone_code as phone_co2_2_ from city city0_
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?
Hibernate: select country0_.id as id1_3_0_, country0_.name as name2_3_0_ from country country0_ where country0_.id=?

이번에는 City 클래스의 Country 변수 위 어노테이션을 @ManyToOne(fetch = Fetchtype.LAZY) 로 바꾼 뒤 실행해본다:

 

InvalidDefinitionException 이 발생해버렸다. 이하 생략한다. 해결 방법은 이후에 포스팅하도록 하겠다.

 

어쨌든 이것이 Fetchtype.LAZY 를 쓰는 이유이다. 연관관계가 있는 항목들을 미리 모두 불러오지 않고, 미뤄두었다가 나중에 쿼리문을 통해 그 항목들을 실제로 조회할 때에만 불러오는 것이다. 어쨌든, JPQL 에서는 이 문제를 JOIN FETCH 를 통해 해결한다. 필요한 항목들을 한 번에 불러놓고 찾을 수 있게 되기 때문이다.

출처

https://www.objectdb.com/java/jpa/query/jpql/from

 

FROM clause (JPQL / Criteria API)

FROM clause (JPQL / Criteria API) The FROM clause declares query identification variables that represent iteration over objects in the database. A query identification variable is similar to a variable of a Java enhanced for loop in a program, since both a

www.objectdb.com

https://stackoverflow.com/questions/97197/what-is-the-n1-selects-problem-in-orm-object-relational-mapping

 

What is the "N+1 selects problem" in ORM (Object-Relational Mapping)?

The "N+1 selects problem" is generally stated as a problem in Object-Relational mapping (ORM) discussions, and I understand that it has something to do with having to make a lot of database queries...

stackoverflow.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함