๐ ๊ฐ์
๋ฐ๋ธ์ฝ์ค 4๊ธฐ์์ ๋ด์ฌ ํ๋ซํผ์ธ Anifriends ์ ๊ฐ๋ฐํ๋ฉด์ ์กฐํ ์๋น์ค์์ N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ์๋ค. N+1 ๋ฌธ์ ๋ ๋ฌด์์ด๊ณ ํด๊ฒฐ ๋ฐฉ์์ ์ด๋ค ๊ฒ๋ค์ด ์๋์ง ํ์ตํด ๋ณด๋ฉฐ ์ ์ฉํด ๋ณด์.
๐ N+1 ๋ฌธ์ ๋?
JPA ์ ์ฐ๊ด ๊ด๊ณ๊ฐ ์ค์ ๋ ์ํฐํฐ๋ฅผ ์กฐํํ ๊ฒฝ์ฐ ํด๋น ์ํฐํฐ์ ์ฐ๊ด ๊ด๊ณ๋ฅผ ๊ฐ๋ ์ํฐํฐ๋ฅผ ์กฐํํ๊ธฐ ์ํด N ๊ฐ์ ์ถ๊ฐ์ ์ธ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ๋ฌธ์ ์ด๋ค. ์ฆ, 1๋ฒ์ ์ฟผ๋ฆฌ๋ฅผ ๋ ๋ ธ์ ๋ N ๊ฐ์ ์ถ๊ฐ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ ๋ฌธ์ ์ด๋ค. ์ด๋ ์๋์น ์์ ์ฟผ๋ฆฌ๋ฅผ ๋ฐ์์์ผ ์ดํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ ์ ํ๋ก ์ด์ด์ง ์ ์๋ค.
โ N+1 ๋ฌธ์ ๊ฐ์ ํ๊ธฐ
๋ด๊ฐ ๋งก์ ๋ถ๋ถ์ ๋ณดํธ ๋๋ฌผ ์กฐํ์ด๋ค. ๋ณดํธ ๋๋ฌผ ๊ด๋ จ ERD ๋ ๋ค์๊ณผ ๊ฐ๋ค.
- ๋ณดํธ ๋๋ฌผ & ๋ณดํธ ๋๋ฌผ ์ด๋ฏธ์ง : oneToMany
- ๋ณดํธ๋๋ฌผ & ๋ณดํธ์ : manyToOne
- ๋ณดํธ์ & ๋ณดํธ์ ์ด๋ฏธ์ง : OneToOne
๋ณดํธ ๋๋ฌผ ๋จ์ผ ์กฐํ, ๋ณดํธ์์ ๋ณดํธ ๋๋ฌผ ๋ชฉ๋ก ์กฐํ, ๋ด์ฌ์์ ๋ณดํธ ๋๋ฌผ ๋ชฉ๋ก ์กฐํ์์ N+1 ๋ฌธ์ ๊ฐ ๋ฐ์ํ์๋ค. ํ๋์ฉ ๊ฐ์ ํด ๋ณด์.
๋ณดํธ ๋๋ฌผ ๋จ์ผ ์กฐํ
๋ณดํธ ๋๋ฌผ๊ณผ ๋ณดํธ ๋๋ฌผ ์ด๋ฏธ์ง๋ oneToMany ์ฐ๊ด ๊ด๊ณ๋ฅผ ๊ฐ๋๋ค.
@Entity
public class Animal {
...
@OneToMany(mappedBy = "animal", fetch = FetchType.LAZY, ...)
private List<AnimalImage> images = new ArrayList<>();
}
๋ณดํธ ๋๋ฌผ์ ๋จ์ผ ์กฐํํ ๊ฒฝ์ฐ ๋ค์๊ณผ ๊ฐ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค.
Hibernate:
select
a1_0.animal_id,
...
from
animal a1_0
where
a1_0.animal_id=?
Hibernate:
select
i1_0.animal_id,
i1_0.animal_image_id,
...
from
animal_image i1_0
where
i1_0.animal_id=?
- animal ์ ๋จ์ผ ์กฐํํ๋ ์ฟผ๋ฆฌ 1๋ฒ
- animal_image ๋ชฉ๋ก์ ์กฐํํ๋ ์ฟผ๋ฆฌ 1๋ฒ
fetch join ์ ์ฌ์ฉํด์ ๊ฐ์ ํด ๋ณด์.
๐ fetch join ์ด๋?
์ฐ๊ด ๊ด๊ณ๋ฅผ ๊ฐ๋ ์ํฐํฐ๋ ์ปฌ๋ ์ ์ join ํ์ฌ ํ ๋ฒ์ ๋ถ๋ฌ์ค๋ ๋ฐฉ๋ฒ์ด๋ค. JPQL ์์ ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด ์ ๊ณตํ๋ join ์ผ๋ก ์ผ๋ฐ SQL join ๊ณผ๋ ๋ค๋ฅด๋ค.(SQL join ์ ์ฐ๊ด๋ ์ํฐํฐ๋ฅผ ํจ๊ป ์กฐํํด์ค์ง ์๋๋ค.)
@Query("select a from Animal a "
+ "join fetch a.images "
+ "where a.animalId = :animalId")
Optional<Animal> findByAnimalIdWithImages(@Param("animalId") Long animalId);
์ด์ ๋ค์ ์กฐํ ๋ก์ง์ ์คํํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ๋ณดํธ ๋๋ฌผ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ฒ ๋๋ค.
Hibernate:
select
a1_0.animal_id,
...
from
animal a1_0
join
animal_image i1_0
on a1_0.animal_id=i1_0.animal_id
where
a1_0.animal_id=?
๋ณดํธ์์ ๋ณดํธ ๋๋ฌผ ๋ชฉ๋ก ์กฐํ
๋ณดํธ์๋ ์์ ์ด ๋ฑ๋กํ ๋ณดํธ ๋๋ฌผ ๋ชฉ๋ก๋ง ์กฐํํ ์ ์์ผ๋ฉฐ ๋ณดํธ ๋๋ฌผ ๋ชฉ๋ก ์กฐํ ์ ๋ค์๊ณผ ๊ฐ์ด ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค.
Hibernate:
select
a1_0.animal_id,
...
from
animal a1_0
where
a1_0.shelter_id=?
order by
a1_0.created_at desc
offset
? rows
fetch
first ? rows only
Hibernate:
select
i1_0.animal_id,
i1_0.animal_image_id,
...
from
animal_image i1_0
where
i1_0.animal_id=?
Hibernate:
select
i1_0.animal_id,
i1_0.animal_image_id,
...
from
animal_image i1_0
where
i1_0.animal_id=?
Hibernate:
select
i1_0.animal_id,
i1_0.animal_image_id,
...
from
animal_image i1_0
where
i1_0.animal_id=?
....
- shelter_id ์ ํด๋นํ๋ animal ๋ชฉ๋ก์ ์กฐํํ๋ ์ฟผ๋ฆฌ 1๋ฒ
- ๊ฐ animal_id ์ ํด๋นํ๋ animal_image ๋ชฉ๋ก์ ์กฐํํ๋ ์ฟผ๋ฆฌ N๋ฒ
์ด N + 1๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค. ์์ธ ์กฐํ์ ๋ง์ฐฌ๊ฐ์ง๋ก fetch join ์ ์ฌ์ฉํด์ ๊ฐ์ ํด ๋ณด์.
List<Animal> animals = query.select(animal)
.from(animal)
.join(animal.images).fetchJoin()
.where(
animal.shelter.shelterId.eq(shelterId),
...
)
.orderBy(animal.createdAt.desc())
.limit(pageable.getPageSize())
.offset(paeable.getOffset())
.fetch();
์ด์ ๋ค์ ๋ณดํธ ๋๋ฌผ ๋ชฉ๋ก์ ์กฐํํด ๋ณด์.
Hibernate:
select
a1_0.animal_id,
...
from
animal a1_0
join
animal_image i1_0
on a1_0.animal_id=i1_0.animal_id
where
a1_0.shelter_id=?
order by
a1_0.created_at desc
์ฅ? offset ๊ณผ limit ์ด ์ ํ ๋์ํ์ง ์๊ณ ์๋ค. ๋ํ ๋ค์๊ณผ ๊ฐ์ด ํ์ด๋ฒ๋ค์ดํธ WARN ์ด ๋ฐ์ํ๋ค.
db ์ ์ ์ฅ๋์ด ์๋ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์์ผ๋ก ๊ฐ์ ธ์์ ํ์ด๋ฒ๋ค์ดํธ๊ฐ ๊ฒฝ๊ณ ๋ฅผ ์ค ๊ฒ์ด๋ค. offset ๊ณผ limit ์ด ๋์ํ์ง ์๋ ์ด์ ๋ ๋ฌด์์ผ๊น?
oneToMany ๊ด๊ณ์์ fetch join ์ ํ๊ฒ ๋๋ฉด Many ์ ๊ฐ์๋งํผ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์จ๋ค. ๊ฐ๋ฐ์๊ฐ ์ํ๋ ํ์ด์ง์ ๊ธฐ์ค์ Many ๊ฐ ์๋ One ์ด๋ค. ์ฆ, ๋ณดํธ ๋๋ฌผ ์ด๋ฏธ์ง๊ฐ ์๋ ๋ณดํธ ๋๋ฌผ์ด ๊ธฐ์ค์ด ๋์ด์ผ ํ๋ค.
Hibernate ๋ oneToMany ๊ด๊ณ์์ fetch join ์ ํ ๊ฒฝ์ฐ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ db ์์ ์กฐํํ์ฌ ๋ฉ๋ชจ๋ฆฌ์ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ์ฌ๋ ค๋๊ณ ํ์ด์ง ์ฒ๋ฆฌ๋ฅผ ํ๊ณ WARN ๋ก๊ทธ๋ฅผ ๋จ๊ธฐ๋ ๋ฐฉ๋ฒ์ ์ ํํ๋ค. ์ด ๊ฒฝ์ฐ ์ํํ ์ํฉ์ด ๋ฐ์ํ ์ ์๋ค. ๋ง์ฝ ๋ณดํธ ๋๋ฌผ์ด 100๋ง๊ฐ ๋๋ ๊ทธ ์ด์์ด db ์ ์ ์ฅ๋์ด ์๋ค๋ฉด ํ์ด์ง์ฒ๋ฆฌ๋ฅผ ํ์ง ์๊ณ ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ฌ๋ฆฌ๊ฒ ๋๋ฏ๋ก ์ง์ฐ์๊ฐ ์ฆ๊ฐ๋ ๋ฌผ๋ก ๋ฉ๋ชจ๋ฆฌ ๋ถ์กฑ์ผ๋ก ์ธํด OutOfMemoryError ๊ฐ ๋ฐ์ํ ์ํ์ด ์๋ค.
๊ทธ๋ ๋ค๋ฉด ๋ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ธ BatchSize ๋ฅผ ์ค์ ํด ๋ณด์.
๐ BatchSize ๋?
์ฐ๊ด ๊ด๊ณ๋ฅผ ๊ฐ๋ ์ํฐํฐ๋ ์ปฌ๋ ์ ์ ์กฐํํ ๋ BatchSize ๋งํผ where in ์ ์ ์ฌ์ฉํ์ฌ ์กฐํํด ์ค๋ ๋ฐฉ์์ด๋ค.
@OneToMany(mappedBy = "animal", fetch = FetchType.LAZY, ... )
@BatchSize(size = 100)
private List<AnimalImage> images = new ArrayList<>();
๋ณ๊ฒฝ๋๋ ์ฟผ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ๋ค.
Hibernate:
select
a1_0.animal_id,
...
from
animal a1_0
where
a1_0.shelter_id=?
order by
a1_0.created_at desc
offset
? rows
fetch
first ? row only
Hibernate:
select
i1_0.animal_id,
i1_0.animal_image_id,
...
from
animal_image i1_0
where
i1_0.animal_id in (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
animal_image ๋ฅผ join ํด์ค๋ ๊ฒ์ด ์๋ ์ค์ ํ BatchSize ๋งํผ where in ๋ฌธ์ ํตํด ๊ฐ์ ธ์ค๊ฒ ๋๋ค.
๋ด์ฌ์์ ๋ณดํธ ๋๋ฌผ ๋ชฉ๋ก ์กฐํ
๋ณดํธ ๋๋ฌผ๊ณผ ๋ณดํธ์๋ manyToOne ์ฐ๊ด๊ด๊ณ๋ฅผ ๊ฐ์ผ๋ฉฐ, ๋ณดํธ์์ ๋ณดํธ์ ์ด๋ฏธ์ง๋ oneToOne ์ฐ๊ด๊ด๊ณ๋ฅผ ๊ฐ๋๋ค.
@Entity
public class Animal {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "shelter_id")
private Shelter shelter;
}
@Entity
public class Shelter {
...
@OneToOne(mappedBy = "shelter", fetch = FetchType.LAZY, ...)
private ShelterImage image;
}
๋ด์ฌ์๋ ๋ฑ๋ก๋ ๋ชจ๋ ๋ณดํธ ๋๋ฌผ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ผ๋ฉฐ ๋ฐ์๋๋ ์ฟผ๋ฆฌ๋ ๋ค์๊ณผ ๊ฐ๋ค.
Hibernate:
select
a1_0.animal_id,
...
from
animal a1_0
where
a1_0.shelter_id=?
order by
a1_0.created_at desc
offset
? rows
fetch
first ? row only
Hibernate:
select
s1_0.shelter_id,
...
from
shelter s1_0
where
s1_0.shelter_id=?
Hibernate:
select
s1_0.shelter_image_id,
...
from
shelter_image s1_0
where
s1_0.shelter_id=?
...
Hibernate:
select
i1_0.animal_id,
i1_0.animal_image_id,
...
from
animal_image i1_0
where
i1_0.animal_id=?
...
- animal ๋ชฉ๋ก์ ์กฐํํ๋ ์ฟผ๋ฆฌ 1๋ฒ
- ๊ฐ animal ์ animal_image ๋ชฉ๋ก์ ์กฐํํ๋ ์ฟผ๋ฆฌ N๋ฒ
- ๊ฐ animal ์ shelter ๋ฅผ ์กฐํํ๋ ์ฟผ๋ฆฌ N ๋ฒ
- ๊ฐ shelter ์ shelter_image ๋ชฉ๋ก์ ์กฐํํ๋ ์ฟผ๋ฆฌ N๋ฒ
์ด N + N + N + 1๋ฒ์ ์ฟผ๋ฆฌ๊ฐ ๋ฐ์ํ๋ค.
๊ฐ์ ํ๊ธฐ์ ์์ ํ ๊ฐ์ง ์๋ฌธ์ ์ด ์๋ค. shelter_image ๋ ์ด๋์์๋ ์ ๊ทผํ๊ณ ์์ง ์์๋ฐ fetchType ์ด LAZY ์์๋ ๋ถ๊ตฌํ๊ณ ์ฆ์ ๋ก๋ฉ์ด ๋๊ณ ์๋ ์ด์ ๋ ๋ฌด์์ผ๊น?
๊ทธ ์ด์ ๋ JPA ์์์ ์ง์ฐ ๋ก๋ฉ์ ํ๋ก์๋ก ๋์ํ๊ธฐ ๋๋ฌธ์ด๋ค.
db ์์์ FK ๋ shelter_image ๊ฐ ๊ฐ๊ณ ์๋ค. ๋ง์ฝ ์ฐ๊ด๊ด๊ณ ์ฃผ์ธ์ธ shelter_image๋ฅผ ์กฐํํ๋ค๋ฉด shelter ๋ ํ๋ก์๋ก ์ ์ฅ๋ ๊ฒ์ด๋ค. ํ์ง๋ง, shelter ๋ฅผ ์กฐํํ๋ค๋ฉด shelter ๋ shelter_image_id ๋ฅผ ๊ฐ๊ณ ์์ง ์๊ธฐ ๋๋ฌธ์ ์กด์ฌํ์ง ์์ ๊ฒ์ ํ๋ก์๋ก ๊ฐ์ ์ ์์ผ๋ฏ๋ก ์ฆ์ ๋ก๋ฉ์ ํ๊ฒ ๋๋ค.
์ฆ, oneToOne ์ฐ๊ด๊ด๊ณ์์ ์ฐ๊ด๊ด๊ณ ์ฃผ์ธ์ด ์๋ ๊ณณ์์ ์กฐํ๋ฅผ ํ ๊ฒฝ์ฐ FK ๋ฅผ ๊ฐ๊ณ ์์ง ์์ผ๋ฏ๋ก ์ฆ์๋ก๋ฉ์ผ๋ก ๋์ํ๊ฒ ๋๋ ๊ฒ์ด๋ค.
๋ ํ ๊ฐ์ง ๋ฌธ์ ๋ ๋ด์ฌ์์ ๋ณดํธ๋๋ฌผ ์กฐํ ๋ก์ง์ ๊ฒฝ์ฐ ํ์ํ ๋ฐ์ดํฐ๋ animal_id, animal_name, animal_imageUrl, created_at, shelter_name, shelter_address ์ด 6๊ฐ์ธ๋ฐ 30๊ฐ ์ด์์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ณ ์๋ค. ๋ถํ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ๊ฒ์ ์ฑ๋ฅ ์ ํ๋ฅผ ์ผ์ผํฌ ์ ์๋ค. dto projection ์ ์ฌ์ฉํ์ฌ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค์.
๐ dto projection ์ด๋?
select ์ ์ ๋์์ ์ง์ ํ์ฌ ์ํ๋ ๊ฐ๋ง ์กฐํํด ์ค๋ ๊ฒ์ ์๋ฏธํ๋ค. dto projection ์ ์ฌ์ฉํ๋ฉด ๋ถํ์ํ ๋ฐ์ดํฐ๋ฅผ ์กฐํํ์ง ์๊ณ ํ์ํ ๋ฐ์ดํฐ๋ง ์กฐํํด ์ฌ ์ ์๋ค.
@Getter
public class FindAnimalsResult {
private final Long animalId;
private final String animalName;
private final LocalDateTime createdAt;
private final String shelterName;
private final String shelterAddress;
private final String animalImageUrl;
@QueryProjection
public FindAnimalsResult(
Long animalId,
String animalName,
LocalDateTime createdAt,
String shelterName,
String shelterAddress,
String animalImageUrl
) {
this.animalId = animalId;
this.animalName = animalName;
this.createdAt = createdAt;
this.shelterName = shelterName;
this.shelterAddress = shelterAddress;
this.animalImageUrl = animalImageUrl;
}
}
List<FindAnimalsResult> animals = query.select(new QFindAnimalsResult(
animal.animalId,
animal.name.name,
animal.createdAt,
animal.shelter.name.name,
animal.shelter.addressInfo.address,
ExpressionUtils.as(
select(animalImage.imageUrl)
.from(animalImage)
.where(animalImage.animalImageId.eq(
select(animalImage.animalImageId.min())
.from(animalImage)
.where(animalImage.animal.eq(animal)
))), "animalImageUrl")
))
...
์ด์ ํ ๋ฒ์ ์ฟผ๋ฆฌ๋ก ํ์ํ ํ๋๋ง ์ ํํด์ ๊ฐ์ ธ์ฌ ์ ์๊ฒ ๋์๋ค.
Hibernate:
select
a1_0.animal_id,
a1_0.name,
a1_0.created_at,
s1_0.name,
s1_0.address,
(select
a2_0.image_url
from
animal_image a2_0
where
a2_0.animal_image_id=(
select
min(a3_0.animal_image_id)
from
animal_image a3_0
where
a3_0.animal_id=a1_0.animal_id
)
)
from
animal a1_0
join
shelter s1_0
on s1_0.shelter_id=a1_0.shelter_id
...
postman ์ผ๋ก ์๋ต ์๊ฐ์ ํ ์คํธํด ๋ณด๋ ๋ชจ๋ ์ฟผ๋ฆฌ๋ฅผ ๊ฐ์ ธ์ฌ ๊ฒฝ์ฐ 1.5์ด, ํ์ํ ๋ฐ์ดํฐ๋ง ๊ฐ์ ธ์ฌ ๊ฒฝ์ฐ 0.13์ด๋ก ์ง์ฐ ์๊ฐ์ด ์ฝ 1/10์ผ๋ก ๊ฐ์๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
๐ ๊ฒฐ๋ก
N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๋ฐฉ๋ฒ์๋ ์ฌ๋ฌ๊ฐ์ง๊ฐ ์๊ณ ์ํฉ๋ง๋ค ์ ์ ํ ํด๊ฒฐ์ฑ ์ด ์กด์ฌํ๋ค.
fetch join
- toOne ์ฐ๊ด๊ด๊ณ์ผ ๊ฒฝ์ฐ
- toMany ์ฐ๊ด๊ด๊ณ์ ๋จ์ผ ์กฐํ์ผ ๊ฒฝ์ฐ
BatchSize
- toMany ์ฐ๊ด๊ด๊ณ์ ๋ฉํฐ ์กฐํ์ด๋ฉฐ ํ์ด์ง๋ค์ด์ ์ด ํ์ํ ๊ฒฝ์ฐ
- toMany ์ฐ๊ด๊ด๊ณ์์ ์ปฌ๋ ์ ์ด ๋ ๊ฐ ์ด์ ์กด์ฌํ๋ ๊ฒฝ์ฐ
dto Projection
- ์ค์ ์ฌ์ฉํ๋ ์ปฌ๋ผ๋ณด๋ค ๋ถ๋ฌ์ค๋ ์ปฌ๋ผ์ด ํจ์ฌ ๋ง์ ์ฑ๋ฅ ๊ฐ์ ์ด ํ์ํ ๊ฒฝ์ฐ
- ์์์ฑ ์ปจํ ์คํธ์ ๊ด๋ฆฌ๊ฐ ํ์ ์๋ ๊ฒฝ์ฐ