ํฐ์คํ ๋ฆฌ ๋ทฐ
[SPRING] Spring Data JPA๋ก ๋์ ์ฟผ๋ฆฌ์ ํ์ด์ง ์ต์ ํํ๊ธฐ: Specification๊ณผ @EntityGraph ํ์ฉ
chaewonni 2024. 11. 5. 18:101. ๐ซ ๋์
ํ๋ก์ ํธ๋ฅผ ์งํํ๋ฉด์ ๋์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์กฐํํด์ผ ํ ์ํฉ์ด ๋ง์๋ค. category, memberId, ์ ๋ ฌ ๊ธฐ์ค ๋ฑ ์กฐํ ์กฐ๊ฑด์ด ์์ฃผ ๋ฐ๋์๊ณ , ์ด ์กฐ๊ฑด๋ค์ ์กฐํฉํ์ฌ ์ ์ฐํ๊ฒ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ ์ ์๋ ๋ฐฉ์์ด ํ์ํ๋ค. ํนํ, ์กฐ๊ฑด์ด ๋น๋ฒํ๊ฒ ๋ณ๊ฒฝ๋๊ฑฐ๋ ์ํฉ์ ๋ฐ๋ผ ์ถ๊ฐ๋๋ ๊ฒฝ์ฐ, ์ฝ๋๊ฐ ๋ณต์กํด์ง๊ณ ์ ์ง๋ณด์๋ ์ด๋ ค์์ง ๊ฐ๋ฅ์ฑ์ด ์ปธ๋ค.
์ด๋ Spring Data JPA์ Specification ๊ธฐ๋ฅ์ ์๊ฒ ๋์๊ณ , ์ด๋ฅผ ํตํด ๋ค์ํ ํํฐ ์กฐ๊ฑด์ ์ ์ฐํ๊ฒ ์ถ๊ฐํ๊ณ ์กฐํฉํ์ฌ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ์กฐํํ ์ ์์์ ์๊ฒ๋์๋ค. Specification์ ํ์ฉํ๋ฉด ์ฝ๋์ ๋ณต์ก์ฑ์ ์ค์ด๊ณ , ์กฐ๊ฑด์ด ๋ณํด๋ ์ฟผ๋ฆฌ๋ฅผ ์ฝ๊ฒ ๊ด๋ฆฌํ ์ ์๊ธฐ ๋๋ฌธ์ด๋ค.
- ๋์ ์กฐ๊ฑด ์ถ๊ฐ: Specification์ ์ฌ์ฉํ๋ฉด ์ฌ๋ฌ ํํฐ ์กฐ๊ฑด์ ์ํฉ์ ๋ฐ๋ผ ๋์ ์ผ๋ก ์กฐํฉํ ์ ์๋ค. ์๋ฅผ ๋ค์ด, memberId, category, ์ ๋ ฌ ์กฐ๊ฑด ๋ฑ์ ํ์ํ ๋๋ก ์ถ๊ฐํ ์ ์๋ค.
- JPA์์ ํตํฉ: JpaSpecificationExecutor์ ํจ๊ป ์ฌ์ฉํ์ฌ, Specification์ ํตํด ๋ง๋ค์ด์ง ๋์ ์ฟผ๋ฆฌ์ ํ์ด์ง(page, size)์ ์์ฐ์ค๋ฝ๊ฒ ๊ฒฐํฉํ ์ ์๋ค.
2. ๐ซ ์ฌ์ฉ ๊ณผ์
public class DiarySpecification {
public static Specification<DiaryEntity> withCategoryAndIsPublic(Category category) {
return (Root<DiaryEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) ->
category != null
? cb.and(cb.isTrue(root.get("isPublic")), cb.equal(root.get("category"), category))
: cb.isTrue(root.get("isPublic"));
}
public static Specification<DiaryEntity> orderByCreatedAt() {
return (Root<DiaryEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
query.orderBy(cb.desc(root.get("createdAt")));
return query.getRestriction();
};
}
public static Specification<DiaryEntity> orderByContentLength() {
return (Root<DiaryEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
query.orderBy(cb.desc(cb.length(root.get("content"))));
return query.getRestriction();
};
}
}
๐ Specification๊ณผ Query ๊ฐ์ด ์ฌ์ฉ ๋ถ๊ฐ
์ฒ์์ @Query์ Specification์ ํจ๊ป ์ฌ์ฉํ์ฌ, @Query๋ฅผ ์ฌ์ฉํ์ฌ ํน์ ํํฐ(์: memberId์ ๊ธฐ๋ณธ ์กฐ๊ฑด)๋ฅผ ์ฒ๋ฆฌํ๊ณ , ๋๋จธ์ง ์กฐ๊ฑด์ Specification์ ํตํด ๋์ ์ผ๋ก ์ถ๊ฐํ๋ ๋ฐฉ์์ ์ ํํ๋ค.
@Query("SELECT d FROM DiaryEntity d JOIN FETCH d.soptMember m " +
"WHERE m.id = :memberId ")
Page<DiaryEntity> findByMemberId(
Long memberId, Specification<DiaryEntity> specification, Pageable pageable
);
๊ทธ๋ฌ๋๋
Could not create query for public abstract org.springframework.data.domain.Page org.sopt.diary.domain.repository.DiaryRepository.findByMemberId(java.lang.Long,org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable);
Reason: Using named parameters for method public abstract org.springframework.data.domain.Page org.sopt.diary.domain.repository.DiaryRepository.findByMemberId(java.lang.Long,org.springframework.data.jpa.domain.Specification,org.springframework.data.domain.Pageable) but parameter 'Optional[specification]' not found in annotated query 'SELECT d FROM DiaryEntity d JOIN FETCH d.soptMember m WHERE m.id = :memberId '
์ด๋ฐ ์๋ฌ๊ฐ ๋ฐ์ํ๋๋ฐ, ์ด๋ Spring Data JPA๋ @Query์ Specification์ ๋์์ ์ฌ์ฉํ๋ ๊ฒ์ ์ง์ํ์ง ์๊ธฐ ๋๋ฌธ์, @Query ๋ฐฉ์์ผ๋ก Specification์ ์ฒ๋ฆฌํ ์ ์๋ค๊ณ ํ๋ค. Specification๊ณผ @Query๋ ๋ณ๋๋ก ์๋ํ๋ฏ๋ก, @Query ๋ฉ์๋์ Specification ํ๋ผ๋ฏธํฐ๋ฅผ ์ถ๊ฐํ๋ฉด ์๋ฌ๊ฐ ๋ฐ์ํ๋ค.
๊ทธ๋์ @Query๋ฅผ ์ ๊ฑฐํ๊ณ ๊ทธ๋ฅ JPA ๋ฉ์๋ ์ด๋ฆ ๊ธฐ๋ฐ ์ฟผ๋ฆฌ๋ก ์ฒ๋ฆฌํ๋๋ก
Page<DiaryEntity> findBySoptMemberId(
Long memberId, Specification<DiaryEntity> specification, Pageable pageable
);
๋ก ๋ฐ๊ฟ์ฃผ์๋๋ฐ,
ERROR 42274 --- [nio-8080-exec-1] o.s.d.c.advice.GlobalExceptionHandler : Unhandled exception occurred: At least 2 parameter(s) provided but only 1 parameter(s) present in query
๋ ์ด๋ฐ ์๋ฌ๊ฐ ๋ฐ์ํ๋ค. ์ด ์๋ฌ๋ ์ฟผ๋ฆฌ์ ์ ๋ฌ๋ ํ๋ผ๋ฏธํฐ ์๊ฐ ์ค์ ํ์ํ ํ๋ผ๋ฏธํฐ ์์ ์ผ์นํ์ง ์๋ค๋ ๋ฉ์ธ์ง์๋๋ฐ, ์ด๋ findBySoptMemberId ๋ฉ์๋๋ฅผ ์ฌ์ฉํ ๋ Specification์ ํจ๊ป ์ ๋ฌํ ์ ์๋ค๋ ๊ฒ์ด์๋ค. Specification์ ๋ณ๋๋ก ์ฌ์ฉํ๋ ค๋ฉด findAll ๋ฉ์๋์ ๊ฒฐํฉํด์ผ ํ๋ค๊ณ ํ๋ค.
Specification๊ณผ ํจ๊ป ์ฌ์ฉ ๊ฐ๋ฅํ ๋ฉ์๋
JpaSpecificationExecutor ์ธํฐํ์ด์ค๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ค์ ๋ฉ์๋๋ค์์๋ง Specification์ ์ฌ์ฉํ ์ ์๋๋ก ์ ๊ณตํ๋ค:
- findAll(Specification<T> spec): Specification์ ์ฌ์ฉํด ๋ชจ๋ ์กฐ๊ฑด์ ํํฐ๋งํ๊ณ ์กฐํ
- findAll(Specification<T> spec, Pageable pageable): Specification๊ณผ ํ์ด์ง์ ํจ๊ป ์ฌ์ฉํด ์กฐํ
- findOne(Specification<T> spec): ๋จ์ผ ๊ฒฐ๊ณผ๋ฅผ ์กฐํํ ๋ ์ฌ์ฉ
- count(Specification<T> spec): ์กฐ๊ฑด์ ๋ง๋ ์ํฐํฐ์ ๊ฐ์๋ฅผ ์กฐํ
- exists(Specification<T> spec): ์กฐ๊ฑด์ ๋ง๋ ์ํฐํฐ๊ฐ ์กด์ฌํ๋์ง ํ์ธ
๋ฐ๋ผ์ Specification์์ memberId์ ์ถ๊ฐ ์กฐ๊ฑด์ ํจ๊ป ์ฒ๋ฆฌํ๊ณ , findAll ๋ฉ์๋๋ก ์กฐํํ๋ ๋ฐฉ์์ ์ต์ข ์ ์ผ๋ก ์ ํํ๊ฒ ๋์๋ค.
//service
Page<DiaryEntity> diaryEntities = diaryRepository.findAll(specification, pageable);
// repository
Page<DiaryEntity> findAll(Specification<DiaryEntity> specification, Pageable pageable);
๐ Specification์์ JOIN FETCH๊ฐ ์๋จ
N+1 ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด soptMember JOIN FETCH ์ค์ ์ Specification์ ํด์ฃผ์๋๋ฐ,
public static Specification<DiaryEntity> withCategoryAndIsPublic(Category category) {
return (Root<DiaryEntity> root, CriteriaQuery<?>query, CriteriaBuilder cb) -> {
root.fetch("soptMember"); // JOIN FETCH ์ค์
query.distinct(true); // ์ค๋ณต ์ ๊ฑฐ
return cb.and(
cb.equal(root.get("category"), category),
cb.isTrue(root.get("isPublic")) // isPublic์ด true์ธ ๊ฒฝ์ฐ๋ง
);
};
}
Unhandled exception occurred: org.hibernate.query.SemanticException: Query specified join fetching, but the owner of the fetched association was not present in the select list [SqmSingularJoin(org.sopt.diary.domain.DiaryEntity(12).soptMember(13) : soptMember)]
์์ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
@ManyToOne ๊ด๊ณ๋ผ ํ์ด์ง๊ณผ JOIN FETCH ์กฐํฉ์์ ๋ฌธ์ ๊ฐ ์์ ์ค ์์๋๋ฐ, ์ฐพ์๋ณด๋ ์ด ์๋ฌ๋ Specification์์ JOIN FETCH๋ฅผ ์ฌ์ฉํ๋ฉด์ ํ์ด์ง์ ํ๋ ค๊ณ ํ ๋ ๋ฐ์ํ๋ Hibernate์ ์ ์ฝ ์ฌํญ ๋๋ฌธ์ ๋ฐ์ํ๋ค๊ณ ํ๋ค.
JOIN FETCH์ ํ์ด์ง์ ํจ๊ป ์ฌ์ฉํ๋ฉด Hibernate๋ ๊ธฐ๋ณธ์ ์ผ๋ก ํ์น ์กฐ์ธ์ ์ฌ์ฉํ ์ํฐํฐ๋ฅผ select ๋์์ ํฌํจ์ํค์ง ๋ชปํด ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์๋ค.
@ManyToOne ๊ด๊ณ๋ผ๋ ํ์ด์ง๊ณผ Criteria API ๊ธฐ๋ฐ์ Specification์ JOIN FETCH ์กฐํฉ์์ Hibernate์ ์ ํ์ด ๋ฐ์ํ ์ ์๋ค๊ณ ํ๋ค.
๋ฐ๋ผ์ @EntityGraph๋ฅผ ํ์ฉํ์ฌ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด์ฃผ์๋ค.
๐ @EntityGraph ํ์ฉ
์ผ๊ธฐ ๋ชฉ๋ก์ ์กฐํํ ๋,
// ์ผ๊ธฐ ๋ชฉ๋ก ์กฐํ
@Transactional(readOnly = true)
public DiaryListResponse getDiaryList(
final Category category,
final String sortBy,
Pageable pageable
) {
Specification<DiaryEntity> specification = createSpecificationWithSorting(category, null, sortBy);
pageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize());
Page<DiaryEntity> diaryEntities = diaryRepository.findAll(specification, pageable);
return DiaryListResponse.from(diaryEntities);
}
์ด๋ฐ์์ผ๋ก Specification ๊ฐ์ฒด๋ฅผ ํตํด ๋์ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌ์ฑํ๊ณ , diaryRepository์์ findAll ๋ฉ์๋๋ฅผ ํตํด ์กฐ๊ฑด๊ณผ ํ์ด์ง์ ๋ง๋ ์ผ๊ธฐ ๋ชฉ๋ก์ ์กฐํํ๋๋ฐ, ์ด ๋ findAll๋ก ์ผ๊ธฐ ๋ชฉ๋ก์ ๊ฐ์ ธ์ค๋ ๋ฉ์๋์
@Override
@EntityGraph(attributePaths = {"soptMember"})
Page<DiaryEntity> findAll(Specification<DiaryEntity> specification, Pageable pageable);
์ด๋ ๊ฒ @EntityGraph(attributePaths = {"soptMember"})๋ฅผ ์ ์ฉํ์ฌ soptMember๋ฅผ ์ฆ์ ๋ก๋ฉ(fetch join)ํ๋๋ก ์ค์ ํ์๋ค.
3. ๐ซ ์ต์ข ์ฝ๋
//Service
// ๋ด ์ผ๊ธฐ ๋ชฉ๋ก ์กฐํ
@Transactional(readOnly = true)
public MyDiaryListResponse getMyDiaryList(
final Long memberId,
final Category category,
final String sortBy,
Pageable pageable
) {
Specification<DiaryEntity> specification = createSpecificationWithSorting(category, memberId, sortBy);
pageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize());
memberService.findById(memberId);
Page<DiaryEntity> diaryEntities = diaryRepository.findAll(specification, pageable);
return MyDiaryListResponse.from(diaryEntities);
}
private Specification<DiaryEntity> createSpecificationWithSorting(Category category, Long memberId, String sortBy) {
Specification<DiaryEntity> specification;
if (memberId != null) {
specification = DiarySpecification.withMemberIdAndCategory(memberId, category);
} else {
specification = DiarySpecification.withCategoryAndIsPublic(category);
}
// ์ ๋ ฌ ์กฐ๊ฑด ์ถ๊ฐ
if (CREATED_AT.equals(sortBy)) {
specification = specification.and(DiarySpecification.orderByCreatedAt());
} else if (CONTENT_LENGTH.equals(sortBy)) {
specification = specification.and(DiarySpecification.orderByContentLength());
}
return specification;
}
// Specification
public class DiarySpecification {
public static Specification<DiaryEntity> withCategoryAndIsPublic(Category category) {
return (Root<DiaryEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) ->
category != null
? cb.and(cb.isTrue(root.get("isPublic")), cb.equal(root.get("category"), category))
: cb.isTrue(root.get("isPublic"));
}
public static Specification<DiaryEntity> withMemberIdAndCategory(Long memberId, Category category) {
return (root, query, cb) -> {
Join<Object, Object> memberJoin = root.join("soptMember"); // soptMember ํ๋ ์กฐ์ธ
return category != null
? cb.and(cb.equal(memberJoin.get("id"), memberId), cb.equal(root.get("category"), category))
: cb.equal(memberJoin.get("id"), memberId);
};
}
public static Specification<DiaryEntity> orderByCreatedAt() {
return (Root<DiaryEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
query.orderBy(cb.desc(root.get("createdAt")));
return query.getRestriction();
};
}
public static Specification<DiaryEntity> orderByContentLength() {
return (Root<DiaryEntity> root, CriteriaQuery<?> query, CriteriaBuilder cb) -> {
query.orderBy(cb.desc(cb.length(root.get("content"))));
return query.getRestriction();
};
}
}
// repository
public interface DiaryRepository extends JpaRepository<DiaryEntity, Long>, JpaSpecificationExecutor<DiaryEntity> {
@Override
@EntityGraph(attributePaths = {"soptMember"})
Page<DiaryEntity> findAll(Specification<DiaryEntity> specification, Pageable pageable);
}
์์ ๊ฐ์ ๋ฐฉ์์ผ๋ก ๊ตฌํํ๋ฉด, ๋ค์ํ ํํฐ ์กฐ๊ฑด์ ๋์ ์ผ๋ก ์ถ๊ฐํ๋ฉด์๋ ์ฑ๋ฅ์ ์ต์ ํํ ์ ์๋ค.
Specification๊ณผ @EntityGraph๋ฅผ ์กฐํฉํ๋ฉด ์๋์ ๊ฐ์ ์ฅ์ ์ ์ป์ ์ ์๋ค.
- ์ ์ฐํ ์กฐ๊ฑด ์ถ๊ฐ: Specification์ ํตํด ๋ค์ํ ํํฐ ์กฐ๊ฑด์ ๋์ ์ผ๋ก ์กฐํฉํ์ฌ ์ ์ฉํ ์ ์๋ค.
- N+1 ๋ฌธ์ ํด๊ฒฐ: @EntityGraph๋ก ํ์ด์ง๊ณผ ํจ๊ป JOIN FETCH ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ , ํ์ํ ์ฐ๊ด ๋ฐ์ดํฐ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ฐ์ ธ์ฌ ์ ์๋ค.
๋ค์์ QueryDSL์ ์ด์ฉํ์ฌ ๋์ ์ฟผ๋ฆฌ๋ฅผ ๊ตฌํํด๋ณด์์ผ๊ฒ ๋ค!
'Spring' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Elasticsearch ์ ์ฉ๊ธฐ] #1 Spring์์ Elasticsearch๋ก ๊ฒ์ ์ฑ๋ฅ ์ต์ ํํ๊ธฐ (1) | 2025.02.18 |
---|---|
[์บก์คํค ํ๋ก์ ํธ] Spring Boot HTTPS๋ก ๋ฐฐํฌํ๊ธฐ (0) | 2024.11.26 |
[SPRING] Hibernate ์ง์ฐ ๋ก๋ฉ์ผ๋ก ์ธํ JSON ์ง๋ ฌํ ์ค๋ฅ ํด๊ฒฐํ๊ธฐ (0) | 2024.06.07 |
[SPRING] ๋ก๊น ์ AOP ์ ์ฉํด๋ณด๊ธฐ (3) | 2024.06.01 |
[SPRING] ๋ก๊น ์ ๋ํด์ ์์๋ณด์ (0) | 2024.05.25 |
- Total
- Today
- Yesterday
- SQL ๋ ๋ฒจ์
- ์คํ๋ง
- ์นMVC
- elasticsearch
- ์ปค๋ฎค๋ํฐ
- ๋น์์
- ๋ก๊น
- ๋ฐฑ์ค ํ์ด์ฌ
- ๋ค์ด๋๋ฏน ํ๋ก๊ทธ๋๋ฐ
- ์์
- SQLD
- ์ง์ฐ๋ก๋ฉ
- EnumType.ORDINAL
- ์ธํ ๋ฆฌ์ ์ด
- ์ค์์
- ๋ก๊ทธ์์
- ์คํ๋ง ์ปค๋ฎค๋ํฐ
- ๋ถ๋งํฌ
- ์๋ฐ ์คํ๋ง
- ํ๋ก ํธ์๋
- ํ์ด์ฌ
- SQL
- DP
- ํ์ํํด
- ์คํ๋ง ๋ถ๋งํฌ
- JPA
- ์๋ฐ
- ์คํ๋ง๋ถํธ
- ๋ฐฑ์ค
- ์น MVC
์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
---|---|---|---|---|---|---|
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 |
31 |